上一篇介绍了OAuth2协议的基本原理,以及Spring Security框架中自带的OAuth2客户端GitHub的实现细节,本篇以微信公众号网页授权登录为目的,介绍如何在原框架基础上定制开发OAuth2客户端。

一、微信公众平台OAuth2服务

先简单地介绍一下微信公众平台网页授权主要流程,具体可以参考微信公众平台的官方文档(https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

1.1 请求code

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

参数部分说明:

  • appId:必填参数,即clientId,公众号唯一标识
  • redirect_uri:必填参数,同OAuth2标准协议,表示服务端生成code之后重定向会本系统的地址
  • response_type:必填参数,同OAuth2标准协议,需填写"code"
  • scope: 必填参数,同OAuth2标准协议,在微信公众号访问中有两个场景,一种参数值为"snsapi_login",用于静默授权并自动重定向,只能获取到用户的openId,另一种参数值为“snsapi_userinfo”,用于弹出授权页面,供用户手动确认的场景,可以获取昵称、性别、所在地等信息
  • state: 非必填参数,同OAuth2标准协议,可防止CSRF攻击,最好加上,可使用Spring Security框提供的默认实现,上一篇已提过。
  • #wechat_redirect:这个fragment不能少,但也不是OAuth2标准协议的规范,官方也未作过多说明,可能是出于某种安全考虑

另外需要格外注意的是,微信公众平台会对这个授权请求的参数顺序进行校验,如果顺序不对,也会导致授权失败。

1.2 服务端重定向

服务端在收到请求后,就弹出用户授权页面,用户同意授权后(如使用静默授权则直接通过),又会重定向到redirect_uri的地址,并携带code和state参数,例如redirect_uri?code=CODE&state=STATE,客户端在收到这个请求后,获得code和state的参数值,并再次发起请求,获取access_token

1.3 获取access_token

其服务端点为:https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code

参数部分说明:

  • appId:必填参数,即clientId,公众号唯一标识
  • secret:必填参数,即client_secret,可在公众平台内查看
  • code:必填参数,同OAuth2标准协议,即上一步获取的code参数
  • grant_type:必填参数,同OAuth2标准协议,固定值“authorization_code”

这个端点看似是用GET请求,但实测用POST请求也是可以获取到access_token。

响应数据示例如下

{
  "access_token":"ACCESS_TOKEN",
  "expires_in":7200,
  "refresh_token":"REFRESH_TOKEN",
  "openid":"OPENID",
  "scope":"SCOPE",
  "is_snapshotuser": 1,
  "unionid": "UNIONID"
}

1.4 获取用户基础信息

其服务端点为:https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN

参数部分说明:

  • access_token:必填参数,即上一步获取到的acces_token
  • openid:必填参数,即上一步获取到的openid,用户唯一标识
  • lang:非必填参数,即返回数据的语言,zh_CN 简体,zh_TW 繁体,en 英语

这里没有按照标准协议的建议,将access_token放在Header中的Authorization字段,而是作为URL参数。

响应数据示例如下:

{   
  "openid": "OPENID",
  "nickname": NICKNAME,
  "sex": 1,
  "province":"PROVINCE",
  "city":"CITY",
  "country":"COUNTRY",
  "headimgurl":"https://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46",
  "privilege":[ "PRIVILEGE1" "PRIVILEGE2"     ],
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
}

1.5 差异分析

可以看到,微信公众平台提供的OAuth2授权服务没有严格遵循标准协议,所以先梳理一下哪些是需要定制的部分,综上所述,主要有以下3点:

  1. 在发起授权请求时,包括:
  • client-id这个参数需重命名为appid
  • 请求code的参数顺序必须依次为appid,redirect_uri,response_type,scope,state
  • 参数最后必须加上“#wechat_redirect”这个锚点

2. 在获取access_token时,包括:

  • client-id,client-secret这两个参数需重命名为appid和secret
  • 服务端响应的MediaType为text/plain,而默认HttpMessageConverter仅支持application/json
  • 根据OAuth2标准协议,返回的数据字段中缺少了一个必须字段:token_type,需要自动填充进去,否则反序列化时就会报错

3. 在获取用户信息时,包括

  • 需要在请求地址中拼接access_token,openid这两个参数,并指定为GET请求
  • 同上,需要兼容text/plain的MediaType

二、开发实战

下面我们逐步介绍如何优雅地实现这些定制需求,这里秉持一种原则,尽量复用框架的代码,减少重复造轮所带来的成本。

2.1 准备工作

这里我们使用微信公众平台提供的测试账号进行开发(https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login),只要扫描即可登录使用。另外,为了方便调试,可以下载微信开发者工具模拟微信客户端环境(https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html

2.2 引入依赖

说明:本篇所使用的Spring Boot为3.3.0,对应Spring Security版本为6.3.0,但其他6.x版本也同样适用。

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.3.0</spring-boot.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>annotationProcessor</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-oauth2-client</artifactId>
    </dependency>
</dependencies>

2.3 配置客户端信息

首先在application.yml文件中配置关于微信公众平台OAuth2客户端的基础信息:

spring:
  security:
    oauth2:
      client:
        registration:
          wechat:
            client-id: *********
            client-secret: *******************
            authorization-grant-type: authorization_code
            redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}"
            scope: snsapi_userinfo # 该scope允许获取微信的用户信息
        provider:
          wechat:
            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: nickname # 用户名对应的属性名称,即微信昵称

其次在HttpSecurity的oauth2Login DSL中,重点关注3个配置项:authorizationEndpoint,tokenEndpoint及userInfoEndpoint,分别用于定制发起授权请求,获取access_token,以及获取用户信息这3个部分的业务逻辑,下面详细介绍如何利用这些配置项将定制逻辑注入进来,当然也可以直接跳过2.4-2.6小节,2.7小节直接给出了完整的代码。

2.4 authorizationEndpoint配置

该配置项其中有一个authorizationRequestResolver的扩展点,用于配置接口OAuth2AuthorizationRequestResolver的实例,OAuth2AuthorizationRequestResolver是用来生成发起授权请求对象OAuth2AuthorizationRequest,最终用于发起授权请求的地址authorizationRequestUri就是从OAuth2AuthorizationRequest对象中获取的,其默认实现类是DefaultOAuth2AuthorizationRequestResolver,下面是其核心方法resolve,实际生成过程其实依赖OAuth2AuthorizationRequest.Builder构造器,这里预留了一个authorizationRequestCustomizer对象,可以实现对Builder对象的定制

public final class DefaultOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
    ...
    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer = (customizer) -> {};
    ...         
    private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId,
           String redirectUriAction) {
        if (registrationId == null) {
           return null;
        }
        ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
        if (clientRegistration == null) {
           throw new InvalidClientRegistrationIdException("Invalid Client Registration with Id: " + registrationId);
        }
        OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
        ...
        this.authorizationRequestCustomizer.accept(builder); // 可在此注入定制逻辑
        return builder.build();
    }
    ...
}

再看一下构造器Builder内部authorizationRequestUri的生成方法,其build方法源码如下,这里有两个扩展点,一个是parametersConsumer,一个是authorizationRequestUriFunction,前者可以用于替换参数名称,以及调整参数顺序,后者可以对UriBuilder作进一步的定制,我们可以用来添加“#wechat_redirect”。

public OAuth2AuthorizationRequest build() {
    ...
    authorizationRequest.authorizationRequestUri = StringUtils.hasText(this.authorizationRequestUri)
          ? this.authorizationRequestUri : this.buildAuthorizationRequestUri(); // 如果没有额外设置,最终构造URL的方法是buildAuthorizationRequestUri
    return authorizationRequest;
}

private String buildAuthorizationRequestUri() {
    Map<String, Object> parameters = getParameters(); 
    this.parametersConsumer.accept(parameters); // 扩展点
    MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
    parameters.forEach((k, v) -> queryParams.set(encodeQueryParam(k), encodeQueryParam(String.valueOf(v)))); 
    UriBuilder uriBuilder = this.uriBuilderFactory.uriString(this.authorizationUri).queryParams(queryParams);
    return this.authorizationRequestUriFunction.apply(uriBuilder).toString(); // 扩展点
}

2.5 tokenEndpoint配置

该配置项仅有一个accessTokenResponseClient的扩展点,用于配置接口OAuth2AccessTokenResponseClient的实例,它定义了获取access_token的客户端操作,其中授权码模式的实现类为DefaultAuthorizationCodeTokenResponseClient,可以看到这里有两个扩展点,一个是requestEntityConverter,可以用于调整参数,二是RestOperations,为了支持响应的MediaType,以及默认填充token_type字段,再对RestTemplate实例做进一步定制。

public final class DefaultAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {
    ...
    private Converter<OAuth2AuthorizationCodeGrantRequest, RequestEntity<?>> requestEntityConverter = new ClientAuthenticationMethodValidatingRequestEntityConverter<>(
          new OAuth2AuthorizationCodeGrantRequestEntityConverter());

    private RestOperations restOperations;
    
    public DefaultAuthorizationCodeTokenResponseClient() {
        RestTemplate restTemplate = new RestTemplate(
              Arrays.asList(new FormHttpMessageConverter(), new OAuth2AccessTokenResponseHttpMessageConverter())); // 定制OAuth2AccessTokenResponseHttpMessageConverter
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        this.restOperations = restTemplate;
    }

    @Override
    public OAuth2AccessTokenResponse getTokenResponse(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
       ...
       RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest); //生成请求实体对象,利用这个converter注入定制逻辑
       ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
       OAuth2AccessTokenResponse tokenResponse = response.getBody();
       ...
       return tokenResponse;
    }

    private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
       ...
          return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);

    }
    ...
}

2.6 userInfoEndpoint配置

该配置项有一个userService的扩展点,用于配置接口OAuth2UserService的实例,它定义了发起获取用户信息请求的客户端操作,默认实现类为DefaultOAuth2UserService,与上面类似,它也有两个扩展点,一个是requestEntityConverter,以及一个RestOperations,定制逻辑也基本类似。

public class DefaultOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    ...
    private Converter<OAuth2UserRequest, RequestEntity<?>> requestEntityConverter = new OAuth2UserRequestEntityConverter();

    private RestOperations restOperations;

    public DefaultOAuth2UserService() {
       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");
        String userNameAttributeName = getUserNameAttributeName(userRequest);
        RequestEntity<?> request = this.requestEntityConverter.convert(userRequest);
        ResponseEntity<Map<String, Object>> response = getResponse(userRequest, request);
        OAuth2AccessToken token = userRequest.getAccessToken();
        Map<String, Object> attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
        Collection<GrantedAuthority> authorities = getAuthorities(token, attributes);
        return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
    }
    ...
}

2.7 定制开发

下面给出完整代码,为了方便展示,下面将所有的定制实现类,都放在同一个Configuration类中,实际开发过程中,可以根据需要进行拆分调整

@Slf4j
@EnableWebSecurity
@Configuration
public class SpringSecurityConfiguration {

    @Resource
    private ClientRegistrationRepository clientRegistrationRepository;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.oauth2Login(oauth2 -> oauth2
                .authorizationEndpoint(authorization -> authorization.authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository)))
                .tokenEndpoint(token -> token.accessTokenResponseClient(accessTokenResponseClient()))
                .userInfoEndpoint(userInfo -> userInfo.userService(userService()))
        );
        DefaultSecurityFilterChain filterChain = http.build();
        filterChain.getFilters().stream().map(Object::toString).forEach(log::info);
        return filterChain;
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        String authorizationRequestBaseUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;
        // 参考框架内默认的实例构造方法
        DefaultOAuth2AuthorizationRequestResolver resolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository, authorizationRequestBaseUri);
        // 设置OAuth2AuthorizationRequest.builder的定制逻辑
        resolver.setAuthorizationRequestCustomizer(builder -> builder.parameters(this::parametersConsumer).authorizationRequestUri(this::authorizationRequestUriFunction));
        return resolver;
    }

    private void parametersConsumer(Map<String, Object> parameters) {
        Object clientId = parameters.get(OAuth2ParameterNames.CLIENT_ID);
        Object redirectUri = parameters.get(OAuth2ParameterNames.REDIRECT_URI);
        Object responseType = parameters.get(OAuth2ParameterNames.RESPONSE_TYPE);
        Object scope = parameters.get(OAuth2ParameterNames.SCOPE);
        Object state = parameters.get(OAuth2ParameterNames.STATE);
        // 清除掉原来所有的参数
        parameters.clear();
        // 重新调整顺序
        parameters.put("appid", clientId);// 修改clientId参数名称为appid
        parameters.put(OAuth2ParameterNames.REDIRECT_URI, redirectUri);
        parameters.put(OAuth2ParameterNames.RESPONSE_TYPE, responseType);
        parameters.put(OAuth2ParameterNames.SCOPE, scope);
        parameters.put(OAuth2ParameterNames.STATE, state);

    }

    private URI authorizationRequestUriFunction(UriBuilder builder) {
        builder.fragment("wechat_redirect");// 添加#wechat_redirect
        return builder.build();
    }
    

    private OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        DefaultAuthorizationCodeTokenResponseClient client = new DefaultAuthorizationCodeTokenResponseClient();
        // 注入自定义WechatOAuth2AuthorizationCodeGrantRequestEntityConverter
        client.setRequestEntityConverter(new WechatOAuth2AuthorizationCodeGrantRequestEntityConverter()); 
        // 创建一个OAuth2AccessTokenResponseHttpMessageConverter对象,设置支持的MediaType为text/plain
        OAuth2AccessTokenResponseHttpMessageConverter messageConverter = new OAuth2AccessTokenResponseHttpMessageConverter();
        messageConverter.setSupportedMediaTypes(List.of(MediaType.TEXT_PLAIN)); 
        messageConverter.setAccessTokenResponseConverter(new WechatOAuth2AccessTokenResponseConverter());
        // 其他配置照搬源码
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(new FormHttpMessageConverter(), messageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        client.setRestOperations(restTemplate);
        return client;
    }

    private static class WechatOAuth2AccessTokenResponseConverter implements Converter<Map<String, Object>, OAuth2AccessTokenResponse> {

        private static final DefaultMapOAuth2AccessTokenResponseConverter delegate = new DefaultMapOAuth2AccessTokenResponseConverter();
        //响应中缺少token_type字段,为避免报错默认填充,剩余部分依然委托给默认的DefaultMapOAuth2AccessTokenResponseConverter处理
        @Override
        public OAuth2AccessTokenResponse convert(Map<String, Object> source) {
            source.put(OAuth2ParameterNames.TOKEN_TYPE, OAuth2AccessToken.TokenType.BEARER.getValue()); 
            return delegate.convert(source);
        }
    }

    // 无法直接实现接口,不过可以继承OAuth2AuthorizationCodeGrantRequestEntityConverter
    private static class WechatOAuth2AuthorizationCodeGrantRequestEntityConverter extends OAuth2AuthorizationCodeGrantRequestEntityConverter {
        //参考父类的源码,依葫芦画瓢重写createParameters方法,根据微信的文档,依次添加appid,secret,grant_type,code这四个参数
        @Override
        protected MultiValueMap<String, String> createParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
            ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
            OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
            MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
            parameters.add("appid", clientRegistration.getClientId());
            parameters.add("secret", clientRegistration.getClientSecret());
            parameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
            parameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
            return parameters;
        }
    }

    private OAuth2UserService<OAuth2UserRequest, OAuth2User> userService() {
        DefaultOAuth2UserService userService = new DefaultOAuth2UserService();
        // 注入自定义的requestEntityConverter
        userService.setRequestEntityConverter(new WechatOAuth2UserRequestEntityConverter());
        // 创建一个MappingJackson2HttpMessageConverter对象,同样设置支持的MediaType为text/plain
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        messageConverter.setSupportedMediaTypes(List.of(MediaType.TEXT_PLAIN));
        RestTemplate restTemplate = new RestTemplate(List.of(messageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());
        userService.setRestOperations(restTemplate);
        return userService;
    }

    private static class WechatOAuth2UserRequestEntityConverter implements Converter<OAuth2UserRequest, RequestEntity<?>> {
        // 根据微信文档,在请求地址中拼接上access_token和openid两个参数
        @Override
        public RequestEntity<?> convert(OAuth2UserRequest userRequest) {
            ClientRegistration clientRegistration = userRequest.getClientRegistration();
            URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
                    .queryParam(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue())
                    .queryParam("openid", userRequest.getAdditionalParameters().get("openid"))
                    .build()
                    .toUri();
            return new RequestEntity<>(HttpMethod.GET, uri);
        }
    }
}

三、测试验证

首先为了方便测试,可以在hosts文件中将本地IP"127.0.0.1"映射为一个虚拟的域名,例如www.oauth2.com,然后在微信公众平台测试账号内设置授权回调页面域名地址,找到“网页账号”这一项,点击修改,在弹窗中输入“www.oauth2.com”,点击确认即可。

接着就可以启动程序验证效果了,测试时可以打开spring security的debug日志,在微信开发者工具内访问http://www.oauth2.com/oauth2/authorization/wechat,观察日志输出,请求被重定向到了https://open.weixin.qq.com/connect/oauth2/authorize这个地址,并且参数都按照预期设置成功。

o.s.security.web.FilterChainProxy        : Securing GET /oauth2/authorization/wechat
o.s.s.web.DefaultRedirectStrategy        : Redirecting to https://open.weixin.qq.com/connect/oauth2/authorize?appid=wx3574913d730b2837  
&redirect_uri=http://www.oauth2.com/login/oauth2/code/wechat&response_type=code&scope=snsapi_userinfo
&state=pBpqcIj_Z_A7iiApozDIBPw1IY1XFJQw1uTHhoqvGvs%3D#wechat_redirect

此时客户端会跳转到微信授权页面,如下图

点击同意后,服务端会重定向到redirect_uri的地址,即http://www.oauth2.com/login/oauth2/code/wechat,若该地址后面携带了code和state这两个参数,则表示code获取成功,另外回调地址中的state和此前发起请求时的state两个值也是一样的。

然后通过日志可以看到,接着又发起了获取access_token的请求,如果成功获取到access_token,随即就会使用acces_token再请求获取用户信息的接口,最后在得到用户数据后会创建对应的Authentication对象,并为其进行持久化操作,至此微信公众号网页授权的整个过程就完成了。

o.s.security.web.FilterChainProxy        : Securing GET /login/oauth2/code/wechat?
code=071jlkGa1xcSxH0okrFa1ERx9y0jlkGu&state=KXg4KA_6s6imMwr1Vm0DTZz7m8vn
iA2Bi4RZIjVEx2o%3D

o.s.web.client.RestTemplate              : HTTP POST https://api.weixin.qq.com/sns/oauth2/access_token
o.s.web.client.RestTemplate              : Response 200 OK

o.s.web.client.RestTemplate              : HTTP GET https://api.weixin.qq.com/sns/userinfo?
access_token=81_8wWRqWFvwAVQldmWeraiE7sNOwt7eRZJ5S5teKN2ua90TgxaCpRo97Eh
zR1Hr_3gP0eL7hpUK7zH0zYcFqZ-5Zxxs4as-6P5HJjDHiZ7Tyg&openid=***
2024-06-01T16:57:34.093+08:00 DEBUG 3965 --- [p-nio-80-exec-2] 
o.s.web.client.RestTemplate              : Response 200 OK

2024-06-01T16:57:37.730+08:00 DEBUG 3965 --- [p-nio-80-exec-2] 
w.c.HttpSessionSecurityContextRepository : Stored SecurityContextImpl 
[Authentication=OAuth2AuthenticationToken [Principal=Name: [**], Granted 
Authorities: [[OAUTH2_USER, SCOPE_snsapi_userinfo]], User Attributes: 
[{openid=***, nickname=**, sex=0, language=, city=, province=, country=, 
headimgurl=https://thirdwx.qlogo.cn/mmopen/vi_32/****, privilege=[]}], 
Credentials=[PROTECTED], Authenticated=true, 
Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, 
SessionId=6D5003EC4A8EF36118C02A7138CBAAAF], Granted Authorities=
[OAUTH2_USER, SCOPE_snsapi_userinfo]]] to HttpSession 
[org.apache.catalina.session.StandardSessionFacade@50161408]
2024-06-01T16:57:37.730+08:00 DEBUG 3965 --- [p-nio-80-exec-2] 
.s.o.c.w.OAuth2LoginAuthenticationFilter : Set SecurityContextHolder to 
OAuth2AuthenticationToken [Principal=Name: [**], Granted Authorities: 
[[OAUTH2_USER, SCOPE_snsapi_userinfo]], User Attributes: [{openid=***, 
nickname=**, sex=0, language=, city=, province=, country=, 
headimgurl=https://thirdwx.qlogo.cn/mmopen/vi_32/****, privilege=[]}], 
Credentials=[PROTECTED], Authenticated=true, 
Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, 
SessionId=6D5003EC4A8EF36118C02A7138CBAAAF], Granted Authorities=
[OAUTH2_USER, SCOPE_snsapi_userinfo]]
o.s.s.web.DefaultRedirectStrategy        : Redirecting to /

可以再写一个简单Controller,查看一下实际的Authentication对象

@RestController
public class UserController {

    @GetMapping("/user")
    public String user() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        return "Hello " + username;
    }
    
}

四、结束语

微信公众平台提供的OAuth2授权服务与标准协议的规范存在着诸多不同之处,但是基本框架流程都是相同的,Spring Security框架也为这些差异预留了相应的扩展点,我们在学习源码的时候,要尽量观察和思考这些扩展点的实际用途,这样可以帮助我们找到定制化开发的最佳方案。