中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Spring Boot集成Spring Security實現OAuth 2.0登錄

發布時間:2020-07-05 16:16:47 來源:網絡 閱讀:833 作者:川川Jason 欄目:軟件技術

Spring Security OAuth項目已棄用,最新的OAuth 2.0支持由Spring Security提供。目前Spring Security尚不支持Authorization Server,仍需使用Spring Security OAuth項目,但最終將被Spring Security完全取代。

本文介紹了Spring Security OAuth3 Client的基礎知識,如何利用Spring Security實現微信OAuth 2.0登錄。GitHub源碼wechat-api。

Spring Boot版本:2.2.2.RELEASE

為使用Spring Security OAuth3 Client,僅需在Spring Boot項目中增加以下依賴:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth3-client'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    ...
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
    testImplementation 'org.springframework.security:spring-security-test'
}

GitHub登錄

Spring Security(CommonOAuth3Provider)預定義了Google、GitHub、Facebook和Okta的OAuth Client配置,其中GitHub的定義如下:

private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth3/code/{registrationId}";

GITHUB {

    @Override
    public Builder getBuilder(String registrationId) {
        ClientRegistration.Builder builder = getBuilder(registrationId,
                ClientAuthenticationMethod.BASIC, DEFAULT_REDIRECT_URL);
        builder.scope("read:user");
        builder.authorizationUri("https://github.com/login/oauth/authorize");
        builder.tokenUri("https://github.com/login/oauth/access_token");
        builder.userInfoUri("https://api.github.com/user");
        builder.userNameAttributeName("id");
        builder.clientName("GitHub");
        return builder;
    }
}

為實現GitHub OAuth登錄,僅需兩步:

配置OAuth App

登錄GitHub,依次進入Settings -> Developer settings -> OAuth Apps,然后點擊New OAuth App:
Spring Boot集成Spring Security實現OAuth 2.0登錄
其中Authorization callback URL即OAuth Redirect URL,默認為{baseUrl}/login/oauth3/code/{registrationId},registrationId為github,這里我們僅為測試可以輸入http://localhost/login/oauth3/code/github 。
保存后會生成Client ID和Client Secret。

配置GitHub Client

spring:
  security:
    oauth3:
      client:
        registration:
          github:
            client-id: 34fbdcaae11111111111
            client-secret: ca32a5ea5ad4b357777777777777777777777777

配置完畢后啟動Spring Boot項目,從瀏覽器訪問則會自動跳轉到GitHub登錄頁面:
Spring Boot集成Spring Security實現OAuth 2.0登錄
如果配置了多個Client,則會跳轉到登錄選擇頁面:
Spring Boot集成Spring Security實現OAuth 2.0登錄
默認,OAuth 2.0 Login Page由DefaultLoginPageGeneratingFilter自動生成,每個clientName一個鏈接。默認鏈接地址為OAuth3AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"。

微信

注冊帳號

根據需要在微信開放平臺或微信公眾平臺注冊帳號,注冊成功后會獲得Client ID和Client Secret,不再贅述。
我使用了微信公眾平臺的網頁授權服務。微信網頁授權是通過OAuth3.0的Authorization Code機制實現的:

  1. 用戶進入授權頁面同意授權,獲取code
  2. 通過code換取網頁授權access_token(與基礎支持中的access_token不同)
  3. 通過網頁授權access_token和openid獲取用戶基本信息(支持UnionID機制)

配置微信Client

spring:
  security:
    oauth3:
      client:
        registration:
          weixin:
            client-id: wx2226666666666666
            client-secret: 39899999999999999999999999999999
            redirect-uri: http://wechat.itrunner.org/login/oauth3/code/weixin
            authorization-grant-type: authorization_code
            scope: snsapi_userinfo
            client-name: WeiXin
        provider:
          weixin:
            authorization-uri: https://open.weixin.qq.com/connect/oauth3/authorize
            token-uri: https://api.weixin.qq.com/sns/oauth3/access_token
            user-info-uri: https://api.weixin.qq.com/sns/userinfo
            user-name-attribute: openid

說明,為了安全,實際應用中應使用https。

自定義實現

微信OAuth 2.0請求參數、請求方法和返回類型均與Spring Security的默認實現不一致,需要自定義實現。
OAuth3LoginSecurityConfig

package org.itrunner.wechat.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth3.client.registration.ClientRegistrationRepository;

@EnableWebSecurity
public class OAuth3LoginSecurityConfig extends WebSecurityConfigurerAdapter {
    @Value("${security.ignore-paths}")
    private String[] ignorePaths;

    private final ClientRegistrationRepository clientRegistrationRepository;

    public OAuth3LoginSecurityConfig(ClientRegistrationRepository clientRegistrationRepository) {
        this.clientRegistrationRepository = clientRegistrationRepository;
    }

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers(ignorePaths);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().headers().disable()
                .oauth3Login(oauth3Login ->
                        oauth3Login.authorizationEndpoint(authorizationEndpoint ->
                                authorizationEndpoint.authorizationRequestResolver(new WeChatOAuth3AuthorizationRequestResolver(this.clientRegistrationRepository))
                        ).tokenEndpoint(tokenEndpoint ->
                                tokenEndpoint.accessTokenResponseClient(new WeChatAuthorizationCodeTokenResponseClient())
                        ).userInfoEndpoint(userInfoEndpoint ->
                                userInfoEndpoint.userService(new WeChatOAuth3UserService()))
                ).authorizeRequests(authorizeRequests ->
                authorizeRequests.anyRequest().authenticated());
    }
}

在configure(HttpSecurity http)中調用oauth3Login()定義authorization、token和userInfo的實現方法。
Authorization
微信獲取code的鏈接如下:

https://open.weixin.qq.com/connect/oauth3/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

Spring Security默認實現為DefaultOAuth3AuthorizationRequestResolver,自定義實現WeChatOAuth3AuthorizationRequestResolver如下:

package org.itrunner.wechat.config;

import org.springframework.security.oauth3.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth3.client.web.DefaultOAuth3AuthorizationRequestResolver;
import org.springframework.security.oauth3.client.web.OAuth3AuthorizationRequestResolver;
import org.springframework.security.oauth3.core.endpoint.OAuth3AuthorizationRequest;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_AUTHORIZATION_REQUEST_URL_FORMAT;
import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_REGISTRATION_ID;
import static org.springframework.security.oauth3.client.web.OAuth3AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;

public class WeChatOAuth3AuthorizationRequestResolver implements OAuth3AuthorizationRequestResolver {
    private static final String WEIXIN_DEFAULT_SCOPE = "snsapi_userinfo";
    private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";

    private final OAuth3AuthorizationRequestResolver defaultAuthorizationRequestResolver;
    private final AntPathRequestMatcher authorizationRequestMatcher;

    public WeChatOAuth3AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        this.defaultAuthorizationRequestResolver = new DefaultOAuth3AuthorizationRequestResolver(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
        this.authorizationRequestMatcher = new AntPathRequestMatcher(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");
    }

    @Override
    public OAuth3AuthorizationRequest resolve(HttpServletRequest request) {
        String clientRegistrationId = this.resolveRegistrationId(request);

        OAuth3AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request);

        return resolve(authorizationRequest, clientRegistrationId);
    }

    @Override
    public OAuth3AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
        OAuth3AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId);

        return resolve(authorizationRequest, clientRegistrationId);
    }

    private OAuth3AuthorizationRequest resolve(OAuth3AuthorizationRequest authorizationRequest, String registrationId) {
        if (authorizationRequest == null) {
            return null;
        }

        // 如不是WeiXin則使用默認實現
        if (!WEIXIN_REGISTRATION_ID.equals(registrationId)) {
            return authorizationRequest;
        }

        // 微信Authorization Request URL
        String authorizationRequestUri = String.format(WEIXIN_AUTHORIZATION_REQUEST_URL_FORMAT, authorizationRequest.getAuthorizationUri(), authorizationRequest.getClientId(),
                encodeURL(authorizationRequest.getRedirectUri()), authorizationRequest.getResponseType().getValue(), getScope(authorizationRequest), authorizationRequest.getState());

        OAuth3AuthorizationRequest.Builder builder = OAuth3AuthorizationRequest.from(authorizationRequest);
        builder.authorizationRequestUri(authorizationRequestUri);

        return builder.build();
    }

    private String resolveRegistrationId(HttpServletRequest request) {
        if (this.authorizationRequestMatcher.matches(request)) {
            return this.authorizationRequestMatcher.matcher(request).getVariables().get(REGISTRATION_ID_URI_VARIABLE_NAME);
        }
        return null;
    }

    private static String encodeURL(String url) {
        try {
            return URLEncoder.encode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            // The system should always have the platform default
            return null;
        }
    }

    private static String getScope(OAuth3AuthorizationRequest authorizationRequest) {
        return authorizationRequest.getScopes().stream().findFirst().orElse(WEIXIN_DEFAULT_SCOPE);
    }
}

Access Token
微信獲取Access Token的鏈接如下:

https://api.weixin.qq.com/sns/oauth3/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

Spring Security默認實現類為DefaultAuthorizationCodeTokenResponseClient、OAuth3AuthorizationCodeGrantRequestEntityConverter、OAuth3AccessTokenResponse,自定義實現分別為WeChatAuthorizationCodeTokenResponseClient、WeChatAuthorizationCodeGrantRequestEntityConverter、WeChatAccessTokenResponse。

WeChatAuthorizationCodeTokenResponseClient執行請求獲取Token:

package org.itrunner.wechat.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth3.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth3.client.endpoint.OAuth3AccessTokenResponseClient;
import org.springframework.security.oauth3.client.endpoint.OAuth3AuthorizationCodeGrantRequest;
import org.springframework.security.oauth3.client.http.OAuth3ErrorResponseErrorHandler;
import org.springframework.security.oauth3.core.OAuth3AuthorizationException;
import org.springframework.security.oauth3.core.OAuth3Error;
import org.springframework.security.oauth3.core.endpoint.OAuth3AccessTokenResponse;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_REGISTRATION_ID;

@Slf4j
public class WeChatAuthorizationCodeTokenResponseClient implements OAuth3AccessTokenResponseClient<OAuth3AuthorizationCodeGrantRequest> {
    private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";

    private Converter<OAuth3AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new WeChatAuthorizationCodeGrantRequestEntityConverter();

    private RestOperations restOperations;

    private DefaultAuthorizationCodeTokenResponseClient defaultAuthorizationCodeTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();

    public WeChatAuthorizationCodeTokenResponseClient() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new OAuth3ErrorResponseErrorHandler());
        this.restOperations = restTemplate;
    }

    @Override
    public OAuth3AccessTokenResponse getTokenResponse(OAuth3AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");

        // 如不是WeiXin則使用默認實現
        if (!authorizationCodeGrantRequest.getClientRegistration().getRegistrationId().equals(WEIXIN_REGISTRATION_ID)) {
            return defaultAuthorizationCodeTokenResponseClient.getTokenResponse(authorizationCodeGrantRequest);
        }

        // 調用WeChatAuthorizationCodeGrantRequestEntityConverter獲取request
        RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);

        ResponseEntity<String> response;
        try {
            // 執行request
            response = this.restOperations.exchange(request, String.class);
        } catch (RestClientException ex) {
            String description = "An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: ";
            log.error(description, ex);
            OAuth3Error oauth3Error = new OAuth3Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, description + ex.getMessage(), null);
            throw new OAuth3AuthorizationException(oauth3Error, ex);
        }

        // 解析response
        OAuth3AccessTokenResponse tokenResponse = WeChatAccessTokenResponse.build(response.getBody()).toOAuth3AccessTokenResponse();

        if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
            tokenResponse = OAuth3AccessTokenResponse.withResponse(tokenResponse)
                    .scopes(authorizationCodeGrantRequest.getClientRegistration().getScopes())
                    .build();
        }

        return tokenResponse;
    }
}

WeChatAuthorizationCodeGrantRequestEntityConverter構建Access Token RequestEntity:

package org.itrunner.wechat.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth3.client.endpoint.OAuth3AuthorizationCodeGrantRequest;
import org.springframework.security.oauth3.client.registration.ClientRegistration;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.Collections;

import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_ACCESS_TOKEN_URL_FORMAT;

@Slf4j
public class WeChatAuthorizationCodeGrantRequestEntityConverter implements Converter<OAuth3AuthorizationCodeGrantRequest, RequestEntity<?>> {
    @Override
    public RequestEntity<?> convert(OAuth3AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        HttpHeaders headers = getTokenRequestHeaders();
        URI uri = buildUri(authorizationCodeGrantRequest);
        return new RequestEntity<>(headers, HttpMethod.GET, uri);
    }

    private HttpHeaders getTokenRequestHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
        return headers;
    }

    private URI buildUri(OAuth3AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
        String tokenUri = clientRegistration.getProviderDetails().getTokenUri();
        String appid = clientRegistration.getClientId();
        String secret = clientRegistration.getClientSecret();
        String code = authorizationCodeGrantRequest.getAuthorizationExchange().getAuthorizationResponse().getCode();
        String grantType = authorizationCodeGrantRequest.getGrantType().getValue();

        String uriString = String.format(WEIXIN_ACCESS_TOKEN_URL_FORMAT, tokenUri, appid, secret, code, grantType);
        return UriComponentsBuilder.fromUriString(uriString).build().toUri();
    }
}

WeChatAccessTokenResponse解析Response:

package org.itrunner.wechat.config;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.itrunner.wechat.util.JsonUtils;
import org.springframework.security.oauth3.core.OAuth3AccessToken;
import org.springframework.security.oauth3.core.endpoint.OAuth3AccessTokenResponse;

import java.util.*;

@Getter
@Setter
@Slf4j
public class WeChatAccessTokenResponse {
    @JsonProperty("access_token")
    private String accessToken;
    @JsonProperty("expires_in")
    private Long expiresIn;
    @JsonProperty("refresh_token")
    private String refreshToken;
    private String openid;
    private String scope;

    private WeChatAccessTokenResponse() {

    }

    public static WeChatAccessTokenResponse build(String json) {
        try {
            return JsonUtils.parseJson(json, WeChatAccessTokenResponse.class);
        } catch (JsonProcessingException e) {
            log.error("An error occurred while attempting to parse the WeiXin Access Token Response: " + e.getMessage());
            return null;
        }
    }

    public OAuth3AccessTokenResponse toOAuth3AccessTokenResponse() {
        OAuth3AccessTokenResponse.Builder builder = OAuth3AccessTokenResponse.withToken(accessToken);
        builder.tokenType(OAuth3AccessToken.TokenType.BEARER);
        builder.expiresIn(expiresIn);
        builder.refreshToken(refreshToken);

        String[] scopes = scope.split(",");
        Set<String> scopeSet = new HashSet<>();
        Collections.addAll(scopeSet, scopes);
        builder.scopes(scopeSet);

        Map<String, Object> additionalParameters = new LinkedHashMap<>();
        additionalParameters.put("openid", openid);
        builder.additionalParameters(additionalParameters);
        return builder.build();
    }
}

User Info
微信獲取User Info的鏈接如下:

https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

Spring Security默認實現類為DefaultOAuth3UserService、OAuth3UserRequestEntityConverter、DefaultOAuth3User,自定義實現分別為WeChatOAuth3UserService、WeChatUserRequestEntityConverter、WeChatOAuth3User。

WeChatOAuth3UserService執行請求獲取User Info:

package org.itrunner.wechat.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.convert.converter.Converter;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth3.client.http.OAuth3ErrorResponseErrorHandler;
import org.springframework.security.oauth3.client.userinfo.DefaultOAuth3UserService;
import org.springframework.security.oauth3.client.userinfo.OAuth3UserRequest;
import org.springframework.security.oauth3.client.userinfo.OAuth3UserService;
import org.springframework.security.oauth3.core.OAuth3AuthenticationException;
import org.springframework.security.oauth3.core.OAuth3AuthorizationException;
import org.springframework.security.oauth3.core.OAuth3Error;
import org.springframework.security.oauth3.core.user.OAuth3User;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;

import java.io.UnsupportedEncodingException;

import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_REGISTRATION_ID;

@Slf4j
public class WeChatOAuth3UserService implements OAuth3UserService<OAuth3UserRequest, OAuth3User> {
    private static final String MISSING_USER_INFO_URI_ERROR_CODE = "missing_user_info_uri";
    private static final String MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE = "missing_user_name_attribute";
    private static final String INVALID_USER_INFO_RESPONSE_ERROR_CODE = "invalid_user_info_response";

    private Converter<OAuth3UserRequest, RequestEntity<?>> requestEntityConverter = new WeChatUserRequestEntityConverter();
    private RestOperations restOperations;

    private DefaultOAuth3UserService defaultOAuth3UserService = new DefaultOAuth3UserService();

    public WeChatOAuth3UserService() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new OAuth3ErrorResponseErrorHandler());
        this.restOperations = restTemplate;
    }

    @Override
    public OAuth3User loadUser(OAuth3UserRequest userRequest) throws OAuth3AuthenticationException {
        Assert.notNull(userRequest, "userRequest cannot be null");

        // 如不是WeiXin則使用默認實現
        if (!userRequest.getClientRegistration().getRegistrationId().equals(WEIXIN_REGISTRATION_ID)) {
            return defaultOAuth3UserService.loadUser(userRequest);
        }

        if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {
            OAuth3Error oauth3Error = new OAuth3Error(MISSING_USER_INFO_URI_ERROR_CODE,
                    "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "
                            + userRequest.getClientRegistration().getRegistrationId(),
                    null);
            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString());
        }

        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
        if (!StringUtils.hasText(userNameAttributeName)) {
            OAuth3Error oauth3Error = new OAuth3Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,
                    "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "
                            + userRequest.getClientRegistration().getRegistrationId(),
                    null);
            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString());
        }

        // 獲得request
        RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);

        ResponseEntity<String> response;
        try {
            // 執行request
            response = this.restOperations.exchange(request, String.class);
        } catch (OAuth3AuthorizationException ex) {
            OAuth3Error oauth3Error = ex.getError();
            StringBuilder errorDetails = new StringBuilder();
            errorDetails.append("Error details: [");
            errorDetails.append("UserInfo Uri: ").append(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri());
            errorDetails.append(", Error Code: ").append(oauth3Error.getErrorCode());
            if (oauth3Error.getDescription() != null) {
                errorDetails.append(", Error Description: ").append(oauth3Error.getDescription());
            }
            errorDetails.append("]");
            oauth3Error = new OAuth3Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString(), ex);
        } catch (RestClientException ex) {
            OAuth3Error oauth3Error = new OAuth3Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString(), ex);
        }

        // 解析response
        String userAttributes = response.getBody();
        try {
            // 編碼轉換
            userAttributes = new String(userAttributes.getBytes("ISO-8859-1"), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            log.error("An error occurred while attempting to encode userAttributes: " + e.getMessage());
        }
        return WeChatOAuth3User.build(userAttributes, userNameAttributeName);
    }
}

WeChatUserRequestEntityConverter構建Use Info RequestEntity:

package org.itrunner.wechat.config;

import org.springframework.core.convert.converter.Converter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.security.oauth3.client.registration.ClientRegistration;
import org.springframework.security.oauth3.client.userinfo.OAuth3UserRequest;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.util.Collections;

import static org.itrunner.wechat.config.WeChatConstants.WEIXIN_USER_INFO_URL_FORMAT;

public class WeChatUserRequestEntityConverter implements Converter<OAuth3UserRequest, RequestEntity<?>> {
    @Override
    public RequestEntity<?> convert(OAuth3UserRequest userRequest) {
        HttpHeaders headers = getUserRequestHeaders();
        URI uri = buildUri(userRequest);
        return new RequestEntity<>(headers, HttpMethod.GET, uri);
    }

    private HttpHeaders getUserRequestHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Collections.singletonList(MediaType.TEXT_PLAIN));
        return headers;
    }

    private URI buildUri(OAuth3UserRequest userRequest) {
        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        String uri = clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri();
        String accessToken = userRequest.getAccessToken().getTokenValue();
        String openId = (String) userRequest.getAdditionalParameters().get("openid");

        String userInfoUrl = String.format(WEIXIN_USER_INFO_URL_FORMAT, uri, accessToken, openId, "zh_CN");
        return UriComponentsBuilder.fromUriString(userInfoUrl).build().toUri();
    }
}

WeChatOAuth3User:

package org.itrunner.wechat.config;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.extern.slf4j.Slf4j;
import org.itrunner.wechat.util.JsonUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth3.core.user.OAuth3User;

import java.util.*;

@Slf4j
public class WeChatOAuth3User implements OAuth3User {
    private String openid;
    private String nickname;
    private int sex;
    private String language;
    private String city;
    private String province;
    private String country;
    private String headimgurl;
    private String[] privilege;

    @JsonIgnore
    private Set<GrantedAuthority> authorities = new HashSet<>();
    @JsonIgnore
    private Map<String, Object> attributes;
    @JsonIgnore
    private String nameAttributeKey;

    public static WeChatOAuth3User build(String json, String userNameAttributeName) {
        try {
            WeChatOAuth3User user = JsonUtils.parseJson(json, WeChatOAuth3User.class);
            user.nameAttributeKey = userNameAttributeName;
            user.setAttributes();
            user.setAuthorities();

            return user;
        } catch (JsonProcessingException e) {
            log.error("An error occurred while attempting to parse the weixin User Info Response: " + e.getMessage());
            return null;
        }
    }

    private void setAttributes() {
        attributes = new HashMap<>();

        this.attributes.put("openid", openid);
        this.attributes.put("nickname", nickname);
        this.attributes.put("sex", sex);
        this.attributes.put("language", language);
        this.attributes.put("city", city);
        this.attributes.put("province", province);
        this.attributes.put("country", country);
        this.attributes.put("headimgurl", headimgurl);
    }

    private void setAuthorities() {
        authorities = new LinkedHashSet<>();
        for (String authority : privilege) {
            authorities.add(new SimpleGrantedAuthority(authority));
        }
    }

    @Override
    public Map<String, Object> getAttributes() {
        return this.attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    @Override
    public String getName() {
        return this.getAttribute(this.nameAttributeKey).toString();
    }

    // getter and setter
    ....

OAuth3AuthenticationToken

package org.itrunner.wechat.util;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth3.client.authentication.OAuth3AuthenticationToken;
import org.springframework.security.oauth3.core.user.OAuth3User;

public final class OAuth3Context {
    private OAuth3Context() {
    }

    public static String getPrincipalName() {
        return getOAuth3AuthenticationToken().getName();
    }

    public static String getClientRegistrationId() {
        return getOAuth3AuthenticationToken().getAuthorizedClientRegistrationId();
    }

    public static OAuth3User getOAuth3User() {
        return getOAuth3AuthenticationToken().getPrincipal();
    }

    public static OAuth3AuthenticationToken getOAuth3AuthenticationToken() {
        return (OAuth3AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    }

}

OAuth3AccessToken

獲取OAuth3AccessToken的方法:

@Controller
public class OAuth3ClientController {

    @Autowired
    private OAuth3AuthorizedClientService authorizedClientService;

    @GetMapping("/")
    public String index() {
        OAuth3AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(OAuth3Context.getClientRegistrationId(), OAuth3Context.getPrincipalName());
        OAuth3AccessToken accessToken = authorizedClient.getAccessToken();
        ...

        return "index";
    }
}

@Controller
public class OAuth3ClientController {

    @GetMapping("/")
    public String index(@RegisteredOAuth3AuthorizedClient("weixin") OAuth3AuthorizedClient authorizedClient) {
        OAuth3AccessToken accessToken = authorizedClient.getAccessToken();
        ...

        return "index";
    }
}

測試

WithMockOAuth3User

package org.itrunner.wechat.base;

import org.springframework.security.test.context.support.WithSecurityContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockOAuth3User {
    String name() default "123456789";
}

WithMockCustomUserSecurityContextFactory

package org.itrunner.wechat.base;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth3.client.authentication.OAuth3AuthenticationToken;
import org.springframework.security.oauth3.core.user.OAuth3User;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth3User> {
    @Override
    public SecurityContext createSecurityContext(WithMockOAuth3User oauth3User) {
        OAuth3User principal = new OAuth3User() {
            @Override
            public Map<String, Object> getAttributes() {
                Map<String, Object> attributes = new HashMap<>();
                attributes.put("openid", oauth3User.name());
                return attributes;
            }

            @Override
            public Collection<? extends GrantedAuthority> getAuthorities() {
                return Collections.EMPTY_LIST;
            }

            @Override
            public String getName() {
                return oauth3User.name();
            }
        };

        OAuth3AuthenticationToken authenticationToken = new OAuth3AuthenticationToken(principal, Collections.emptyList(), "weixin");
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(authenticationToken);
        return context;
    }
}

測試示例

package org.itrunner.wechat.controller;

import org.itrunner.wechat.base.WithMockOAuth3User;
import org.itrunner.wechat.domain.Hero;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.itrunner.wechat.util.JsonUtils.asJson;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class HeroControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    @WithMockOAuth3User
    public void crudSuccess() throws Exception {
        Hero hero = new Hero();
        hero.setName("Jack");

        // add hero
        mvc.perform(post("/heroes").content(asJson(hero)).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(content().json("{'id':11, 'name':'Jack', 'createBy':'123456789'}"));

        // update hero
        hero.setId(11l);
        hero.setName("Jacky");
        mvc.perform(put("/heroes").content(asJson(hero)).contentType(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(content().json("{'name':'Jacky'}"));

        // find heroes by name
        mvc.perform(get("/heroes/?name=m").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());

        // get hero by id
        mvc.perform(get("/heroes/11").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk()).andExpect(content().json("{'name':'Jacky'}"));

        // delete hero successfully
        mvc.perform(delete("/heroes/11").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk());

        // delete hero
        mvc.perform(delete("/heroes/9999")).andExpect(status().is4xxClientError());
    }

    @Test
    @WithMockOAuth3User
    void addHeroValidationFailed() throws Exception {
        Hero hero = new Hero();
        mvc.perform(post("/heroes").content(asJson(hero)).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().is(400));
    }
}

微信開發者工具

下載安裝微信開發者工具,綁定開發者微信帳號,可以更方便、更安全地開發和調試基于微信的網頁。
Spring Boot集成Spring Security實現OAuth 2.0登錄

參考資料

OAuth Community Site
OAuth 2.0 Login Sample
微信官方文檔

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

鱼台县| 精河县| 吉首市| 马鞍山市| 吴堡县| 普格县| 九龙坡区| 灌阳县| 文昌市| 衡阳市| 元谋县| 梅州市| 龙江县| 神池县| 会泽县| 河池市| 顺平县| 施甸县| 林口县| 定安县| 房产| 都昌县| 西贡区| 定南县| 英德市| 雷波县| 金阳县| 龙里县| 色达县| 隆德县| 中西区| 织金县| 鱼台县| 石门县| 永泰县| 故城县| 杂多县| 禹州市| 扬中市| 商南县| 崇阳县|