概述
大家都知道在oauth2认证体系中有四种授权模式:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 客户端模式(client credentials)
- 密码模式(password)
那么如何新增一个自定义的授权模式,比如像下面这样根据手机号和短信验证码进行登录呢?
要自定义授权模式我们得先了解下oauth2.0的整体认证过程,认证入口在 org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#postAccessToken
方法中
@RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.POST}
)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
...
}
通过阅读源码可以梳理出核心认证逻辑代码的执行顺序(password模式):
核心源码解读
TokenEndpoint#postAccessToken(...)
主入口
OAuth2AccessToken token =
getTokenGranter().grant(tokenRequest.getGrantType(),
tokenRequest);
CompositeTokenGranter#grant(String grantType,TokenRequest tokenRequest )
负责从所有的TokenGranter中根据授权类型找到具体的TokenGranter
public class CompositeTokenGranter implements TokenGranter {
private final List<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;
}
...
}
那么这里的tokenGranters又是从哪来的呢?答案是oauth2认证服务器端点配置类 AuthorizationServerEndpointsConfigurer
public final class AuthorizationServerEndpointsConfigurer {
...
private TokenGranter tokenGranter;
public TokenGranter getTokenGranter() {
return tokenGranter();
}
//默认的四种授权模式+Refresh令牌模式
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;
}
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;
}
...
}
可以看到Spring已经把把默认的四种授权模式+刷新令牌的模式的配置在代码中写死了,那么如何让Spring能识别我们自定义的授权模式呢?
我们可以通过配置类覆盖TokenGranter,在里面注入我们自定义的授权模式!
ProviderManager#authenticate(Authentication authentication)
这个类是提供了认证的实现逻辑和流程,他负责从所有的AuthenticationProvider中找出具体的Provider进行认证
public class ProviderManager implements AuthenticationManager, MessageSourceAware,
InitializingBean {
...
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
//遍历所有的providers使用supports方法判断该provider是否支持当前的认证类型
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
//找到具体的provider进行认证
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
throw lastException;
}
...
}
代码实现(核心代码)
image.png
使用手机号登录时先在表单中输入正确的手机号码,请求后端获取验证码。(此时后台服务一般会将手机号码和验证码进行关联,并设置一个较短时间的有效期)
手机获取到验证码后将其输入到表单中即可登录,后端框架将手机号与用户进行关联认证。
短信验证需要两个基础表单数据:手机号码,短信验证码。
本文并没有实现表单登录方式,是使用postman的方式进行认证。使用上图只是让大家对短信认证过程有个印象。
”
SmsCodeAuthenticationToken
/**
* <p>
* <code>SmsAuthenticationToken</code>
* </p>
* Description:
* 实现手机号登录,参考org.springframework.security.authentication.UsernamePasswordAuthenticationToken
* @author javadaily
* @date 2020/7/13 8:44
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
/**
* 账号主体信息,手机号验证码登录体系中代表 手机号码
*/
private final Object principal;
/**
* 构建未授权的 SmsCodeAuthenticationToken
* @param mobile 手机号码
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 构建已经授权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(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;
}
@Override
public void setAuthenticated(boolean isAuthenticated) {
if(isAuthenticated){
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}else{
super.setAuthenticated(false);
}
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
SmsCodeAuthenticationProvider
/**
* Description:
* 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
* @author javadaily
* @date 2020/7/13 13:07
*/
@Log4j2
public class SmsCodeAuthenticationProvider implements AuthenticationProvider{
private IUserService userService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;
userService = SpringContextHolder.getBean(IUserService.class);
String mobile = (String) smsCodeAuthenticationToken.getPrincipal();
//校验手机号验证码
checkSmsCode(mobile);
User user = userService.getUserByMobile(mobile);
if(null == user){
throw new BadCredentialsException("Invalid mobile!");
}
//授权通过
UserDetails userDetails = buildUserDetails(user);
return new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
}
/**
* 构建用户认证信息
* @param user 用户对象
* @return UserDetails
*/
private UserDetails buildUserDetails(User user) {
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList("ADMIN")) ;
}
/**
* 校验手机号与验证码的绑定关系是否正确
* todo 需要根据业务逻辑自行处理
* @author javadaily
* @date 2020/7/23 17:31
* @param mobile 手机号码
*/
private void checkSmsCode(String mobile) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//获取验证码
String smsCode = request.getParameter("smsCode");
if(StringUtils.isEmpty(smsCode) || !"666666".equals(smsCode)){
throw new BadCredentialsException("Incorrect sms code,please check !");
}
//todo 手机号与验证码是否匹配
}
/**
* ProviderManager 选择具体Provider时根据此方法判断
* 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
}
短信验证码模式认证实现类,需要实现AuthenticationProvider,通过 supports
方法会被ProviderManager选中成为具体的认证实现类。手机号码与短信的关联关系需要根据自己业务场景实现,这里直接先写死。
配置类SmsCodeSecurityConfig
@Component
public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
/**
* 短信验证码配置器
* 所有的配置都可以移步到WebSecurityConfig
* builder.authenticationProvider() 相当于 auth.authenticationProvider();
* 使用外部配置必须要在WebSecurityConfig中用http.apply(smsCodeSecurityConfig)将配置注入进去
* @param builder
* @throws Exception
*/
@Override
public void configure(HttpSecurity builder) throws Exception {
//注入SmsCodeAuthenticationProvider
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
builder.authenticationProvider(smsCodeAuthenticationProvider);
}
}
此类主要实现SmsCodeAuthenticationProvider的注入,否则ProviderManager无法选到SmsCodeAuthenticationProvider。
SmsCodeTokenGranter
/**
* 扩展认证模式
* @author javadaily
* @date 2020/7/14 8:31
*/
public class SmsCodeTokenGranter extends AbstractTokenGranter{
private static final String GRANT_TYPE = "sms_code";
private final AuthenticationManager authenticationManager;
public SmsCodeTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
}
protected SmsCodeTokenGranter(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(tokenRequest.getRequestParameters());
String mobile = parameters.get("mobile");
Authentication userAuth = new SmsCodeAuthenticationToken(mobile);
((AbstractAuthenticationToken)userAuth).setDetails(parameters);
try {
userAuth = this.authenticationManager.authenticate(userAuth);
} catch (AccountStatusException ex) {
throw new InvalidGrantException(ex.getMessage());
} catch (BadCredentialsException ex) {
throw new InvalidGrantException(ex.getMessage());
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
throw new InvalidGrantException("Could not authenticate mobile: " + mobile);
}
}
}
继承AbstractTokenGranter扩展认证模式sms_code,需要将其添加到Spring中并通过grantType被选中。
配置类TokenGranterConfig
通过前面几步自定义认证的基础逻辑都已实现,接下来需要将我们的短信认证模式添加到Spring中,主要参考 org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters()
实现。
/**
*参考实现:org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer#getDefaultTokenGranters()
* @author javadaily
* @date 2020/7/14 8:38
*/
@Configuration
public class TokenGranterConfig {
@Autowired
private ClientDetailsService clientDetailsService;
private TokenGranter tokenGranter;
@Autowired
private TokenStore tokenStore;
@Autowired
TokenEnhancer tokenEnhancer;
@Autowired
private AuthenticationManager authenticationManager;
private AuthorizationServerTokenServices tokenServices;
private boolean reuseRefreshToken = true;
private AuthorizationCodeServices authorizationCodeServices;
@Autowired
private UserDetailsService userDetailsService;
@Bean
public TokenGranter tokenGranter(){
if(null == tokenGranter){
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;
}
private List<TokenGranter> getDefaultTokenGranters() {
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
List<TokenGranter> tokenGranters = new ArrayList();
//授权码模式
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
//refresh模式
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
//简化模式
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory);
tokenGranters.add(implicit);
//客户端模式
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));
if (authenticationManager != null) {
//密码模式
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
//短信验证码模式
tokenGranters.add(new SmsCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
}
return tokenGranters;
}
private AuthorizationServerTokenServices tokenServices() {
if (tokenServices != null) {
return tokenServices;
}
this.tokenServices = createDefaultTokenServices();
return tokenServices;
}
private AuthorizationServerTokenServices createDefaultTokenServices() {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore);
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(reuseRefreshToken);
tokenServices.setClientDetailsService(clientDetailsService);
tokenServices.setTokenEnhancer(tokenEnhancer);
addUserDetailsService(tokenServices, this.userDetailsService);
return tokenServices;
}
/**
* 添加预身份验证
* @param tokenServices
* @param userDetailsService
*/
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)));
}
}
/**
* OAuth2RequestFactory的默认实现,它初始化参数映射中的字段,
* 验证授权类型(grant_type)和范围(scope),并使用客户端的默认值填充范围(scope)(如果缺少这些值)。
*/
private OAuth2RequestFactory requestFactory() {
return new DefaultOAuth2RequestFactory(clientDetailsService);
}
/**
* 授权码API
* @return
*/
private AuthorizationCodeServices authorizationCodeServices() {
if (this.authorizationCodeServices == null) {
this.authorizationCodeServices = new InMemoryAuthorizationCodeServices();
}
return this.authorizationCodeServices;
}
}
修改认证服务器配置 AuthorizationServerConfig
在上面的TokenGranterConfig中已经创建了AuthorizationServerTokenServices,所以我们可以将AuthorizationServerConfig中的tokenServices功能删除,然后在方法 configure(AuthorizationServerEndpointsConfigurer endpoints)
中注入 tokenGranter
即可
@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private TokenGranter tokenGranter;
...
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenGranter(tokenGranter);
}
...
}
测试
-
正常测试 通过debug模式可以看到SmsCodeTokenGranter已经被加入Spring,并能正常返回jwt token了。
-
输入错误的手机号码进行认证
-
输入错误的短信验证码进行认证
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。