一. 背景
最近在学习并使用SpringSecurty Oauth2, 已经实现账号密码的授权登陆, 需要新增一个手机号验证码的授权登陆.
在翻阅大量文章, 发现实现方式都比较复杂, 大部分是自己写filter和拦截器来做处理. 代码量较大, 而且不利于阅读跟扩展.
经过一整天的学习和探索, 大概明白Oauth2四种场景的授权流程, 又恰好有幸看到某一个大神的文章, 给予启发: https://www.appblog.cn/2019/10/09/Spring Security Oauth2 中优雅的扩展自定义(短信验证码)登录方式/
如果您正好和我有一样的需求, 那么恭喜您, 参考此文章能解决您的问题 (本人已踩大量的坑)
二. 实现思路
经过学习发现, SpringSecurityOauth2的登陆逻辑, 是有该org.springframework.security.oauth2.provider.TokenGranter完成的, 账号密码的验证是在对应实现类org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter里完成.
SpringSecurity找对应的TokenGranter是根据grant_type找到的, 比如账号密码的登陆, 我们需要传一个grant_type:password, SpringSecurty就会根据grant_type找到对应的TokenGranter.
所以发现, 我们可以自定义一个SMSCodeTokenGranter也去实现TokenGranter,用来验证手机号验证码的登陆, 然后添加一个grant_type为sms.
这里的难点是, SpringSecurity内置写死了TokenGranter, 所以我们需要覆盖掉原来的 使用自己的.
三. 代码实现
授权认证配置
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserDetailServiceImpl userDetailService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
@Autowired
public CustomUserDetailsService customUserDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("tplus-client")
.secret(passwordEncoder.encode("Jason295"))
.scopes("all")
// sms 为自己添加的授权方式
.authorizedGrantTypes("password", "refresh_token", "sms")
.accessTokenValiditySeconds(3600*24) // 24小时
.refreshTokenValiditySeconds(3600*24*7); // 7天
}
/**
* 自定义TokenGranter
*/
private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
TokenGranter tokenGranter = new TokenGranter() {
private CompositeTokenGranter delegate;
@Override
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (delegate == null) {
delegate = new CompositeTokenGranter(getDefaultTokenGranters(endpoints));
}
return delegate.grant(grantType, tokenRequest);
}
};
return tokenGranter;
}
/**
* 这是从spring 的代码中 copy出来的, 默认的几个TokenGranter, 还原封不动加进去.
* 主要目的是覆盖原来的List,方便我们添加自定义的授权方式,比如SMSCodeTokenGranter短信验证码授权
*/
private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {
AuthorizationServerTokenServices tokenServices = endpoints.getDefaultAuthorizationServerTokenServices();
AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices,
authorizationCodeServices, endpoints.getClientDetailsService(), requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, endpoints.getClientDetailsService(), requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, endpoints.getClientDetailsService(),
requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(
new ClientCredentialsTokenGranter(tokenServices, endpoints.getClientDetailsService(), requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager,
tokenServices, endpoints.getClientDetailsService(), requestFactory));
}
// 这里就是我们自己的授权验证
tokenGranters.add(new SMSCodeTokenGranter(tokenServices, endpoints.getClientDetailsService(), requestFactory, "sms"));
// 再有其他的验证, 就往下面添加....
return tokenGranters;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
enhancerChain.setTokenEnhancers(delegates); // 配置JWT的内容增强器
// 替换成我们自定义的TokenGranter,因为里面包含我们自己的授权验证
endpoints.tokenGranter(tokenGranter(endpoints));
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailService) // 配置加载用户信息的服务
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain);
}
/**
* 必须要有这个配置,不然Oauth2的授权请求会报错401没有权限.
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
/**
* 这里使用的是jwt的keyPair
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}
}
自定义的短信验证码授权验证
public class SMSCodeTokenGranter extends AbstractTokenGranter {
public SMSCodeTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
String userMobileNo = parameters.get("mobile"); //客户端提交的用户名
String smsCode = parameters.get("smscode"); //客户端提交的验证码
/** 下面写自己的验证逻辑 */
SecurityUser user = new SecurityUser();
user.setAppId("111");
user.setPhone("11111111");
Authentication userAuth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
四. Postman测试