Spring Boot implement OAuth 2.0 integration Spring Security Log

Spring Security OAuth project has been abandoned, the latest OAuth 2.0 support is provided by Spring Security. Spring Security is not currently supported Authorization Server, still need to use Spring Security OAuth project, but will eventually be completely replaced by Spring Security.

This article describes the basics of Spring Security OAuth2 Client, how to use Spring Security to achieve micro letter OAuth 2.0 login. GitHub source wechat-API .

Spring Boot version: 2.2.2.RELEASE

Using Spring Security OAuth2 Client, simply add the following dependence in Spring Boot project:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-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 Login

Spring Security (CommonOAuth2Provider) predefined Google, GitHub, Facebook and Okta of OAuth Client configuration, wherein GitHub defined as follows:

private static final String DEFAULT_REDIRECT_URL = "{baseUrl}/{action}/oauth2/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;
    }
}

To achieve GitHub OAuth login, only two steps:

Configuring OAuth App

Log in GitHub, turn into Settings -> Developer settings -> OAuth Apps, then click New OAuth App:
Spring Boot implement OAuth 2.0 integration Spring Security Log
which Authorization callback URL that is OAuth Redirect URL, the default is {baseUrl} / login / oauth2 / code / {registrationId}, registrationId to github, here we can only test the input http: // localhost / login / oauth2 / code / github.
After saving will generate Client ID and Client Secret.

GitHub Client Configuration

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

Once configured Spring Boot start the project, it will automatically jump from the browser to access the login page GitHub:
Spring Boot implement OAuth 2.0 integration Spring Security Log
You can configure multiple Client, you'll jump to the login selection page:
Spring Boot implement OAuth 2.0 integration Spring Security Log
default, OAuth 2.0 Login Page automatically generated by DefaultLoginPageGeneratingFilter, each a clientName a link. Default link address OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/ {registrationId}" .

Micro letter

Register Account

As needed WeChat open platform or micro-channel public platform registered account, after successful registration will get Client ID and Client Secret, not repeat them.
I use the micro-channel public platform web authorization service. Micro-letter web OAuth2.0 authorization is achieved through the mechanism of Authorization Code:

  1. 用户进入授权页面同意授权,获取code
  2. 通过code换取网页授权access_token(与基础支持中的access_token不同)
  3. 通过网页授权access_token和openid获取用户基本信息(支持UnionID机制)

配置微信Client

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

说明,为了安全,实际应用中应使用https。

自定义实现

微信OAuth 2.0请求参数、请求方法和返回类型均与Spring Security的默认实现不一致,需要自定义实现。
OAuth2LoginSecurityConfig

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.oauth2.client.registration.ClientRegistrationRepository;

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

    private final ClientRegistrationRepository clientRegistrationRepository;

    public OAuth2LoginSecurityConfig(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()
                .oauth2Login(oauth2Login ->
                        oauth2Login.authorizationEndpoint(authorizationEndpoint ->
                                authorizationEndpoint.authorizationRequestResolver(new WeChatOAuth2AuthorizationRequestResolver(this.clientRegistrationRepository))
                        ).tokenEndpoint(tokenEndpoint ->
                                tokenEndpoint.accessTokenResponseClient(new WeChatAuthorizationCodeTokenResponseClient())
                        ).userInfoEndpoint(userInfoEndpoint ->
                                userInfoEndpoint.userService(new WeChatOAuth2UserService()))
                ).authorizeRequests(authorizeRequests ->
                authorizeRequests.anyRequest().authenticated());
    }
}

在configure(HttpSecurity http)中调用oauth2Login()定义authorization、token和userInfo的实现方法。
Authorization
微信获取code的链接如下:

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

Spring Security默认实现为DefaultOAuth2AuthorizationRequestResolver,自定义实现WeChatOAuth2AuthorizationRequestResolver如下:

package org.itrunner.wechat.config;

import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
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.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;

public class WeChatOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
    private static final String WEIXIN_DEFAULT_SCOPE = "snsapi_userinfo";
    private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";

    private final OAuth2AuthorizationRequestResolver defaultAuthorizationRequestResolver;
    private final AntPathRequestMatcher authorizationRequestMatcher;

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

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

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

        return resolve(authorizationRequest, clientRegistrationId);
    }

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

        return resolve(authorizationRequest, clientRegistrationId);
    }

    private OAuth2AuthorizationRequest resolve(OAuth2AuthorizationRequest 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());

        OAuth2AuthorizationRequest.Builder builder = OAuth2AuthorizationRequest.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(OAuth2AuthorizationRequest authorizationRequest) {
        return authorizationRequest.getScopes().stream().findFirst().orElse(WEIXIN_DEFAULT_SCOPE);
    }
}

Access Token
微信获取Access Token的链接如下:

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

Spring Security默认实现类为DefaultAuthorizationCodeTokenResponseClient、OAuth2AuthorizationCodeGrantRequestEntityConverter、OAuth2AccessTokenResponse,自定义实现分别为WeChatAuthorizationCodeTokenResponseClient、WeChatAuthorizationCodeGrantRequestEntityConverter、WeChatAccessTokenResponse。

WeChatAuthorizationCodeTokenResponseClient execution Request 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.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient;
import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
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 OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
    private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";

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

    private RestOperations restOperations;

    private DefaultAuthorizationCodeTokenResponseClient defaultAuthorizationCodeTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();

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

    @Override
    public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest 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);
            OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, description + ex.getMessage(), null);
            throw new OAuth2AuthorizationException(oauth2Error, ex);
        }

        // 解析response
        OAuth2AccessTokenResponse tokenResponse = WeChatAccessTokenResponse.build(response.getBody()).toOAuth2AccessTokenResponse();

        if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {
            tokenResponse = OAuth2AccessTokenResponse.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.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest;
import org.springframework.security.oauth2.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<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> {
    @Override
    public RequestEntity<?> convert(OAuth2AuthorizationCodeGrantRequest 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(OAuth2AuthorizationCodeGrantRequest 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 analysis 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.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;

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 OAuth2AccessTokenResponse toOAuth2AccessTokenResponse() {
        OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken);
        builder.tokenType(OAuth2AccessToken.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
micro-channel acquisition User Info link as follows:

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

Spring Security default implementation class for DefaultOAuth2UserService, OAuth2UserRequestEntityConverter, DefaultOAuth2User, custom implementation are WeChatOAuth2UserService, WeChatUserRequestEntityConverter, WeChatOAuth2User.

WeChatOAuth2UserService execution Request 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.oauth2.client.http.OAuth2ErrorResponseErrorHandler;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.user.OAuth2User;
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 WeChatOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    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<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new WeChatUserRequestEntityConverter();
    private RestOperations restOperations;

    private DefaultOAuth2UserService defaultOAuth2UserService = new DefaultOAuth2UserService();

    public WeChatOAuth2UserService() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        this.restOperations = restTemplate;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        Assert.notNull(userRequest, "userRequest cannot be null");

        // 如不是WeiXin则使用默认实现
        if (!userRequest.getClientRegistration().getRegistrationId().equals(WEIXIN_REGISTRATION_ID)) {
            return defaultOAuth2UserService.loadUser(userRequest);
        }

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

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

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

        ResponseEntity<String> response;
        try {
            // 执行request
            response = this.restOperations.exchange(request, String.class);
        } catch (OAuth2AuthorizationException ex) {
            OAuth2Error oauth2Error = 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(oauth2Error.getErrorCode());
            if (oauth2Error.getDescription() != null) {
                errorDetails.append(", Error Description: ").append(oauth2Error.getDescription());
            }
            errorDetails.append("]");
            oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
        } catch (RestClientException ex) {
            OAuth2Error oauth2Error = new OAuth2Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,
                    "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);
            throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.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 WeChatOAuth2User.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.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
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<OAuth2UserRequest, RequestEntity<?>> {
    @Override
    public RequestEntity<?> convert(OAuth2UserRequest 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(OAuth2UserRequest 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();
    }
}

WeChatOAuth2User:

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.oauth2.core.user.OAuth2User;

import java.util.*;

@Slf4j
public class WeChatOAuth2User implements OAuth2User {
    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 WeChatOAuth2User build(String json, String userNameAttributeName) {
        try {
            WeChatOAuth2User user = JsonUtils.parseJson(json, WeChatOAuth2User.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
    ....

OAuth2AuthenticationToken

package org.itrunner.wechat.util;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;

public final class OAuth2Context {
    private OAuth2Context() {
    }

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

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

    public static OAuth2User getOAuth2User() {
        return getOAuth2AuthenticationToken().getPrincipal();
    }

    public static OAuth2AuthenticationToken getOAuth2AuthenticationToken() {
        return (OAuth2AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    }

}

OAuth2AccessToken

Get OAuth2AccessToken method:

@Controller
public class OAuth2ClientController {

    @Autowired
    private OAuth2AuthorizedClientService authorizedClientService;

    @GetMapping("/")
    public String index() {
        OAuth2AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(OAuth2Context.getClientRegistrationId(), OAuth2Context.getPrincipalName());
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
        ...

        return "index";
    }
}

or

@Controller
public class OAuth2ClientController {

    @GetMapping("/")
    public String index(@RegisteredOAuth2AuthorizedClient("weixin") OAuth2AuthorizedClient authorizedClient) {
        OAuth2AccessToken accessToken = authorizedClient.getAccessToken();
        ...

        return "index";
    }
}

test

WithMockOAuth2User

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 WithMockOAuth2User {
    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.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
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<WithMockOAuth2User> {
    @Override
    public SecurityContext createSecurityContext(WithMockOAuth2User oauth2User) {
        OAuth2User principal = new OAuth2User() {
            @Override
            public Map<String, Object> getAttributes() {
                Map<String, Object> attributes = new HashMap<>();
                attributes.put("openid", oauth2User.name());
                return attributes;
            }

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

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

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

Test the sample

package org.itrunner.wechat.controller;

import org.itrunner.wechat.base.WithMockOAuth2User;
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
    @WithMockOAuth2User
    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
    @WithMockOAuth2User
    void addHeroValidationFailed() throws Exception {
        Hero hero = new Hero();
        mvc.perform(post("/heroes").content(asJson(hero)).contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().is(400));
    }
}

Micro letter Developer Tools

Download and install the developer tools micro-channel, micro-channel binding developer account, easier and safer to develop and debug web-based micro letter.
Spring Boot implement OAuth 2.0 integration Spring Security Log

Reference material

Community Community Site OAuth
OAuth 2.0 the Login the Sample
micro-channel official documents

Guess you like

Origin blog.51cto.com/7308310/2457336