在我们实际开发过程中SpringSecurityOAuth2默认提供的5种授权模式不够用,那么就需要我们自己来定义授权模式,可能有人不是真的很了解SpringSecurityOauth2框架的使用,比如我们要开发一种短信验证码授权登录的场景,可能有些程序员直接就编写一个controller然后自己在controller中组装token,或者是沿用SpringSecurity的过滤器思维,编写一个短信验证么的过滤器,配置在过滤器链上,按照SpringSecurity认证的思维,加上一个登录成功处理器,在登录成功处理器生成Token,这两种写法其实不是很正规,正规的写法应该是按照SpringSecurityOAuth2框架的设计思想来完成自定义授权模式。SpringSecurityOAuth2的授权思想和SpringSecurity有点区别,区别就再授权认证的时机不同,SpringSecurityOAuth2是在请求进入controller后得到请求参数,然后使用SpringSecurityOAuth2提供的一些内置组件完成Token的生成,而SpringSecurity则是依赖过滤器链来实现的,SpringSecurity是在请求未到达controller时,被匹配的过滤器拦截,然后进行认证,生成Token的,所以这是两种框架设计实现,不过SpringSecurityOAuth沿用了SpringSecurity部分认证流程,注意二者是认证时机不同!
在写SpringSecurityOAuth2自定义授权模式的时候,建议先查看这几篇文章SpringSecurityOAuth2授权流程源码分析、SpringSecurityOAuth2授权流程加载源码分析,不然涉及到授权模式中一些概念,以及一些组件就有点搞不清楚了,本文将以最常见的短信登录这种模式,完成短信验证码自定义授权模式
当我们需要自定义授权模式之前,请先完成SpringSecurityOAuth2默认的几种模式。也就是配置好AuthorizationServerConfig、WebSecurityConfig,确保SpringSecurityOAuth2默认几种模式能跑起来。
1.短信登录用户验证信息封装类
/**
* @author TAO
* @description: 短信登录用户验证信息封装类
* @date 2021/4/17 17:01
*/
@Slf4j
public class SmsVerificationCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public SmsVerificationCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
public SmsVerificationCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
super.setAuthenticated(true);//设置为已认证
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
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");
}
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
2.短信验证码校验逻辑的类
/**
* @author TAO
* @description: 提供短信验证码校验逻辑的类
* @date 2021/4/17 17:02
*/
@Slf4j
public class SmsVerificationCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;//得到UserDetailsService对象用来获取用户信息
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
//此方法就是通过请求参数查询数据库用户信息,进行匹配
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsVerificationCodeAuthenticationToken authenticationToken = (SmsVerificationCodeAuthenticationToken) authentication;
log.info("进行短信身份验证的逻辑");
log.info("(JSONObject) authenticationToken.getPrincipal()===>" + authenticationToken.getPrincipal());
UserDetails user = new YYExpandUser(2, 3, "YYYYYY", "111111111", true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("sys_menu_edit"));
/*if( authenticationToken.getPrincipal().toString().equals("100")) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
*/
SmsVerificationCodeAuthenticationToken smsVerificationCodeAuthenticationToken = new SmsVerificationCodeAuthenticationToken(user, user.getAuthorities());
smsVerificationCodeAuthenticationToken.setDetails(authenticationToken.getDetails());
return smsVerificationCodeAuthenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
// 判断authentication是否是SmsCodeAuthenticationToken类型
return SmsVerificationCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
3.短信验证么登录复合配置
@Component
public class SmsVerificationCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
protected AuthenticationSuccessHandler successAuthenticationHandler;//表单登录成功处理器
@Autowired
protected AuthenticationFailureHandler failureAuthenticationHandler;//表单登录失败处理器
@Autowired
private UserDetailsService userDetailsService;//又来得到用户信息的
@Override
public void configure(HttpSecurity http) {
SmsVerificationCodeAuthenticationFilter smsVerificationCodeAuthenticationFilter=new SmsVerificationCodeAuthenticationFilter();
smsVerificationCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));//得到认证管理器
smsVerificationCodeAuthenticationFilter.setAuthenticationSuccessHandler(successAuthenticationHandler);
smsVerificationCodeAuthenticationFilter.setAuthenticationFailureHandler(failureAuthenticationHandler);
SmsVerificationCodeAuthenticationProvider smsVerificationCodeAuthenticationProvider=new SmsVerificationCodeAuthenticationProvider();
smsVerificationCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
http
/**
* 将短信验证码校验Provider绑定到authenticationProvider中,就是为了在
* SpringSecurity的SmsVerificationCodeAuthenticationFilter中return this.getAuthenticationManager().authenticate(authRequest);处找到当前过滤器的Provider
* SpringSecurityOAuth2的SmsVerificationCodeTokenGranter中userAuth = authenticationManager.authenticate(userAuth);处找到当前授权策略的Provider
*/
.authenticationProvider(smsVerificationCodeAuthenticationProvider)
.addFilterAfter(smsVerificationCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
上面两个是不是很熟悉,对没错,就和SpringSecurity中的写法一样,文章开头前言部分已经提到过了,认证流程是一样的,所以和SpringSecurity同样需要
SmsVerificationCodeAuthenticationToken
和SmsVerificationCodeAuthenticationProvider
、SmsVerificationCodeAuthenticationConfig
,我这里的SmsVerificationCodeAuthenticationConfig
配置类中是有SmsVerificationCodeAuthenticationFilter
,你们写的时候可以完全去掉,我这里是故意没去掉的,就是为了给你们体现SpringSecurityOAuth2和SpringSecurity的认证逻辑是一样的,所以我这里才没有去掉!当然不去掉也没关系,这里如果去掉是可以不用编写SmsVerificationCodeAuthenticationFilter
这个玩意的。
4.SecurityConfig配置挂载一下
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SmsVerificationCodeAuthenticationConfig smsVerificationCodeAuthenticationConfig;//短信安全验证配置
@Override
protected void configure(HttpSecurity http) throws Exception {
.apply(smsVerificationCodeAuthenticationConfig);//关键代码
}
}
以上4步操作和SpringSecurity配置差不多,等下有个报错,下面会演示,没别的意思,就是让各位知道每个配置是干嘛的!
5.短信验证码授权
/**
* @author TAO
* @description: 短信验证码授权
* 实际上就是复制ResourceOwnerPasswordTokenGranter中的代码,进行getOAuth2Authentication方法部分修改,
* 这个类有点类似SpringSecurity中的各种AuthenticationFilter,也就是类似过滤器的作用
* @date 2021/4/16 21:28
*/
@Slf4j
public class SmsVerificationCodeTokenGranter extends AbstractTokenGranter {
private static final String grantType="sms_code";//授权类型,和password是一样的作用
private final AuthenticationManager authenticationManager;
public SmsVerificationCodeTokenGranter(AuthenticationManager authenticationManager,AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, grantType);
}
protected SmsVerificationCodeTokenGranter(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) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());//得到请求参数
String phone = parameters.get(SecurityConstants.PHONE_PARAMETER);//得到手机号
String verification_code = parameters.get(SecurityConstants.VERIFICATION_CODE);//得到手机验证码
String client_id = parameters.get(SecurityConstants.CLIENT_ID);//得到client_id
String client_secret = parameters.get(SecurityConstants.CLIENT_SECRET);//得到client_secret
log.info("phone===>" + phone);
log.info("verification_code===>" + verification_code);
log.info("client_id===>" + client_id);
log.info("client_secret===>" + client_secret);
Authentication userAuth= new SmsVerificationCodeAuthenticationToken(parameters);
((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,phone number is: " + phone);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
这个的作用有点类似SpringSecurity中的
SmsVerificationCodeAuthenticationFilter
,SmsVerificationCodeAuthenticationFilter
说白了就是根据配置好要拦截的请求URL进行拦截,然后引导到对应的检验用户身份的SmsVerificationCodeAuthenticationProvider
上,那么SmsVerificationCodeTokenGranter
其实就是SpringSecurityOAuth2
中通过授权模式的不同将请求中携带的grant_type引导到对应的SmsVerificationCodeAuthenticationProvider
中的作用
6.授权模式配置类
/**
* @author TAO
* @description: token授权模式配置类
* 之所以下面这么多配置是因为AuthorizationServerEndpointsConfigurer为final类,无法继承,所以只能copy一些方法,保证流程走通
* @date 2021/4/16 21:30
*/
@Configuration
public class TokenGranterConfig {
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore redisTokenStore;
@Autowired
private TokenEnhancer jwtTokenEnhancer;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
private AuthorizationCodeServices authorizationCodeServices;
private boolean reuseRefreshToken = true;
private AuthorizationServerTokenServices tokenServices;
private TokenGranter tokenGranter;
//默认写法
@Bean
public 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(getAllTokenGranters());
}
return delegate.grant(grantType, tokenRequest);
}
};
}
return tokenGranter;
}
//获取SpringSecurityOAuth2默认提供的5种授权模式+自定义模式
private List<TokenGranter> getAllTokenGranters() {
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory);//获取SpringSecurityOAuth2默认提供的5种授权模式
if (authenticationManager != null) {
//自定义授权模式
// 添加自定义授权模式(实际是密码模式的复制)
tokenGranters.add(new SmsVerificationCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
}
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);
}
//默认写法+JWT生成token、采用自定义JWT模板
private DefaultTokenServices createDefaultTokenServices() {
//使用JWT生成Token,设置自定义JWT模板
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(jwtTokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(enhancers);
//默认写法
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(redisTokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(enhancerChain);
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}
//默认写法
private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
if (userDetailsService != null) {
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken>(userDetailsService));
tokenServices.setAuthenticationManager(new ProviderManager(Arrays.<AuthenticationProvider>asList(provider)));
}
}
}
其中标注默认写法的直接从AuthorizationServerEndpointsConfiguration类中copy出来,不用更改,但是最好知道是干什么用的。这里我就不过多解释了。不懂的可以自己调试或者看我往期文章,
这里有两个点需要注意如下代码
//默认写法+JWT生成token、采用自定义JWT模板
private DefaultTokenServices createDefaultTokenServices() {
//使用JWT生成Token,设置自定义JWT模板
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(jwtTokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(enhancers);
//默认写法
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(redisTokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(enhancerChain);
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}
我这里是在默认写法前面加了enhancers ,目的就是为了使用JWT+JWT自定义模板,另一个如下
//获取SpringSecurityOAuth2默认提供的5种授权模式+自定义模式
private List<TokenGranter> getAllTokenGranters() {
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory);//获取SpringSecurityOAuth2默认提供的5种授权模式
if (authenticationManager != null) {
//自定义授权模式
// 添加自定义授权模式(实际是密码模式的复制)
tokenGranters.add(new SmsVerificationCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
}
return tokenGranters;
}
这里就是将我们自定义的短信验证码授权模式加入到SpringSecurityOAuth2中的
7.认证授权服务配置
这个配置和基本配置差不多只是多了个TokenGranter的配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenGranter tokenGranter;
@Primary
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)//开启密码模式认证
.tokenGranter(tokenGranter)
.pathMapping("/oauth/confirm_access", "/custom/confirm_access")//替换/oauth/confirm_access为/custom/confirm_access
;
}
}
启动测试
默认密码模式
自定义短信验证码模式
如果有返回为Unauthorized grant type: sms_code,那么需要检查端点是否配置了sms_code授权模式
如果有碰到No AuthenticationProvider found for…报错那么请见SpringSecurityOAuth2自定义授权模式Handling error: ProviderNotFoundException, No AuthenticationProvider foun