问题描述:在微信小程序登录时我们得到的是code,需要自己的服务器换取openid,再得到本系统对应的用户。现在我想使用spring security oauth2在查到对应的用户后可以生成token,整体思路如下图
因为整个架构涉及很多自定义的类,自己做了封装,在这说说大概思路或说明一些类的用法,如未了解spring-security-oauth2-autoconfigure的可以先了解
首先是OAuth2ClientAuthenticationProcessingFilter这个类,如果我们要重写授权模式的处理逻辑,我们需要指定其中的RestTemplate(与授权服务器获取token请求有关)和TokenServices(与获取用户信息有关)。我做的封装如下
package cn.cjx913.spring_custom.oauth2.client;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
public class CustomOAuth2ClientAuthenticationProcessingFilter extends OAuth2ClientAuthenticationProcessingFilter {
public CustomOAuth2ClientAuthenticationProcessingFilter(
String defaultFilterProcessesUrl,//请求的地址
OAuth2ProtectedResourceDetails client,//请求客户端的信息,建议了解这个类封装了那些变量
ResourceServerProperties resource,//请求客户端的资源,建议了解这个类封装了那些变量
OAuth2ClientContext oAuth2ClientContext//oauth2请求上下文,可以通过自动注入的方式
) {
super(defaultFilterProcessesUrl);
OAuth2RestTemplate oAuth2RestTemplate = getOAuth2RestTemplate(client, oAuth2ClientContext);
this.setRestTemplate(oAuth2RestTemplate);
UserInfoTokenServices userInfoTokenServices = getUserInfoTokenServices(resource.getUserInfoUri(),
client.getClientId());
userInfoTokenServices.setRestTemplate(oAuth2RestTemplate);
this.setTokenServices(userInfoTokenServices);
}
public OAuth2RestTemplate getOAuth2RestTemplate(OAuth2ProtectedResourceDetails client, OAuth2ClientContext oAuth2ClientContext) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(client, oAuth2ClientContext);
return oAuth2RestTemplate;
}
public UserInfoTokenServices getUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
UserInfoTokenServices tokenServices = new UserInfoTokenServices(userInfoEndpointUrl, clientId);
return tokenServices;
}
}
对应微信小程序的有WxOAuth2ClientAuthenticationProcessingFilter,如下
package cn.cjx913.spring_custom.oauth2.client.wx.access_token;
import cn.cjx913.spring_custom.oauth2.client.CustomOAuth2ClientAuthenticationProcessingFilter;
import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
public class WxOAuth2ClientAuthenticationProcessingFilter extends CustomOAuth2ClientAuthenticationProcessingFilter {
public WxOAuth2ClientAuthenticationProcessingFilter(String defaultFilterProcessesUrl, OAuth2ProtectedResourceDetails client, ResourceServerProperties resource, OAuth2ClientContext oAuth2ClientContext) {
super(defaultFilterProcessesUrl, client, resource, oAuth2ClientContext);
}
@Override
public OAuth2RestTemplate getOAuth2RestTemplate(OAuth2ProtectedResourceDetails client, OAuth2ClientContext oAuth2ClientContext) {
WxOAuth2RestTemplate wxOAuth2RestTemplate = new WxOAuth2RestTemplate(client, oAuth2ClientContext);
return wxOAuth2RestTemplate;
}
@Override
public UserInfoTokenServices getUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
return new WxUserInfoTokenServices(userInfoEndpointUrl, clientId);
}
}
我们现在提供WxOAuth2RestTemplate和WxUserInfoTokenServices
WxOAuth2RestTemplate和WxUserInfoTokenServices不用特别修改,注意在这里我定义的授权类型为wx
package cn.cjx913.spring_custom.oauth2.client.wx.access_token;
import org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices;
public class WxUserInfoTokenServices extends UserInfoTokenServices {
public WxUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
super(userInfoEndpointUrl, clientId);
}
}
package cn.cjx913.spring_custom.oauth2.client.wx.access_token;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
import java.util.Arrays;
public class WxOAuth2RestTemplate extends OAuth2RestTemplate {
{
AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays. asList(
new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider(),
new WxAccessTokenProvider()));
setAccessTokenProvider(accessTokenProvider);
}
public WxOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource) {
super(resource);
}
public WxOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
super(resource, context);
}
// public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
// OAuth2ClientContext context = getOAuth2ClientContext();
// OAuth2AccessToken accessToken = context.getAccessToken();
//
// if (accessToken == null || accessToken.isExpired()) {
// try {
// accessToken = acquireAccessToken(context);
// } catch (UserRedirectRequiredException e) {
// context.setAccessToken(null); // No point hanging onto it now
// accessToken = null;
// String stateKey = e.getStateKey();
// if (stateKey != null) {
// Object stateToPreserve = e.getStateToPreserve();
// if (stateToPreserve == null) {
// stateToPreserve = "NONE";
// }
// context.setPreservedState(stateKey, stateToPreserve);
// }
// throw e;
// }
// }
// return accessToken;
// }
// @Override
// protected OAuth2AccessToken acquireAccessToken(OAuth2ClientContext oauth2Context) throws UserRedirectRequiredException {
//
// AccessTokenRequest accessTokenRequest = oauth2Context.getAccessTokenRequest();
// if (accessTokenRequest == null) {
// throw new AccessTokenRequiredException(
// "No OAuth 2 security context has been established. Unable to access resource '"
// + getResource().getId() + "'.", getResource());
// }
//
// // Transfer the preserved state from the (longer lived) context to the current request.
// String stateKey = accessTokenRequest.getStateKey();
// if (stateKey != null) {
// accessTokenRequest.setPreservedState(oauth2Context.removePreservedState(stateKey));
// }
//
// OAuth2AccessToken existingToken = oauth2Context.getAccessToken();
// if (existingToken != null) {
// accessTokenRequest.setExistingToken(existingToken);
// }
//
// OAuth2AccessToken accessToken = null;
// accessToken = accessTokenProvider.obtainAccessToken(getResource(), accessTokenRequest);
// if (accessToken == null || accessToken.getValue() == null) {
// throw new IllegalStateException(
// "Access token provider returned a null access token, which is illegal according to the contract.");
// }
// oauth2Context.setAccessToken(accessToken);
// return accessToken;
// }
}
关于AccessTokenProvider,除了基本的集中类型外,我添加了WxAccessTokenProvider,AccessTokenProvider处理参数的
package cn.cjx913.spring_custom.oauth2.client.wx.access_token;
import org.springframework.http.HttpHeaders;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.oauth2.client.filter.state.DefaultStateKeyGenerator;
import org.springframework.security.oauth2.client.filter.state.StateKeyGenerator;
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.Iterator;
import java.util.List;
/**
* Provider for obtaining an oauth2 access token by using resource owner password.
*/
public class WxAccessTokenProvider extends WxOAuth2AccessTokenSupport implements AccessTokenProvider {
private StateKeyGenerator stateKeyGenerator = new DefaultStateKeyGenerator();
public boolean supportsResource(OAuth2ProtectedResourceDetails resource) {
return resource instanceof WxResourceDetails && "wx".equals(resource.getGrantType());
}
public boolean supportsRefresh(OAuth2ProtectedResourceDetails resource) {
return supportsResource(resource);
}
public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails resource,
OAuth2RefreshToken refreshToken, AccessTokenRequest request) throws UserRedirectRequiredException,
OAuth2AccessDeniedException {
MultiValueMap form = new LinkedMultiValueMap ();
form.add("grant_type", "refresh_token");
form.add("refresh_token", refreshToken.getValue());
return retrieveToken(request, resource, form, new HttpHeaders());
}
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
throws UserRedirectRequiredException, AccessDeniedException, OAuth2AccessDeniedException {
WxResourceDetails resource = (WxResourceDetails) details;
MultiValueMap parametersForTokenRequest = getParametersForTokenRequest(resource, request);
HttpHeaders httpHeaders = new HttpHeaders();
return retrieveToken(request, resource, parametersForTokenRequest, httpHeaders);
}
protected MultiValueMap getParametersForTokenRequest(WxResourceDetails details, AccessTokenRequest request) {
request.setStateKey(stateKeyGenerator.generateKey(details));
MultiValueMap form = new LinkedMultiValueMap ();
form.set("grant_type", details.getGrantType());
form.putAll(request);
if (details.isScoped()) {
StringBuilder builder = new StringBuilder();
List scope = details.getScope();
if (scope != null) {
Iterator scopeIt = scope.iterator();
while (scopeIt.hasNext()) {
builder.append(scopeIt.next());
if (scopeIt.hasNext()) {
builder.append(' ');
}
}
}
form.set("scope", builder.toString());
}
return form;
}
}
在WxAccessTokenProvider有WxResourceDetails和WxOAuth2AccessTokenSupport,都是一些封装
package cn.cjx913.spring_custom.oauth2.client.wx.access_token;
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
public class WxResourceDetails extends BaseOAuth2ProtectedResourceDetails {
private String wxCode;
public WxResourceDetails() {
setGrantType("wx");
}
public String getWxCode() {
return wxCode;
}
public void setWxCode(String wxCode) {
this.wxCode = wxCode;
}
}
WxOAuth2AccessTokenSupport可以使用其父类OAuth2AccessTokenSupport(并没有特别的修改,父类是适用的),里有一个重要的方法,更具方法名是取回token,就是请求授权服务器获得token
protected OAuth2AccessToken retrieveToken(AccessTokenRequest request, OAuth2ProtectedResourceDetails resource,MultiValueMap form, HttpHeaders headers) throws OAuth2AccessDeniedException {......}
方法有两句可以去看一下
// Prepare headers and form before going into rest template call in case the URI is affected by the result
authenticationHandler.authenticateTokenRequest(resource, form, headers);
// Opportunity to customize form and headers
tokenRequestEnhancer.enhance(request, resource, form, headers);
关于发送请求到授权服务器的部分 完
补充:如何配置客户端
在WebSecurityConfigurerAdapter的protected void configure(HttpSecurity http) throws Exception {}中添加http.addFilterBefore(customSsoFilter(), BasicAuthenticationFilter.class);
customSsoFilter()如下
@Bean
public CustomSsoFilter customSsoFilter() {
List filters = new ArrayList <>();
filters.add(wx());
CustomSsoFilter customSsoFilter = new CustomSsoFilter(filters);
return customSsoFilter;
}
CustomSsoFilter只是
package cn.cjx913.spring_custom.oauth2.client;
import org.springframework.web.filter.CompositeFilter;
import java.util.List;
public class CustomSsoFilter extends CompositeFilter {
public CustomSsoFilter(List customOAuth2ClientAuthenticationProcessingFilters) {
this.setFilters(customOAuth2ClientAuthenticationProcessingFilters);
}
}
@Bean
public WxOAuth2ClientAuthenticationProcessingFilter wx() {
WxResourceDetails resourceDetails = new WxResourceDetails();
resourceDetails.setClientId("××××××");
resourceDetails.setClientSecret("××××××××××");
ArrayList scopes = new ArrayList <>();
scopes.add("××××");
resourceDetails.setScope(scopes);
resourceDetails.setGrantType("wx");
resourceDetails.setAccessTokenUri("××××××××××");
ResourceServerProperties resourceServerProperties = new ResourceServerProperties();
resourceServerProperties.setUserInfoUri("××××××××××");
return new WxOAuth2ClientAuthenticationProcessingFilter("/login/wx", resourceDetails, resourceServerProperties, oauth2ClientContext);
}
注意:添加下列代码
@Autowired
private OAuth2ClientContext oauth2ClientContext;
@Bean
public FilterRegistrationBean oauth2ClientFilterRegistration(OAuth2ClientContextFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean ();
registration.setFilter(filter);
registration.setOrder(-100);
return registration;
}
授权服务器AuthorizationServerConfigurerAdapter,因为有一些其他的自定义配置,可以只看需要的
package cn.cjx913.spring_custom.oauth2;
import cn.cjx913.spring_custom.common.user.CustomUserDetailsService;
import cn.cjx913.spring_custom.oauth2.client.wx.authenticate.WxTokenGranter;
import cn.cjx913.spring_custom.oauth2.properties.OAuth2Client;
import cn.cjx913.spring_custom.oauth2.properties.OAuth2Properties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Import;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.builders.ClientDetailsServiceBuilder;
import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
//@Configuration
//@EnableAuthorizationServer
@EnableConfigurationProperties(OAuth2Properties.class)
@Import({
OAuth2CustomBeanConfig.class,
AuthServerWebMvcConfiguration.class
})
public class AuthServerCustomConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
private OAuth2Properties oAuth2Properties;
@Autowired(required = false)
private DataSource dataSource;
@Autowired(required = false)
private PasswordEncoder passwordEncoder;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManagerBean;
@Autowired
private TokenStore tokenStore;
@Autowired(required = false)
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired(required = false)
private AbstractJwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess(oAuth2Properties.getTokenKeyAccess())
.checkTokenAccess(oAuth2Properties.getCheckTokenAccess())
;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
if (OAuth2Properties.ClientType.IN_MEMORY.equals(oAuth2Properties.getClientType())) {
List oAuth2Clients = oAuth2Properties.getClients();
if (!CollectionUtils.isEmpty(oAuth2Clients)) {
InMemoryClientDetailsServiceBuilder builder = clients.inMemory();
for (OAuth2Client oAuth2Client : oAuth2Clients) {
ClientDetailsServiceBuilder .ClientBuilder clientBuilder =
builder
.withClient(oAuth2Client.getClientId())
.secret(passwordEncoder.encode(oAuth2Client.getSecret()))
.accessTokenValiditySeconds(oAuth2Client.getAccessTokenValiditySeconds())
.refreshTokenValiditySeconds(oAuth2Client.getRefreshTokenValiditySeconds());
String[] scopes = oAuth2Client.getScopes();
if (!ObjectUtils.isEmpty(scopes)) {
clientBuilder.scopes(scopes);
}
String[] authorizedGrantTypes = oAuth2Client.getAuthorizedGrantTypes();
if (!ObjectUtils.isEmpty(authorizedGrantTypes)) {
clientBuilder.authorizedGrantTypes(authorizedGrantTypes);
}
String[] redirectUris = oAuth2Client.getRedirectUris();
if (!ObjectUtils.isEmpty(redirectUris)) {
clientBuilder.redirectUris(redirectUris);
}
String[] resourceIds = oAuth2Client.getResourceIds();
if (!ObjectUtils.isEmpty(resourceIds)) {
clientBuilder.resourceIds(resourceIds);
}
Map additionalInformation = new HashMap <>();
additionalInformation.put(OAuth2Constant.SECRET, oAuth2Client.getSecret());
String registrationId = oAuth2Client.getRegistrationId();
if (StringUtils.hasText(registrationId)) {
additionalInformation.put(OAuth2Constant.REGISTRATION_ID, registrationId);
}
if (!CollectionUtils.isEmpty(additionalInformation)) {
clientBuilder.additionalInformation(additionalInformation);
}
}
builder.build();
}
} else {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
Assert.notNull(dataSource, "dataSource can not be null");
clients
.jdbc(dataSource)
.passwordEncoder(passwordEncoder)
.build()
;
}
}
public TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
List tokenGranters = new ArrayList <>();
//4种默认的授权模式
TokenGranter tokenGranter = endpoints.getTokenGranter();
tokenGranters.add(tokenGranter);
//自动义的授权模式
WxTokenGranter wxTokenGranter = new WxTokenGranter(authenticationManagerBean, endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory());
wxTokenGranter.setPasswordEncoder(passwordEncoder);
tokenGranters.add(wxTokenGranter);
CompositeTokenGranter delegate = new CompositeTokenGranter(tokenGranters);
return delegate;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore)
.authenticationManager(authenticationManagerBean)
.userDetailsService(customUserDetailsService)
.tokenGranter(tokenGranter(endpoints))
;
if (tokenStore instanceof JwtTokenStore && jwtAccessTokenConverter != null) {
if (jwtTokenEnhancer != null) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List enhancers = new ArrayList ();
enhancers.add(jwtTokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancers);
endpoints.tokenEnhancer(enhancerChain).accessTokenConverter(jwtAccessTokenConverter);
} else {
endpoints.accessTokenConverter(jwtAccessTokenConverter);
}
}
}
public DataSource getDataSource() {
return dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
public CustomUserDetailsService getCustomUserDetailsService() {
return customUserDetailsService;
}
public void setCustomUserDetailsService(CustomUserDetailsService customUserDetailsService) {
this.customUserDetailsService = customUserDetailsService;
}
public AuthenticationManager getAuthenticationManagerBean() {
return authenticationManagerBean;
}
public void setAuthenticationManagerBean(AuthenticationManager authenticationManagerBean) {
this.authenticationManagerBean = authenticationManagerBean;
}
public TokenStore getTokenStore() {
return tokenStore;
}
public void setTokenStore(TokenStore tokenStore) {
this.tokenStore = tokenStore;
}
public JwtAccessTokenConverter getJwtAccessTokenConverter() {
return jwtAccessTokenConverter;
}
public void setJwtAccessTokenConverter(JwtAccessTokenConverter jwtAccessTokenConverter) {
this.jwtAccessTokenConverter = jwtAccessTokenConverter;
}
public AbstractJwtTokenEnhancer getJwtTokenEnhancer() {
return jwtTokenEnhancer;
}
public void setJwtTokenEnhancer(AbstractJwtTokenEnhancer jwtTokenEnhancer) {
this.jwtTokenEnhancer = jwtTokenEnhancer;
}
}
注意PasswordEncoder,如果系统中有,在授权验证时会对客户端的client_id,client_secret匹配
在上述代码中我们需要添加的是WxTokenGranter
package cn.cjx913.spring_custom.oauth2.client.wx.authenticate;
import cn.cjx913.spring_custom.oauth2.OAuth2Constant;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AccountStatusException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.exceptions.InvalidGrantException;
import org.springframework.security.oauth2.provider.*;
import org.springframework.security.oauth2.provider.token.AbstractTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.util.Assert;
import java.util.LinkedHashMap;
import java.util.Map;
public class WxTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "wx";
private PasswordEncoder passwordEncoder;
private final AuthenticationManager authenticationManager;
public WxTokenGranter(AuthenticationManager authenticationManager,
AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService,
OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected WxTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
Map parameters = new LinkedHashMap (tokenRequest.getRequestParameters());
String wxCode = parameters.get("wxCode");
String clientId = client.getClientId();
Map additionalInformation = client.getAdditionalInformation();
String clientSecret = String.valueOf(additionalInformation.get(OAuth2Constant.SECRET));
String registrationId = String.valueOf(additionalInformation.get(OAuth2Constant.REGISTRATION_ID));
Authentication userAuth = new WxAuthenticationToken(clientId, clientSecret, wxCode, registrationId);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
} catch (AccountStatusException ase) {
//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
throw new InvalidGrantException(ase.getMessage());
} catch (BadCredentialsException e) {
// If the username/password are wrong the spec says we should send 400/invalid grant
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user: " + wxCode);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
public AuthenticationManager getAuthenticationManager() {
return authenticationManager;
}
public PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
}
为什么要通过additionalInformation.get(OAuth2Constant.SECRET)获取client_secret?因为通过client.getClientSecret()是加密后的字符串
userAuth = authenticationManager.authenticate(userAuth);将进行用户验证
WxAuthenticationToken的包装
package cn.cjx913.spring_custom.oauth2.client.wx.authenticate;
import cn.cjx913.spring_custom.common.user.CustomUser;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.util.Base64Utils;
import javax.validation.constraints.NotNull;
import java.nio.charset.Charset;
public class WxAuthenticationToken extends AbstractAuthenticationToken {
private String appId;
private String appSecret;
private String wxCode;
private String registrationId;
private CustomUser customUser;
public WxAuthenticationToken(String appId, String appSecret, String wxCode, String registrationId) {
super(null);
this.appId = appId;
this.appSecret = appSecret;
this.wxCode = wxCode;
this.registrationId = registrationId;
setAuthenticated(false);
}
public WxAuthenticationToken(@NotNull CustomUser customUser) {
super(customUser.getAuthorities());
this.customUser = customUser;
super.setAuthenticated(true);
}
@Override
public Object getPrincipal() {
return customUser;
}
@Override
public Object getCredentials() {
if (customUser == null) {
return null;
}
return Base64Utils.encodeToString((customUser.getUserId() + "_" + customUser.getUsername()).getBytes(Charset.forName("UTF-8")));
}
@Override
public void setAuthenticated(boolean authenticated) {
if (authenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
public String getAppId() {
return appId;
}
public String getAppSecret() {
return appSecret;
}
public String getWxCode() {
return wxCode;
}
public String getRegistrationId() {
return registrationId;
}
public CustomUser getCustomUser() {
return customUser;
}
}
用户验证需要WxAuthenticationProvider,在WebSecurityConfigurerAdapter的中有
@Bean
public WxAuthenticationProvider wxAuthenticationProvider(CustomUserDetailsService customUserDetailsService) {
return new WxAuthenticationProvider(customUserDetailsService);
}
@Autowired
private WxAuthenticationProvider wxAuthenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(wxAuthenticationProvider);
auth
.userDetailsService(customUserDetailsService)
.passwordEncoder(passwordEncoder)
;
}
下面看WxAuthenticationProvider
package cn.cjx913.spring_custom.oauth2.client.wx.authenticate;
import cn.cjx913.spring_custom.common.user.CustomUser;
import cn.cjx913.spring_custom.common.user.CustomUserDetailsService;
import cn.cjx913.spring_custom.oauth2.OAuth2Constant;
import cn.cjx913.spring_custom.oauth2.client.wx.exception.WxAuthenticationException;
import cn.cjx913.spring_custom.oauth2.client.wx.exception.WxUnregisterAuthenticationException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.util.Assert;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.util.Map;
public class WxAuthenticationProvider implements AuthenticationProvider {
private CustomUserDetailsService customUserDetailsService;
private RestTemplate restTemplate;
private ObjectMapper objectMapper;
private final static String URL = OAuth2Constant.JSCODE_2_SESSION_URL;
{
if (restTemplate == null) {
restTemplate = new RestTemplate();
}
if (objectMapper == null) {
objectMapper = new ObjectMapper();
}
}
public WxAuthenticationProvider() {
}
public WxAuthenticationProvider(CustomUserDetailsService customUserDetailsService) {
Assert.notNull(customUserDetailsService, WxAuthenticationProvider.class.getName() + ".customUserDetailsService cannot be null");
this.customUserDetailsService = customUserDetailsService;
}
public WxAuthenticationProvider(CustomUserDetailsService customUserDetailsService, RestTemplate restTemplate, ObjectMapper objectMapper) {
Assert.notNull(customUserDetailsService, WxAuthenticationProvider.class.getName() + ".customUserDetailsService cannot be null");
Assert.notNull(restTemplate, WxAuthenticationProvider.class.getName() + ".restTemplate cannot be null");
Assert.notNull(objectMapper, WxAuthenticationProvider.class.getName() + ".objectMapper cannot be null");
this.customUserDetailsService = customUserDetailsService;
this.restTemplate = restTemplate;
this.objectMapper = objectMapper;
}
@Override
public boolean supports(Class > authentication) {
return WxAuthenticationToken.class.isAssignableFrom(authentication);
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(customUserDetailsService, WxAuthenticationProvider.class.getName() + ".customUserDetailsService cannot be null");
Assert.notNull(restTemplate, WxAuthenticationProvider.class.getName() + ".restTemplate cannot be null");
Assert.notNull(objectMapper, WxAuthenticationProvider.class.getName() + ".objectMapper cannot be null");
WxAuthenticationToken wxAuthenticationToken = (WxAuthenticationToken) authentication;
Map parameters = (Map ) authentication.getDetails();
String appId = wxAuthenticationToken.getAppId();
String appSecret = wxAuthenticationToken.getAppSecret();
String wxCode = wxAuthenticationToken.getWxCode();
String url = String.format(URL, appId, appSecret, wxCode);
String result = restTemplate.getForObject(url, String.class);
WxLoginResult wxLoginResult = null;
try {
wxLoginResult = objectMapper.readValue(result, WxLoginResult.class);
} catch (IOException e) {
throw new WxAuthenticationException("auth.code2Session请求错误!");
}
int errCode = wxLoginResult.getErrCode();
if (errCode != 0) {
throw new WxAuthenticationException(wxLoginResult);
}
String registrationId = wxAuthenticationToken.getRegistrationId();
OAuth2User oAuth2User = new DefaultOAuth2User(AuthorityUtils.createAuthorityList("ROLE_USER"), wxLoginResult.toMap(), "openId");
CustomUser customUser = customUserDetailsService.loadCustomUser(registrationId, oAuth2User);
if (customUser == null) {
throw new WxUnregisterAuthenticationException("用户未注册");
}
WxAuthenticationToken authenticationResult = new WxAuthenticationToken(customUser);
return authenticationResult;
}
public void setCustomUserDetailsService(CustomUserDetailsService customUserDetailsService) {
Assert.notNull(customUserDetailsService, WxAuthenticationProvider.class.getName() + ".customUserDetailsService cannot be null");
this.customUserDetailsService = customUserDetailsService;
}
public void setRestTemplate(RestTemplate restTemplate) {
Assert.notNull(restTemplate, WxAuthenticationProvider.class.getName() + ".restTemplate cannot be null");
this.restTemplate = restTemplate;
}
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, WxAuthenticationProvider.class.getName() + ".objectMapper cannot be null");
this.objectMapper = objectMapper;
}
}
WxLoginResult
package cn.cjx913.spring_custom.oauth2.client.wx.authenticate;
import com.fasterxml.jackson.annotation.JsonAlias;
import lombok.Getter;
import lombok.Setter;
import java.util.HashMap;
import java.util.Map;
@Setter
@Getter
public class WxLoginResult {
/**
* 用户唯一标识
*/
@JsonAlias("openid")
private String openId;
/**
* 会话密钥
*/
@JsonAlias("session_key")
private String sessionKey;
/**
* 用户在开放平台的唯一标识符,
* 在满足 UnionID
* 下发条件的情况下会返回,
* 详见 UnionID
* 机制说明。
*/
@JsonAlias("union_id")
private String unionId;
/**
* 错误码
* -1 系统繁忙,此时请开发者稍候再试
* 0 请求成功
* 40029 code 无效
* 45011 频率限制,每个用户每分钟100次
*/
@JsonAlias("errcode")
private int errCode;
/**
* 错误信息
*/
@JsonAlias("errmsg")
private String errMsg;
public Map toMap() {
HashMap map = new HashMap <>();
map.put("openId", openId);
map.put("unionId", unionId);
map.put("sessionKey", sessionKey);
map.put("errCode", errCode);
map.put("errMsg", errMsg);
return map;
}
}
CustomUserDetailsService,这里主要是自定义的获取本地用户的接口
package cn.cjx913.spring_custom.common.user;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.user.OAuth2User;
public interface CustomUserDetailsService extends UserDetailsService {
/**
* 第三方登录获取与系统对应的用户
*
* @param registrationId 第三方系统唯一标识
* @param oAuth2User 第三方系统中用户
* @return
*/
CustomUser loadCustomUser(String registrationId, OAuth2User oAuth2User);
/**
* 系统注册第三方登录用户
*
* @param oAuth2User
* @return
*/
CustomUser registerCustomUser(String registrationId, OAuth2User oAuth2User);
@Override
CustomUser loadUserByUsername(String username) throws UsernameNotFoundException;
}
大概是这么多,可能有些地方不好说清楚,写的不好勿喷,源码暂不公开(因为没有充分的测试,而且涉及很多自定义封装的),可以自己研究研究或者留言,需要了解spring-security-oauth2-autoconfigure的具体执行过程,关键是OAuth2ClientAuthenticationProcessingFilter和AbstractTokenGranter(TokenGranter)。了解这些可以修改一些请求参数或者自定义授权模式,微信登录就可以很好解决,例如
AuthorizationCodeAccessTokenProvider(授权码模式)可以做如下修改
protected MultiValueMap getParametersForAuthorizeRequest(AuthorizationCodeResourceDetails resource,
AccessTokenRequest request) {
MultiValueMap form = new LinkedMultiValueMap ();
form.set("response_type", "code");
form.set("appid", resource.getClientId());
if (request.get("scope") != null) {
form.set("scope", request.getFirst("scope"));
} else {
form.set("scope", OAuth2Utils.formatParameterList(resource.getScope()));
}
// Extracting the redirect URI from a saved request should ignore the current URI, so it's not simply a call to
// resource.getRedirectUri()
String redirectUri = resource.getPreEstablishedRedirectUri();
Object preservedState = request.getPreservedState();
if (redirectUri == null && preservedState != null) {
// no pre-established redirect uri: use the preserved state
// TODO: treat redirect URI as a special kind of state (this is a historical mini hack)
redirectUri = String.valueOf(preservedState);
} else {
redirectUri = request.getCurrentUri();
}
String stateKey = request.getStateKey();
if (stateKey != null) {
form.set("state", stateKey);
if (preservedState == null) {
throw new InvalidRequestException(
"Possible CSRF detected - state parameter was present but no state could be found");
}
}
if (redirectUri != null) {
form.set("redirect_uri", redirectUri);
}
return form;
}
protected MultiValueMap getParametersForTokenRequest(WechatResourceDetails resource,
AccessTokenRequest request) {
MultiValueMap form = new LinkedMultiValueMap ();
form.set("grant_type", "authorization_code");
form.set("code", request.getAuthorizationCode());
form.set("appid", resource.getClientId());
form.set("secret", resource.getClientSecret());
Object preservedState = request.getPreservedState();
if (request.getStateKey() != null || stateMandatory) {
// The token endpoint has no use for the state so we don't send it back, but we are using it
// for CSRF detection client side...
if (preservedState == null) {
throw new InvalidRequestException(
"Possible CSRF detected - state parameter was required but no state could be found");
}
}
// Extracting the redirect URI from a saved request should ignore the current URI, so it's not simply a call to
// resource.getRedirectUri()
String redirectUri = null;
// Get the redirect uri from the stored state
if (preservedState instanceof String) {
// Use the preserved state in preference if it is there
// TODO: treat redirect URI as a special kind of state (this is a historical mini hack)
redirectUri = String.valueOf(preservedState);
} else {
redirectUri = resource.getRedirectUri(request);
}
if (redirectUri != null && !"NONE".equals(redirectUri)) {
form.set("redirect_uri", redirectUri);
}
return form;
}
protected UserRedirectRequiredException getRedirectForAuthorization(WechatResourceDetails resource,
AccessTokenRequest request) {
// we don't have an authorization code yet. So first get that.
TreeMap requestParameters = new TreeMap ();
requestParameters.put("response_type", "code"); // oauth2 spec, section 3
requestParameters.put("appid", resource.getClientId());
// Client secret is not required in the initial authorization request
String redirectUri = resource.getRedirectUri(request);
if (redirectUri != null) {
requestParameters.put("redirect_uri", redirectUri);
}
if (resource.isScoped()) {
StringBuilder builder = new StringBuilder();
List scope = resource.getScope();
if (scope != null) {
Iterator scopeIt = scope.iterator();
while (scopeIt.hasNext()) {
builder.append(scopeIt.next());
if (scopeIt.hasNext()) {
builder.append(' ');
}
}
}
requestParameters.put("scope", builder.toString());
}
UserRedirectRequiredException redirectException = new UserRedirectRequiredException(
resource.getUserAuthorizationUri(), requestParameters);
String stateKey = stateKeyGenerator.generateKey(resource);
redirectException.setStateKey(stateKey);
request.setStateKey(stateKey);
redirectException.setStateToPreserve(redirectUri);
request.setPreservedState(redirectUri);
return redirectException;
}
看方法名可以知道其作用,不多说了