文章目录
需求:
- 基于手机+密码+图形验证码
- 基于邮箱+密码+图形验证码
- 基于用户名+密码+图形验证码
- 基于手机+手机验证码
其实无论哪个需求,oauth2自带的授权方式都没办法实现。
看了网上很多都是添加SpringSecurity的过滤器来拦截的,但总感觉这样就脱离了Oauth2,且我这边是通过自定义的登录接口然后通过RestTemplate调用/oauth/token
接口申请token,因此不方便通过过滤器直接拦截登录的URL。
较好的方式是拓展Oauth2的授权方式。
首先需要了解调用申请令牌的接口的原理:
即:/oauth/token?username=xxx&password=xxx&grant_type=xxx
//TokenEndpoint.postAccessToken
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException ;
Filter拦截
在请求落入到TokenEndpoint.postAccessToken
前,会经过两个拦截器ClientCredentialsTokenEndpointFilter
和BasicAuthenticationFilter
。
这两个拦截器会对请求的客户端id和客户端密码进行认证。
ClientCredentialsTokenEndpointFilter
在allowFormAuthenticationForClients()
后才会生效,从请求中获取客户端id和客户端密码进行校验;否则会在BasicAuthenticationFilter
中,从请求头中获取Authorization
参数来进行客户端认证,其中Authorization
中保存的是Basic Base64.encode("clientId:clientPwd")
的编码串。
postAccessToken
Filter通过后就会进入postAccessToken方法进行token申请:
该方法中首先会拿到客户端id,根据客户端id从数据库查询客户端信息,对其scope进行校验,然后对请求参数中的
grant_type等参数进行校验,最后会进行token的申请:
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
这里会调用TokenGranter对象的grant方法进行申请。默认的CompositeTokenGranter中会保存所有的TokenGranter集合:
public class CompositeTokenGranter implements TokenGranter {
private final List<TokenGranter> tokenGranters;
public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
public void addTokenGranter(TokenGranter tokenGranter) {
if (tokenGranter == null) {
throw new IllegalArgumentException("Token granter is null");
}
tokenGranters.add(tokenGranter);
}
}
然后就会在grant方法中,遍历集合中的TokenGranter的grant方法,该方法会进入其抽象父类AbstractTokenGranter
的grant中:
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
//这里就是判断当前TokenGranter(实现类会传入this.grantType)的授权类型是否和请求的授权类型匹配,如果匹配就会往下走
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
}
OK,先到这里,等下再接着这里分析。
再来看保存所有授权类型的Granter集合:tokenGranters
默认注入的TokenEndpoint
中会设置默认的tokenGranter:
@Bean
public TokenEndpoint tokenEndpoint() throws Exception {
TokenEndpoint tokenEndpoint = new TokenEndpoint();
...
tokenEndpoint.setTokenGranter(tokenGranter());
....
return tokenEndpoint;
}
该方法实现如下:
private TokenGranter tokenGranter() {
if (tokenGranter == null) {
tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters());
}
return delegate.grant(grantType, tokenRequest);
}
};
}
return tokenGranter;
}
内部委托了CompositeTokenGranter,在其构造中传入默认的TokenGranter
private List<TokenGranter> getDefaultTokenGranters() {
ClientDetailsService clientDetails = clientDetailsService();
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
clientDetails, requestFactory));
}
return tokenGranters;
}
这五个XXXGranter(即TokenGranter的实现类)就对应了Oauth2的五种授权方式。
其实到这里,就明了了,只要扩展TokenGranter
就可以实现自定义的授权方式。
这里以拓展手机+密码+验证码的授权方式为例,其他都一个道理。
一、自定义Granter
public class MobilePwdCodeTokenGranter extends AbstractPwdCodeTokenGranter {
//授权类型:mobile_pwd_code
private static final String GRANT_TYPE = GrantTypeEnum.MOBILE_PWD_CODE.getType();
private static final int CODE_LENGTH = 5;
private final AuthenticationManager authenticationManager;
public MobilePwdCodeTokenGranter(AuthorizationServerTokenServices tokenServices,
ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, final AuthenticationManager authenticationManager) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
}
@Override
protected Integer getCodeLength() {
return CODE_LENGTH;
}
@Override
protected Authentication createAuthenticationToken(String username, String password, String code) {
return new MobilePwdCodeAuthenticationToken(username,password,code);
}
@Override
protected AuthenticationManager authenticationManager() {
return this.authenticationManager;
}
}
对于带图形验证码的授权方式,所需传入的参数都一样,因此封装一个抽象父类,抽象类来继承AbstractTokenGranter
,后面自定义Granter继承该抽象类即可。
/**
* @author xiaoyunshi
* @date 2020/12/14 5:14 下午
*/
public abstract class AbstractPwdCodeTokenGranter extends AbstractTokenGranter {
protected AbstractPwdCodeTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
}
protected abstract Integer getCodeLength();
protected abstract Authentication createAuthenticationToken(String username, String password, String code,String uuid);
protected abstract AuthenticationManager authenticationManager();
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
//这里获取传入的参数
String username = parameters.get("username");
String password = parameters.get("password");
String code = parameters.get("code");
if (StringUtils.isEmpty(code) || code.length() != getCodeLength()) {
throw new InvalidGrantException("验证码错误");
}
//创建未认证的Authentication 『子类实现』
Authentication userAuth = createAuthenticationToken(username, password, code);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager().authenticate(userAuth);
} catch (AccountStatusException ase) {
throw new InvalidGrantException(ase.getMessage());
} catch (BadCredentialsException e) {
throw new InvalidGrantException(e.getMessage());
}
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate user : " + username);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
在authenticationManager().authenticate(userAuth)
中,会调用ProviderManager
的authenticate
方法。
具体的认证就是在Provider中处理的,因此我们也要自定义一个Provider进行认证,可以参考默认的密码模式的DaoAuthenticationProvider
类进行重写:
二、自定义AuthenticationProvider
public class MobilePwdCodeAuthenticationProvider implements AuthenticationProvider {
private BaseUserDetailsService baseUserDetailsService;
private RedisTemplate redisTemplate;
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//自定义的AuthenticationToken,保存认证的信息
MobilePwdCodeAuthenticationToken authToken = (MobilePwdCodeAuthenticationToken) authentication;
//调用userdetailService获取认证信息(按自己的业务实现)返回封装好的SysAuthUser
SysAuthUser authUser = (SysAuthUser) baseUserDetailsService.loadUserByMobile((String) authToken.getPrincipal());
if (authUser == null) {
throw new UsernameNotFoundException("手机号未注册");
}
String mobile = (String) authToken.getPrincipal();
String cachedCode = getCacheCode();
String password = (String) authToken.getCredentials();
String inputCode = authToken.getCode();
if (StringUtils.isEmpty(cachedCode)) {
throw new InvalidGrantException("验证码已失效");
} else if (!inputCode.equals(cachedCode)) {
throw new InvalidGrantException("验证码错误");
} else {
removeCode(mobile);
}
if (!passwordEncoder.matches(password, authUser.getPassword())) {
throw new BadCredentialsException("账户或密码错误");
}
//认证成功后构造一个新的AuthenticationToken,传入认证好的用户信息和权限信息等
MobilePwdCodeAuthenticationToken authenticationResult = new MobilePwdCodeAuthenticationToken(authUser, authUser.getPassword(), authUser.getAuthorities());
authenticationResult.setDetails(authToken.getDetails());
return authenticationResult;
}
private void removeCode(String username) {
redisTemplate.delete(RedisKeys.LOGIN_PREFIX + username);
}
private String getCacheCode() {
//当然这里图形验证码需要进行唯一标识,不能使用单单的一个key
String code = (String) redisTemplate.opsForValue().get(RedisKeys.LOGIN_PREFIX);
return code;
}
public void setBaseUserDetailsService(BaseUserDetailsService baseUserDetailsService) {
this.baseUserDetailsService = baseUserDetailsService;
}
public void setRedisTemplate(RedisTemplate jsonRedisTemplate) {
this.redisTemplate = jsonRedisTemplate;
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public boolean supports(Class<?> authentication) {
return MobilePwdCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
最后,就是设计认证信息的AuthenticationToken
类,这里可以根据UsernamePasswordAuthenticationToken
进行编写。
三、自定义AuthenticationToken
public class MobilePwdCodeAuthenticationToken extends AbstractAuthenticationToken {
private Object principal;
/**
* 图片验证码
*/
private String code;
private Object credentials;
public MobilePwdCodeAuthenticationToken(String mobile, String password, String code) {
super(null);
//第一次在Granter的createAuthenticationToken中创建,这时传入的是手机号
this.principal = mobile;
this.code = code;
this.credentials=password;
//第一次未认证
this.setAuthenticated(false);
}
/**
* 认证通过后走这个构造
* @param principal 认证后的对象
* @param authorities 认证后查询到的权限
*/
public MobilePwdCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal=principal;
this.credentials=credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return this.credentials;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
public String getCode() {
return code;
}
}
OK,这样就可以了,最后来的配置类配置Provider:
@Component
public class MobilePwdCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private BaseUserDetailsService baseUserDetailsService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
public PasswordEncoder bCryptPasswordEncoder;
@Override
public void configure(HttpSecurity http) throws Exception {
// 构建一个MobileAuthenticationProvider实例
MobilePwdCodeAuthenticationProvider provider = new MobilePwdCodeAuthenticationProvider();
provider.setBaseUserDetailsService(baseUserDetailsService);
provider.setRedisTemplate(stringRedisTemplate);
provider.setPasswordEncoder(bCryptPasswordEncoder);
http.authenticationProvider(provider);
}
}
这里最终会将自定义的Provider添加到AuthenticationManagerBuilder
的 private List<AuthenticationProvider> authenticationProviders
中。
最后重新配置 下TokenGranter:
四、配置TokenGranter
@Configuration
public class TokenGranterConfig {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private UserDetailsService baseUserDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore tokenStore;
@Autowired(required = false)
private List<TokenEnhancer> tokenEnhancer;
private RandomValueAuthorizationCodeServices authorizationCodeServices;
private boolean reuseRefreshToken = true;
private AuthorizationServerTokenServices tokenServices;
private TokenGranter tokenGranter;
/**
* 授权模式
*/
@Bean
public TokenGranter tokenGranter() {
if (tokenGranter == null) {
tokenGranter = new CompositeTokenGranter(getAllTokenGranters());
}
return tokenGranter;
}
/**
* 所有授权模式:默认的5种模式 + 自定义的模式
*/
private List<TokenGranter> getAllTokenGranters() {
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
//获取默认的授权模式
List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory);
if (authenticationManager != null) {
// 添加自定义的一些TokenGranter
tokenGranters.add(new MobilePwdCodeTokenGranter(tokenServices, clientDetailsService, requestFactory, authenticationManager));
}
return tokenGranters;
}
/**
* 默认的授权模式
*/
private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerTokenServices tokenServices
, AuthorizationCodeServices authorizationCodeServices, OAuth2RequestFactory requestFactory) {
List<TokenGranter> tokenGranters = new ArrayList<>();
// 添加授权码模式
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
// 添加刷新令牌的模式
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
// 添加隐士授权模式
tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory));
// 添加客户端模式
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));
if (authenticationManager != null) {
// 添加密码模式
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
}
return tokenGranters;
}
private AuthorizationServerTokenServices tokenServices() {
if (tokenServices != null) {
return tokenServices;
}
this.tokenServices = createDefaultTokenServices();
return tokenServices;
}
private AuthorizationCodeServices authorizationCodeServices() {
if (authorizationCodeServices == null) {
authorizationCodeServices = new InMemoryAuthorizationCodeServices();
}
return authorizationCodeServices;
}
private OAuth2RequestFactory requestFactory() {
return new DefaultOAuth2RequestFactory(clientDetailsService);
}
private DefaultTokenServices createDefaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(tokenEnhancer());
addUserDetailsService(tokenServices, this.baseUserDetailsService);
return tokenServices;
}
private TokenEnhancer tokenEnhancer() {
if (tokenEnhancer != null) {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(tokenEnhancer);
return tokenEnhancerChain;
}
return null;
}
private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
}
}
}
五、添加到EndPoints
然后在AuthorizationServerConfigurerAdapter配置类中将新的授权类添加到endPoints中:
@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenGranter tokenGranter;
.................................................
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.accessTokenConverter(jwtAccessTokenConverter)
//认证管理器
.authenticationManager(authenticationManager)
.tokenStore(tokenStore)
//用户信息service
.userDetailsService(baseUserDetailsService)
//添加重新注入的tokenGranter
.tokenGranter(tokenGranter)
;
}
}
这样就OK了,申请令牌时,只需将grant_type改为granter中设置的类型即可,当前如果添加了其他参数,比如这里我们添加了验证码 code,那也要把code作为参数传入进来。
六、流程分析
重新回到上面暂停的流程。
在CompositeTokenGranter
的grant()
方法中打个断点:
可以看到已经加载了5个默认授权类型和自定义的授权类型,当然还有我添加的其他几个,这里第6个就是手机+密码+验证码的授权方式类。
接着来到抽象类AbstractTokenGranter
的grant方法中,
if (!this.grantType.equals(grantType)) {
return null;
}
这里匹配授权方式通过,就会进行后面的流程。
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
if (logger.isDebugEnabled()) {
logger.debug("Getting access token for: " + clientId);
}
return getAccessToken(client, tokenRequest);
接着会到数据库查询当前的客户端信息,validateGrantType
中拿到数据库中该客户端配置的所有授权方式,看看是否有当前的授权类型(所以别忘了在对应的客户端中把自定义的授权方式添加信息),有就通过,否则抛异常。
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
这里就会调用我们自定义的子类MobileCodeTokenGranter
的getOAuth2Authentication
方法:
完成自定义的校验工作,最后返回:
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
userAuth这里就是MobilePwdCodeAuthenticationToken
对象。
最后调用tokenServices.createAccessToken
就完成了令牌的申请。
over。