文章目录

  • 前言
  • Oauth2认证过程
    • 1、在进行认证时,会先根据认证类型即前端传入的grant_type,从所有的TokenGranter中找到具体匹配的TokenGranter
    • 2、在找到对应的TokenGranter之后,会调用getOAuth2Authentication方法进行认证
    • 3、关于 this.authenticationManager.authenticate(userAuth) 的实现
  • 实现
    • 1、创建我们的SmsCodeAuthenticationToken,这里我们的主体信息就只有手机号
    • 2、实现我们的认证服务提供者
    • 3、将我们的服务提供者添加到认证管理器中
    • 4、添加我们的短信验证码授权实现
    • 5、配置我们的自定义授权模式,接着上一篇,我们添加上短信验证模式
    • 6、最后,修改Oauth2的认证服务器Endpoints配置为我们自定义的授权模式
  • 关于Redis反序列化失败
  • 最后

前言

目前的大多数网站或者APP都会采取短信验证码的方式帮助用户登陆系统授权。
我们现在就在Oauth里添加该功能的实现。
其实在一篇博客中,我们仿照Security自带的密码模式实现并添加自己的密码模式。
因此,我们可以仿照密码模式在上一篇的基础上继续实现自己的短信验证码授权。

Oauth2认证过程

1、在进行认证时,会先根据认证类型即前端传入的grant_type,从所有的TokenGranter中找到具体匹配的TokenGranter

代码如下:

@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParamMap<String, String> parameters) throws HttpRequestMethodNotSupportedException {//代码@1...省略OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);//代码@7if (token == null) {throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());}return getResponse(token);}

2、在找到对应的TokenGranter之后,会调用getOAuth2Authentication方法进行认证

以用户名密码的实现类ResourceOwnerPasswordTokenGranter

  • 这个方法显示通过传入的tokenRequest获得到 username 和 password参数
  • 通过username 和 password 实例化出一个 UsernamePasswordAuthenticationToken 对象,继承自AuthenticationToken,用来存放主要的信息,后面我们可以创建一个属于自己短信验证码的主体类 SmsCodeAuthenticationToken
  • 将请求中的所有参数,赋予detail
    ((AbstractAuthenticationToken)userAuth).setDetails(parameters);
    里面主要存放这用来验证客户端的信息等。
  • 然后最重要的一步 userAuth = this.authenticationManager.authenticate(userAuth); 这里是认证的具体校验实现
  ...protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());String username = (String)parameters.get("username");String password = (String)parameters.get("password");parameters.remove("password");Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);((AbstractAuthenticationToken)userAuth).setDetails(parameters);Authentication userAuth;try {userAuth = this.authenticationManager.authenticate(userAuth);} catch (AccountStatusException var8) {throw new InvalidGrantException(var8.getMessage());} catch (BadCredentialsException var9) {throw new InvalidGrantException(var9.getMessage());}...省略}

3、关于 this.authenticationManager.authenticate(userAuth) 的实现

我们知道,AuthenticationManager是oauth2中的认证管理器,我们查看其缺省实现 ProviderManager:
其中 List providers中包含了所有的认证服务。
那么他又是怎么知道该用哪个服务去处理我们的请求的呢?

    private static final Log logger = LogFactory.getLog(ProviderManager.class);private AuthenticationEventPublisher eventPublisher;private List<AuthenticationProvider> providers;protected MessageSourceAccessor messages;private AuthenticationManager parent;private boolean eraseCredentialsAfterAuthentication;

查看ProviderManager中的authenticate方法:

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;int currentPosition = 0;int size = this.providers.size();Iterator var9 = this.getProviders().iterator();while(var9.hasNext()) {AuthenticationProvider provider = (AuthenticationProvider)var9.next();if (provider.supports(toTest)) {if (logger.isTraceEnabled()) {Log var10000 = logger;String var10002 = provider.getClass().getSimpleName();++currentPosition;var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));}try {result = provider.authenticate(authentication);if (result != null) {this.copyDetails(authentication, result);break;}} catch (InternalAuthenticationServiceException | AccountStatusException var14) {this.prepareException(var14, authentication);throw var14;} catch (AuthenticationException var15) {lastException = var15;}}}if (result == null && this.parent != null) {try {parentResult = this.parent.authenticate(authentication);result = parentResult;} catch (ProviderNotFoundException var12) {} catch (AuthenticationException var13) {parentException = var13;lastException = var13;}}if (result != null) {if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {((CredentialsContainer)result).eraseCredentials();}if (parentResult == null) {this.eventPublisher.publishAuthenticationSuccess(result);}return result;} else {if (lastException == null) {lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));}if (parentException == null) {this.prepareException((AuthenticationException)lastException, authentication);}throw lastException;}}

可以看到这一行代码:if (provider.supports(toTest))
即表示如果服务支持 toTest 这个类型: Class<? extends Authentication> toTest = authentication.getClass();
那么它就会调用这个服务的认证方法:result = provider.authenticate(authentication);
supports 方法是接口AuthenticationProvider中的一个方法:

public interface AuthenticationProvider {Authentication authenticate(Authentication var1) throws AuthenticationException;boolean supports(Class<?> var1);
}

我们打开一个实现类,以AbstractUserDetailsAuthenticationProvider抽象类为例
这个方法的实现表示这个服务支持 UsernamePasswordAuthenticationToken 这个类型的Token
当传入的Token类型是UsernamePasswordAuthenticationToken
并且在服务列表中循环到这个服务的具体实现服务时,就会触发该实现的认证方法
因此我们不仅要创建一个属于自己独一无二的Token类,而且我们还要让我们自己的服务支持这个类

public boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

实现

这里所有的实现都是都是模仿用户名密码的所有代码去实现的

1、创建我们的SmsCodeAuthenticationToken,这里我们的主体信息就只有手机号

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = 520L;//账号主体信息,手机号码private final Object principal;//构建未授权的 SmsCodeAuthenticationTokenpublic SmsCodeAuthenticationToken(String mobile) {super(null);this.principal = mobile;setAuthenticated(false);}//构建已经授权的 SmsCodeAuthenticationTokepublic SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities){super(authorities);this.principal = principal;super.setAuthenticated(true);}@Overridepublic Object getCredentials() {return null;}@Overridepublic Object getPrincipal() {return this.principal;}@Overridepublic 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);}}@Overridepublic void eraseCredentials() {super.eraseCredentials();}
}

2、实现我们的认证服务提供者

@Slf4j
@Configuration
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {private UsersDao usersDao;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken) authentication;String mobile = (String) smsCodeAuthenticationToken.getPrincipal();usersDao = SpringContextUtils.getBean(UsersDao.class);//查询手机号的详细信息QueryWrapper<Users> usersQueryWrapper =new QueryWrapper<>();usersQueryWrapper.eq("phone_no",mobile).eq("delete_flag",0);Users user = usersDao.selectOne(usersQueryWrapper);if(null!=user){//校验手机收到的验证码和rediss中的验证码是否一致checkSmsCode(mobile);//授权通过UserDetails userDetails = buildUserDetails(user);return new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());}else{throw new BadCredentialsException("该手机号未注册或未绑定账号!");}}/*** 构建用户认证信息* @param user 用户对象* @return UserDetails*/private UserDetails buildUserDetails(Users user) {return new org.springframework.security.core.userdetails.User(user.getUserName(),user.getPassword(),AuthorityUtils.createAuthorityList("ADMIN")) ;}/*** 校验手机号与验证码的绑定关系是否正确* 在调用短信验证码认之前我们需要先生成验证码,接口需要自己实现* Redis的存储风格按照自己的习惯,能够保证唯一即可* 然后根据手机号信息去Redis中查询对应的验证码是否正确* @param mobile 手机号码*/private void checkSmsCode(String mobile) {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//获取验证码String smsCode = request.getParameter("smsCode");//从redis中获取只当key的值String smsStr = JedisUtils.getObject("sms"+mobile);if(StringUtils.isEmpty(smsCode) || !smsCode.equals(smsStr)){throw new BadCredentialsException("验证码错误!");}}/*** ProviderManager 选择具体Provider时根据此方法判断* 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口*/@Overridepublic boolean supports(Class<?> authentication) {return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);}
}

3、将我们的服务提供者添加到认证管理器中

我们需要重写 WebSecurityConfigurerAdapter 中的configure(AuthenticationManagerBuilder auth)方法
加入我们的短信验证码服务

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {...省略@Beanpublic SmsCodeAuthenticationProvider providers(){SmsCodeAuthenticationProvider provider =new SmsCodeAuthenticationProvider();return provider;}@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth//添加自定义认证.authenticationProvider(providers()).userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());}
}

4、添加我们的短信验证码授权实现

我们的授权类型是 sms_code,
除了修改以下代码,其他基本和密码模式的实现相同
因为我们需要的时短信类型的Token从而触发短信验证码的认证服务

      Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());String mobile = parameters.get("mobile");Authentication userAuth = new SmsCodeAuthenticationToken(mobile);((AbstractAuthenticationToken)userAuth).setDetails(parameters);

全部代码:

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;}@Overrideprotected 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);}}
}

5、配置我们的自定义授权模式,接着上一篇,我们添加上短信验证模式

@Configuration
public class TokenGranterConfig {...其他省略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 CustomResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));//短信验证码模式tokenGranters.add(new SmsCodeTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));}return tokenGranters;}
}

6、最后,修改Oauth2的认证服务器Endpoints配置为我们自定义的授权模式

其实在上一篇我们已经修改了

@Configuration
@EnableAuthorizationServer
@EnableConfigurationProperties(RedisProperties.class)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {...省略@Autowiredprivate TokenGranter tokenGranter;/*** 认证服务器Endpoints配置*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {//自定义授权模式endpoints.tokenGranter(tokenGranter);}}

关于Redis反序列化失败

可以参考这篇博文

https://blog.csdn.net/CSDN877425287/article/details/120663052

资源服务器在解析 Redis 中的认证信息时,找不到自定义的 Token
在我项目中的情景是:

用户认证是通过 auth-biz/auth-service 项目实现的,在该项目中创建了 SmsCodeAuthenticationToken 类,
当我们使用短信验证码申请授权之后,通过 auth-biz/auth-serviceRedis 中生成了包含 SmsCodeAuthenticationToken 的认证信息。

后来由于项目添加了 gateway 服务 ,除特殊配置的接口路径之外,如 “oauth/*” 等,其他请求都将先通过 gateway 服务进行权限验证。

cloud-gateway 项目目录结构,创建时缺少 SmsCodeAuthenticationToken 这个类
权限验证放行的接口

而在 gateway 中进行解析时找不到 SmsCodeAuthenticationToken 这个类,导致反序列化失败,因此我们只要将 SmsCodeAuthenticationToken 这个类复制到 gateway 项目中即可(注意类的路径要保持一致)。

最后

这样我们的短信验证码模式就已经实现了,
在请求认证时,相对于用户名密码认证:
请求参数中的 grant_type 需要传 sms_code
请求参数中的 username 和 password 我们已经不需要了
我们需要替换为 mobile 和 smsCode

SpringCloud - Oauth2增加短信验证码验证登录相关推荐

  1. Spring Security简单增加短信验证码登录

    查网上资料增加短信验证码登录都要增加一大推,要重头写Spring Security的实现,我呢,只想在原来的密码登录基础上简单实现一下短信验证码登录. 1.首先得先一个认证类,来认证验证码是否正确,这 ...

  2. php 短信验证登录,短信验证码注册登录的实现,php接入的3种方法(附示例)

    上周,有朋友需要帮忙做一个关于手机短信验证码注册登录的功能,之前没有做过,于是我查查资料,汇总出PHP接入短信验证码的3种方法,现在和大家分享: 1.cURL $curl = curl_init(); ...

  3. 系统登录页面短信验证码方式登录实现

    近期公司有个需求,要求使用短信验证码登录,取代原来的图片验证码方式,在此记录一下我的实现方法,希望对你有所帮助 公司需求:目前只有账号和验证码方式验证登录,按照公司网络安全统一部署,要求所有公网系统都 ...

  4. Nodejs实现给手机发送短信验证码用于登录功能(免费短信)

    我们开发的应用通常需要通过手机短信验证码验证进行登录,方便了那些容易忘记密码的用户,同时也降低了是密码被盗的风险性. 文章目录 1.注册容联云通信账号(免费) 2.nodejs发送请求即可发送短信验证 ...

  5. 20、实现短信验证码的登录注册功能

    实现短信验证码的登录注册功能 第一步:查看接口内容 为什么用@RequestBody因为其中我们前端传过来的是json数据那么后端我们就要用@requestBody注解来接收了. 查看写这个实体类 这 ...

  6. 06-发送短信验证码实现登录功能

    1.发送短信验证码实现登录功能的流程 1.1.获取验证码流程 1.2.登录流程 1.3.页面带有图形验证码的流程 2. 注册登录二合一页面的开发 2.1.将src目录下的App.vue页面上通用显示的 ...

  7. 发送短信验证码执行登录操作

    短信服务介绍: 目前市场上有很多第三方提供的短信服务,这些第三方短息服务会和各个运营商(移动,联通,电信)对接,我们只需要注册成为会员,并且按照提供的开发文档进行调用就可以完成发送短信,需要说明的是, ...

  8. springsecurity自定义短信验证码认证登录流程

    文章目录 前言 验证码存储我们采用redis作为缓存 (注意,这里为了测试方便改为手动设置验证码) 经过上面接口,验证码已经存入到redis中,下面开始认证流程 自定义SmsCodeAuthentic ...

  9. 使用阿里云短信验证码API发送短信验证码(配置,获取短信验证码,注册,登录,密码重置)

    获取阿里云短信验证码需要的配置信息. 如果是新用户,可以免费领取3个月,老用户的话就只能购买了,但是也不贵. 申请短信签名 申请短信模板 编写发送短信验证码的工具类 代码中我已经进行了详细的注释,也写 ...

最新文章

  1. 参悟JavaScript
  2. 百万奖金悬赏AI垃圾分类,就问你来不来?
  3. 移动端页面——js控制制作
  4. Matlab2012a下配置LibSVM—3.18
  5. 油管螺纹尺寸对照表_数控加工过程中,如何区分新旧螺纹牌号?
  6. 近期学习的原生JS知识以及jQuery框架
  7. node 加密解密模块_NODE.JS加密模块CRYPTO常用方法介绍
  8. gdb调试多进程和多线程命令
  9. 信息学奥赛C++语言:输出判断
  10. 理解 JS 回调函数中的 this
  11. 机器学习之01篇:初步窥探
  12. 十、Mysql执行计划详细解析
  13. Python——顺序结构
  14. 解剖一些外挂制作原理(DNF)
  15. 计算机共享文件输入网络密码是什么,Win7共享文件时需要输入网络密码怎么办?...
  16. 基于Nginx搭建RTMP-HLS视频直播服务器(推流+拉流)
  17. vsftpd的安装及使用
  18. 搜狐新浪ip库查询接口的使用
  19. 计算机职称证的用途,计算机软考高项过了有什么用处
  20. 在Ubuntu20.04中继续使用linux版“网络调试助手”的方法

热门文章

  1. git difftool 之 vimdiff
  2. JavaWeb项目:新闻发布系统02(新闻系统功能制作)
  3. NoSQL数据库入门与实践答案----第二章
  4. Rest Ful 设计规范
  5. thinkpad电脑连不上网只有飞行模式解决办法
  6. CANoe-是如何模拟网络节点通信的
  7. Vigenere密码(维吉尼亚密码)c语言实现
  8. 计算机组成原理网易云,计算机组成原理之CPU
  9. 数字新局面之下,网易数帆有何新思考?
  10. STP生成树的BID字段值介绍