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

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

spring_security_lg-1280x720.png

一、微信公众平台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”这个锚点
  1. 在获取access_token时,包括:
  • client-id,client-secret这两个参数需重命名为appid和secret
  • 服务端响应的MediaType为text/plain,而默认HttpMessageConverter仅支持application/json
  • 根据OAuth2标准协议,返回的数据字段中缺少了一个必须字段:token_type,需要自动填充进去,否则反序列化时就会报错
  1. 在获取用户信息时,包括
  • 需要在请求地址中拼接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”,点击确认即可。

image.png

image.png


接着就可以启动程序验证效果了,测试时可以打开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

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

image.png

点击同意后,服务端会重定向到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_6s6imMwr1Vm0DTZz7m8vniA2Bi4RZIjVEx2o%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_8wWRqWFvwAVQldmWeraiE7sNOwt7eRZJ5S5teKN2ua90TgxaCpRo97EhzR1Hr_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;
    }
   
}

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

image.png

四、结束语

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

相关文章
|
16小时前
|
安全 Java 数据库
Spring Security 入门 (二)
Spring Security 入门 (二)
|
17小时前
|
存储 NoSQL Java
Spring Cloud OAuth2 实现用户认证及单点登录(2)
Spring Cloud OAuth2 实现用户认证及单点登录
|
17小时前
|
存储 Java 数据安全/隐私保护
Spring Cloud OAuth2 实现用户认证及单点登录(1)
Spring Cloud OAuth2 实现用户认证及单点登录
|
20小时前
|
XML Java 数据库
Spring Boot 如何给微信公众号返回消息
Spring Boot 如何给微信公众号返回消息
|
2天前
|
安全 NoSQL Java
记录spring security执行流程
Spring Security登录授权流程简述: 1. 实现UserDetailsService,从DB加载用户信息。 2. 创建UserDetails实现类,封装用户详情。 3. 配置WebSecurityConfigurerAdapter,用BCryptPasswordEncoder加密。 4. 设定登录接口为匿名访问。 5. 注入AuthenticationManager,用其authenticate方法认证用户
|
1月前
|
小程序 API 数据安全/隐私保护
微信小程序开发中的一些常用标签
这些标签是微信小程序开发中的基础,开发者可以根据需要组合使用这些标签来构建小程序的界面。每个标签都有其属性和事件,可以通过属性来调整组件的样式和行为,通过事件来响应用户的操作。
56 5
|
22天前
|
小程序 JavaScript Java
基于SpringBoot+Vue+uniapp微信小程序的智慧旅游平台开发微信小程序的详细设计和实现
基于SpringBoot+Vue+uniapp微信小程序的智慧旅游平台开发微信小程序的详细设计和实现
39 8
|
2天前
|
小程序 Java Maven
springboot开发微信小程序
springboot开发微信小程序
8 0
|
3天前
|
小程序 开发者 Windows
安装VantWeapp开发微信小程序
安装VantWeapp开发微信小程序
12 0
|
30天前
|
PHP
PHP公众号开发给用户发微信消息提醒功能
该内容是一个关于如何在时间紧迫的情况下,通过微信客服消息接口实现用户资金到账或成员变动时发送微信通知的项目总结。
29 2