上一篇介绍了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点:
- 在发起授权请求时,包括:
- 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小节直接给出了完整的代码。
@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对象的定制
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%3Do.s.web.client.RestTemplate : HTTP POST https://api.weixin.qq.com/sns/oauth2/access_token
o.s.web.client.RestTemplate : Response 200 OKo.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框架也为这些差异预留了相应的扩展点,我们在学习源码的时候,要尽量观察和思考这些扩展点的实际用途,这样可以帮助我们找到定制化开发的最佳方案。
我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!