千家信息网

Spring Boot集成Spring Security实现OAuth 2.0登录

发表于:2024-11-13 作者:千家信息网编辑
千家信息网最后更新 2024年11月13日,Spring Security OAuth项目已弃用,最新的OAuth 2.0支持由Spring Security提供。目前Spring Security尚不支持Authorization Serve
千家信息网最后更新 2024年11月13日Spring Boot集成Spring Security实现OAuth 2.0登录

Spring Security OAuth项目已弃用,最新的OAuth 2.0支持由Spring Security提供。目前Spring Security尚不支持Authorization Server,仍需使用Spring Security OAuth项目,但最终将被Spring Security完全取代。

本文介绍了Spring Security OAuth3 Client的基础知识,如何利用Spring Security实现微信OAuth 2.0登录。GitHub源码wechat-api。

Spring Boot版本:2.2.2.RELEASE

为使用Spring Security OAuth3 Client,仅需在Spring Boot项目中增加以下依赖:

dependencies {    implementation 'org.springframework.boot:spring-boot-starter-oauth3-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登录

Spring Security(CommonOAuth3Provider)预定义了Google、GitHub、Facebook和Okta的OAuth Client配置,其中GitHub的定义如下:

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

为实现GitHub OAuth登录,仅需两步:

配置OAuth App

登录GitHub,依次进入Settings -> Developer settings -> OAuth Apps,然后点击New OAuth App:

其中Authorization callback URL即OAuth Redirect URL,默认为{baseUrl}/login/oauth3/code/{registrationId},registrationId为github,这里我们仅为测试可以输入http://localhost/login/oauth3/code/github 。
保存后会生成Client ID和Client Secret。

配置GitHub Client

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

配置完毕后启动Spring Boot项目,从浏览器访问则会自动跳转到GitHub登录页面:

如果配置了多个Client,则会跳转到登录选择页面:

默认,OAuth 2.0 Login Page由DefaultLoginPageGeneratingFilter自动生成,每个clientName一个链接。默认链接地址为OAuth3AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{registrationId}"。

微信

注册帐号

根据需要在微信开放平台或微信公众平台注册帐号,注册成功后会获得Client ID和Client Secret,不再赘述。
我使用了微信公众平台的网页授权服务。微信网页授权是通过OAuth3.0的Authorization Code机制实现的:

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

配置微信Client

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

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

自定义实现

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

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.oauth3.client.registration.ClientRegistrationRepository;@EnableWebSecuritypublic class OAuth3LoginSecurityConfig extends WebSecurityConfigurerAdapter {    @Value("${security.ignore-paths}")    private String[] ignorePaths;    private final ClientRegistrationRepository clientRegistrationRepository;    public OAuth3LoginSecurityConfig(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()                .oauth3Login(oauth3Login ->                        oauth3Login.authorizationEndpoint(authorizationEndpoint ->                                authorizationEndpoint.authorizationRequestResolver(new WeChatOAuth3AuthorizationRequestResolver(this.clientRegistrationRepository))                        ).tokenEndpoint(tokenEndpoint ->                                tokenEndpoint.accessTokenResponseClient(new WeChatAuthorizationCodeTokenResponseClient())                        ).userInfoEndpoint(userInfoEndpoint ->                                userInfoEndpoint.userService(new WeChatOAuth3UserService()))                ).authorizeRequests(authorizeRequests ->                authorizeRequests.anyRequest().authenticated());    }}

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

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

Spring Security默认实现为DefaultOAuth3AuthorizationRequestResolver,自定义实现WeChatOAuth3AuthorizationRequestResolver如下:

package org.itrunner.wechat.config;import org.springframework.security.oauth3.client.registration.ClientRegistrationRepository;import org.springframework.security.oauth3.client.web.DefaultOAuth3AuthorizationRequestResolver;import org.springframework.security.oauth3.client.web.OAuth3AuthorizationRequestResolver;import org.springframework.security.oauth3.core.endpoint.OAuth3AuthorizationRequest;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.oauth3.client.web.OAuth3AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI;public class WeChatOAuth3AuthorizationRequestResolver implements OAuth3AuthorizationRequestResolver {    private static final String WEIXIN_DEFAULT_SCOPE = "snsapi_userinfo";    private static final String REGISTRATION_ID_URI_VARIABLE_NAME = "registrationId";    private final OAuth3AuthorizationRequestResolver defaultAuthorizationRequestResolver;    private final AntPathRequestMatcher authorizationRequestMatcher;    public WeChatOAuth3AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {        this.defaultAuthorizationRequestResolver = new DefaultOAuth3AuthorizationRequestResolver(clientRegistrationRepository, DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);        this.authorizationRequestMatcher = new AntPathRequestMatcher(DEFAULT_AUTHORIZATION_REQUEST_BASE_URI + "/{" + REGISTRATION_ID_URI_VARIABLE_NAME + "}");    }    @Override    public OAuth3AuthorizationRequest resolve(HttpServletRequest request) {        String clientRegistrationId = this.resolveRegistrationId(request);        OAuth3AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request);        return resolve(authorizationRequest, clientRegistrationId);    }    @Override    public OAuth3AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {        OAuth3AuthorizationRequest authorizationRequest = this.defaultAuthorizationRequestResolver.resolve(request, clientRegistrationId);        return resolve(authorizationRequest, clientRegistrationId);    }    private OAuth3AuthorizationRequest resolve(OAuth3AuthorizationRequest 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());        OAuth3AuthorizationRequest.Builder builder = OAuth3AuthorizationRequest.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(OAuth3AuthorizationRequest authorizationRequest) {        return authorizationRequest.getScopes().stream().findFirst().orElse(WEIXIN_DEFAULT_SCOPE);    }}

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

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

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

WeChatAuthorizationCodeTokenResponseClient执行请求获取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.oauth3.client.endpoint.DefaultAuthorizationCodeTokenResponseClient;import org.springframework.security.oauth3.client.endpoint.OAuth3AccessTokenResponseClient;import org.springframework.security.oauth3.client.endpoint.OAuth3AuthorizationCodeGrantRequest;import org.springframework.security.oauth3.client.http.OAuth3ErrorResponseErrorHandler;import org.springframework.security.oauth3.core.OAuth3AuthorizationException;import org.springframework.security.oauth3.core.OAuth3Error;import org.springframework.security.oauth3.core.endpoint.OAuth3AccessTokenResponse;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;@Slf4jpublic class WeChatAuthorizationCodeTokenResponseClient implements OAuth3AccessTokenResponseClient {    private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";    private Converter> requestEntityConverter = new WeChatAuthorizationCodeGrantRequestEntityConverter();    private RestOperations restOperations;    private DefaultAuthorizationCodeTokenResponseClient defaultAuthorizationCodeTokenResponseClient = new DefaultAuthorizationCodeTokenResponseClient();    public WeChatAuthorizationCodeTokenResponseClient() {        RestTemplate restTemplate = new RestTemplate();        restTemplate.setErrorHandler(new OAuth3ErrorResponseErrorHandler());        this.restOperations = restTemplate;    }    @Override    public OAuth3AccessTokenResponse getTokenResponse(OAuth3AuthorizationCodeGrantRequest 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 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);            OAuth3Error oauth3Error = new OAuth3Error(INVALID_TOKEN_RESPONSE_ERROR_CODE, description + ex.getMessage(), null);            throw new OAuth3AuthorizationException(oauth3Error, ex);        }        // 解析response        OAuth3AccessTokenResponse tokenResponse = WeChatAccessTokenResponse.build(response.getBody()).toOAuth3AccessTokenResponse();        if (CollectionUtils.isEmpty(tokenResponse.getAccessToken().getScopes())) {            tokenResponse = OAuth3AccessTokenResponse.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.oauth3.client.endpoint.OAuth3AuthorizationCodeGrantRequest;import org.springframework.security.oauth3.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;@Slf4jpublic class WeChatAuthorizationCodeGrantRequestEntityConverter implements Converter> {    @Override    public RequestEntity convert(OAuth3AuthorizationCodeGrantRequest 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(OAuth3AuthorizationCodeGrantRequest 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解析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.oauth3.core.OAuth3AccessToken;import org.springframework.security.oauth3.core.endpoint.OAuth3AccessTokenResponse;import java.util.*;@Getter@Setter@Slf4jpublic 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 OAuth3AccessTokenResponse toOAuth3AccessTokenResponse() {        OAuth3AccessTokenResponse.Builder builder = OAuth3AccessTokenResponse.withToken(accessToken);        builder.tokenType(OAuth3AccessToken.TokenType.BEARER);        builder.expiresIn(expiresIn);        builder.refreshToken(refreshToken);        String[] scopes = scope.split(",");        Set scopeSet = new HashSet<>();        Collections.addAll(scopeSet, scopes);        builder.scopes(scopeSet);        Map additionalParameters = new LinkedHashMap<>();        additionalParameters.put("openid", openid);        builder.additionalParameters(additionalParameters);        return builder.build();    }}

User Info
微信获取User Info的链接如下:

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

Spring Security默认实现类为DefaultOAuth3UserService、OAuth3UserRequestEntityConverter、DefaultOAuth3User,自定义实现分别为WeChatOAuth3UserService、WeChatUserRequestEntityConverter、WeChatOAuth3User。

WeChatOAuth3UserService执行请求获取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.oauth3.client.http.OAuth3ErrorResponseErrorHandler;import org.springframework.security.oauth3.client.userinfo.DefaultOAuth3UserService;import org.springframework.security.oauth3.client.userinfo.OAuth3UserRequest;import org.springframework.security.oauth3.client.userinfo.OAuth3UserService;import org.springframework.security.oauth3.core.OAuth3AuthenticationException;import org.springframework.security.oauth3.core.OAuth3AuthorizationException;import org.springframework.security.oauth3.core.OAuth3Error;import org.springframework.security.oauth3.core.user.OAuth3User;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;@Slf4jpublic class WeChatOAuth3UserService implements OAuth3UserService {    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> requestEntityConverter = new WeChatUserRequestEntityConverter();    private RestOperations restOperations;    private DefaultOAuth3UserService defaultOAuth3UserService = new DefaultOAuth3UserService();    public WeChatOAuth3UserService() {        RestTemplate restTemplate = new RestTemplate();        restTemplate.setErrorHandler(new OAuth3ErrorResponseErrorHandler());        this.restOperations = restTemplate;    }    @Override    public OAuth3User loadUser(OAuth3UserRequest userRequest) throws OAuth3AuthenticationException {        Assert.notNull(userRequest, "userRequest cannot be null");        // 如不是WeiXin则使用默认实现        if (!userRequest.getClientRegistration().getRegistrationId().equals(WEIXIN_REGISTRATION_ID)) {            return defaultOAuth3UserService.loadUser(userRequest);        }        if (!StringUtils.hasText(userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri())) {            OAuth3Error oauth3Error = new OAuth3Error(MISSING_USER_INFO_URI_ERROR_CODE,                    "Missing required UserInfo Uri in UserInfoEndpoint for Client Registration: "                            + userRequest.getClientRegistration().getRegistrationId(),                    null);            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString());        }        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();        if (!StringUtils.hasText(userNameAttributeName)) {            OAuth3Error oauth3Error = new OAuth3Error(MISSING_USER_NAME_ATTRIBUTE_ERROR_CODE,                    "Missing required \"user name\" attribute name in UserInfoEndpoint for Client Registration: "                            + userRequest.getClientRegistration().getRegistrationId(),                    null);            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString());        }        // 获得request        RequestEntity request = this.requestEntityConverter.convert(userRequest);        ResponseEntity response;        try {            // 执行request            response = this.restOperations.exchange(request, String.class);        } catch (OAuth3AuthorizationException ex) {            OAuth3Error oauth3Error = 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(oauth3Error.getErrorCode());            if (oauth3Error.getDescription() != null) {                errorDetails.append(", Error Description: ").append(oauth3Error.getDescription());            }            errorDetails.append("]");            oauth3Error = new OAuth3Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,                    "An error occurred while attempting to retrieve the UserInfo Resource: " + errorDetails.toString(), null);            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.toString(), ex);        } catch (RestClientException ex) {            OAuth3Error oauth3Error = new OAuth3Error(INVALID_USER_INFO_RESPONSE_ERROR_CODE,                    "An error occurred while attempting to retrieve the UserInfo Resource: " + ex.getMessage(), null);            throw new OAuth3AuthenticationException(oauth3Error, oauth3Error.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 WeChatOAuth3User.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.oauth3.client.registration.ClientRegistration;import org.springframework.security.oauth3.client.userinfo.OAuth3UserRequest;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> {    @Override    public RequestEntity convert(OAuth3UserRequest 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(OAuth3UserRequest 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();    }}

WeChatOAuth3User:

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.oauth3.core.user.OAuth3User;import java.util.*;@Slf4jpublic class WeChatOAuth3User implements OAuth3User {    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 authorities = new HashSet<>();    @JsonIgnore    private Map attributes;    @JsonIgnore    private String nameAttributeKey;    public static WeChatOAuth3User build(String json, String userNameAttributeName) {        try {            WeChatOAuth3User user = JsonUtils.parseJson(json, WeChatOAuth3User.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 getAttributes() {        return this.attributes;    }    @Override    public Collection getAuthorities() {        return this.authorities;    }    @Override    public String getName() {        return this.getAttribute(this.nameAttributeKey).toString();    }    // getter and setter    ....

OAuth3AuthenticationToken

package org.itrunner.wechat.util;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.security.oauth3.client.authentication.OAuth3AuthenticationToken;import org.springframework.security.oauth3.core.user.OAuth3User;public final class OAuth3Context {    private OAuth3Context() {    }    public static String getPrincipalName() {        return getOAuth3AuthenticationToken().getName();    }    public static String getClientRegistrationId() {        return getOAuth3AuthenticationToken().getAuthorizedClientRegistrationId();    }    public static OAuth3User getOAuth3User() {        return getOAuth3AuthenticationToken().getPrincipal();    }    public static OAuth3AuthenticationToken getOAuth3AuthenticationToken() {        return (OAuth3AuthenticationToken) SecurityContextHolder.getContext().getAuthentication();    }}

OAuth3AccessToken

获取OAuth3AccessToken的方法:

@Controllerpublic class OAuth3ClientController {    @Autowired    private OAuth3AuthorizedClientService authorizedClientService;    @GetMapping("/")    public String index() {        OAuth3AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(OAuth3Context.getClientRegistrationId(), OAuth3Context.getPrincipalName());        OAuth3AccessToken accessToken = authorizedClient.getAccessToken();        ...        return "index";    }}

@Controllerpublic class OAuth3ClientController {    @GetMapping("/")    public String index(@RegisteredOAuth3AuthorizedClient("weixin") OAuth3AuthorizedClient authorizedClient) {        OAuth3AccessToken accessToken = authorizedClient.getAccessToken();        ...        return "index";    }}

测试

WithMockOAuth3User

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 WithMockOAuth3User {    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.oauth3.client.authentication.OAuth3AuthenticationToken;import org.springframework.security.oauth3.core.user.OAuth3User;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 {    @Override    public SecurityContext createSecurityContext(WithMockOAuth3User oauth3User) {        OAuth3User principal = new OAuth3User() {            @Override            public Map getAttributes() {                Map attributes = new HashMap<>();                attributes.put("openid", oauth3User.name());                return attributes;            }            @Override            public Collection getAuthorities() {                return Collections.EMPTY_LIST;            }            @Override            public String getName() {                return oauth3User.name();            }        };        OAuth3AuthenticationToken authenticationToken = new OAuth3AuthenticationToken(principal, Collections.emptyList(), "weixin");        SecurityContext context = SecurityContextHolder.createEmptyContext();        context.setAuthentication(authenticationToken);        return context;    }}

测试示例

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

微信开发者工具

下载安装微信开发者工具,绑定开发者微信帐号,可以更方便、更安全地开发和调试基于微信的网页。

参考资料

OAuth Community Site
OAuth 2.0 Login Sample
微信官方文档

0