Spring Security 6.x 微信公众平台OAuth2授权实战

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

一、微信公众平台OAuth2服务

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

1.1 请求code

其服务端点为:

代码语言:javascript
复制
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

其服务端点为

代码语言:http
复制
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。

响应数据示例如下

代码语言:json
复制
{
  "access_token":"ACCESS_TOKEN",
  "expires_in":7200,
  "refresh_token":"REFRESH_TOKEN",
  "openid":"OPENID",
  "scope":"SCOPE",
  "is_snapshotuser": 1,
  "unionid": "UNIONID"
}

1.4 获取用户基础信息

其服务端点为

代码语言:http
复制
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参数。

响应数据示例如下:

代码语言:json
复制
{   
  "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版本也同样适用

代码语言:xml
复制
<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客户端的基础信息

代码语言:yaml
复制
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小节直接给出了完整的代码。

代码语言:java
复制
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorization -> authorization.authorizationRequestResolver()
...)
.tokenEndpoint(token -> token.accessTokenResponseClient()
...)
.userInfoEndpoint(userInfo -> userInfo.userService()
...)
);
return http.build();
}

2.4 authorizationEndpoint配置

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

代码语言:java
复制
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”。

代码语言:java
复制
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实例做进一步定制。

代码语言:java
复制
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&lt;?&gt; request = this.requestEntityConverter.convert(authorizationCodeGrantRequest); //生成请求实体对象,利用这个converter注入定制逻辑
   ResponseEntity&lt;OAuth2AccessTokenResponse&gt; response = getResponse(request);
   OAuth2AccessTokenResponse tokenResponse = response.getBody();
   ...
   return tokenResponse;
}

private ResponseEntity&lt;OAuth2AccessTokenResponse&gt; getResponse(RequestEntity&lt;?&gt; request) {
   ...
      return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);

}
...

}

2.6 userInfoEndpoint配置

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

代码语言:java
复制
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, &#34;userRequest cannot be null&#34;);
    String userNameAttributeName = getUserNameAttributeName(userRequest);
    RequestEntity&lt;?&gt; request = this.requestEntityConverter.convert(userRequest);
    ResponseEntity&lt;Map&lt;String, Object&gt;&gt; response = getResponse(userRequest, request);
    OAuth2AccessToken token = userRequest.getAccessToken();
    Map&lt;String, Object&gt; attributes = this.attributesConverter.convert(userRequest).convert(response.getBody());
    Collection&lt;GrantedAuthority&gt; authorities = getAuthorities(token, attributes);
    return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
}
...

}

2.7 定制开发

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

代码语言:java
复制
@Slf4j
@EnableWebSecurity
@Configuration
public class SpringSecurityConfiguration {

@Resource
private ClientRegistrationRepository clientRegistrationRepository;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.oauth2Login(oauth2 -&gt; oauth2
            .authorizationEndpoint(authorization -&gt; authorization.authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository)))
            .tokenEndpoint(token -&gt; token.accessTokenResponseClient(accessTokenResponseClient()))
            .userInfoEndpoint(userInfo -&gt; 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 -&gt; builder.parameters(this::parametersConsumer).authorizationRequestUri(this::authorizationRequestUriFunction));
    return resolver;
}

private void parametersConsumer(Map&lt;String, Object&gt; 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(&#34;appid&#34;, 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(&#34;wechat_redirect&#34;);// 添加#wechat_redirect
    return builder.build();
}


private OAuth2AccessTokenResponseClient&lt;OAuth2AuthorizationCodeGrantRequest&gt; 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&lt;Map&lt;String, Object&gt;, OAuth2AccessTokenResponse&gt; {

    private static final DefaultMapOAuth2AccessTokenResponseConverter delegate = new DefaultMapOAuth2AccessTokenResponseConverter();
    //响应中缺少token_type字段,为避免报错默认填充,剩余部分依然委托给默认的DefaultMapOAuth2AccessTokenResponseConverter处理
    @Override
    public OAuth2AccessTokenResponse convert(Map&lt;String, Object&gt; 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&lt;String, String&gt; createParameters(OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
        ClientRegistration clientRegistration = authorizationCodeGrantRequest.getClientRegistration();
        OAuth2AuthorizationExchange authorizationExchange = authorizationCodeGrantRequest.getAuthorizationExchange();
        MultiValueMap&lt;String, String&gt; parameters = new LinkedMultiValueMap&lt;&gt;();
        parameters.add(&#34;appid&#34;, clientRegistration.getClientId());
        parameters.add(&#34;secret&#34;, clientRegistration.getClientSecret());
        parameters.add(OAuth2ParameterNames.GRANT_TYPE, authorizationCodeGrantRequest.getGrantType().getValue());
        parameters.add(OAuth2ParameterNames.CODE, authorizationExchange.getAuthorizationResponse().getCode());
        return parameters;
    }
}

private OAuth2UserService&lt;OAuth2UserRequest, OAuth2User&gt; 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&lt;OAuth2UserRequest, RequestEntity&lt;?&gt;&gt; {
    // 根据微信文档,在请求地址中拼接上access_token和openid两个参数
    @Override
    public RequestEntity&lt;?&gt; convert(OAuth2UserRequest userRequest) {
        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        URI uri = UriComponentsBuilder.fromUriString(clientRegistration.getProviderDetails().getUserInfoEndpoint().getUri())
                .queryParam(OAuth2ParameterNames.ACCESS_TOKEN, userRequest.getAccessToken().getTokenValue())
                .queryParam(&#34;openid&#34;, userRequest.getAdditionalParameters().get(&#34;openid&#34;))
                .build()
                .toUri();
        return new RequestEntity&lt;&gt;(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这个地址,并且参数都按照预期设置成功。

代码语言:plaintext
复制
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对象,并为其进行持久化操作,至此微信公众号网页授权的整个过程就完成了。

代码语言:plaintext
复制
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对象

代码语言:java
复制
@RestController
public class UserController {

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

}

请求该接口,可以看到结果中显示对应的微信昵称,说明已经授权成功

四、结束语

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

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!