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机制实现的:
- 用户进入授权页面同意授权,获取code
- 通过code换取网页授权access_token(与基础支持中的access_token不同)
- 通过网页授权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;
@EnableWebSecurity
public 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;
@Slf4j
public class WeChatAuthorizationCodetokenResponseClient implements OAuth3AccesstokenResponseClient<OAuth3AuthorizationCodeGrantRequest> {
private static final String INVALID_TOKEN_RESPONSE_ERROR_CODE = "invalid_token_response";
private Converter<OAuth3AuthorizationCodeGrantRequest, RequestEntity<?>> 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<String> 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;
@Slf4j
public class WeChatAuthorizationCodeGrantRequestEntityConverter implements Converter<OAuth3AuthorizationCodeGrantRequest, RequestEntity<?>> {
@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
@Slf4j
public 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<String> scopeSet = new HashSet<>();
Collections.addAll(scopeSet, scopes);
builder.scopes(scopeSet);
Map<String, Object> additionalParameters = new LinkedHashMap<>();
additionalParameters.put("openid", openid);
builder.additionalParameters(additionalParameters);
return builder.build();
}
}
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;
@Slf4j
public class WeChatOAuth3UserService implements OAuth3UserService<OAuth3UserRequest, OAuth3User> {
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<OAuth3UserRequest, RequestEntity<?>> 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<String> 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<OAuth3UserRequest, RequestEntity<?>> {
@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.*;
@Slf4j
public 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<GrantedAuthority> authorities = new HashSet<>();
@JsonIgnore
private Map<String, Object> 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<String, Object> getAttributes() {
return this.attributes;
}
@Override
public Collection<? extends GrantedAuthority> 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
@Controller
public class OAuth3ClientController {
@Autowired
private OAuth3AuthorizedClientService authorizedClientService;
@GetMapping("/")
public String index() {
OAuth3AuthorizedClient authorizedClient = authorizedClientService.loadAuthorizedClient(OAuth3Context.getClientRegistrationId(), OAuth3Context.getPrincipalName());
OAuth3Accesstoken accesstoken = authorizedClient.getAccesstoken();
...
return "index";
}
}
或
@Controller
public 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<WithMockOAuth3User> {
@Override
public SecurityContext createSecurityContext(WithMockOAuth3User oauth3User) {
OAuth3User principal = new OAuth3User() {
@Override
public Map<String, Object> getAttributes() {
Map<String, Object> attributes = new HashMap<>();
attributes.put("openid", oauth3User.name());
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> 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
@AutoConfiguremockmvc
class 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
微信官方文档
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。