SpringSceurity(5)---短信验证码登陆功能

有关SpringSceurity系列之前有写文章

1、SpringSecurity(1)---认证+授权代码实现

2、SpringSecurity(2)---记住我功能实现

3、SpringSceurity(3)---图形验证码功能实现

4、SpringSceurity(4)---短信验证码功能实现

一、短信登录验证机制原理分析

了解短信验证码的登陆机制之前,我们首先是要了解用户账号密码登陆的机制是如何的,我们来简要分析一下Spring Security是如何验证基于用户名和密码登录方式的,

分析完毕之后,再一起思考如何将短信登录验证方式集成到Spring Security中。

1、账号密码登陆的流程

一般账号密码登陆都有附带 图形验证码记住我功能 ,那么它的大致流程是这样的。

1、 用户在输入用户名,账号、图片验证码后点击登陆。那么对于springSceurity首先会进入短信验证码Filter,因为在配置的时候会把它配置在
UsernamePasswordAuthenticationFilter之前,把当前的验证码的信息跟存在session的图片验证码的验证码进行校验。 2、短信验证码通过后,进入 UsernamePasswordAuthenticationFilter 中,根据输入的用户名和密码信息,构造出一个暂时没有鉴权的
UsernamePasswordAuthenticationToken,并将 UsernamePasswordAuthenticationToken 交给 AuthenticationManager 处理。 3、AuthenticationManager 本身并不做验证处理,他通过 for-each 遍历找到符合当前登录方式的一个 AuthenticationProvider,并交给它进行验证处理
,对于用户名密码登录方式,这个 Provider 就是 DaoAuthenticationProvider。 4、在这个 Provider 中进行一系列的验证处理,如果验证通过,就会重新构造一个添加了鉴权的 UsernamePasswordAuthenticationToken,并将这个
token 传回到 UsernamePasswordAuthenticationFilter 中。 5、在该 Filter 的父类 AbstractAuthenticationProcessingFilter 中,会根据上一步验证的结果,跳转到 successHandler 或者是 failureHandler。

流程图

2、短信验证码登陆流程

因为短信登录的方式并没有集成到Spring Security中,所以往往还需要我们自己开发短信登录逻辑,将其集成到Spring Security中,那么这里我们就模仿账号

密码登陆来实现短信验证码登陆。

1、用户名密码登录有个 UsernamePasswordAuthenticationFilter,我们搞一个SmsAuthenticationFilter,代码粘过来改一改。
2、用户名密码登录需要UsernamePasswordAuthenticationToken,我们搞一个SmsAuthenticationToken,代码粘过来改一改。
3、用户名密码登录需要DaoAuthenticationProvider,我们模仿它也 implenments AuthenticationProvider,叫做 SmsAuthenticationProvider。

这个图是网上找到,自己不想画了

我们自己搞了上面三个类以后,想要实现的效果如上图所示。当我们使用短信验证码登录的时候:

1、先经过 SmsAuthenticationFilter,构造一个没有鉴权的 SmsAuthenticationToken,然后交给 AuthenticationManager处理。

2、AuthenticationManager 通过 for-each 挑选出一个合适的 provider 进行处理,当然我们希望这个 provider 要是 SmsAuthenticationProvider。

3、验证通过后,重新构造一个有鉴权的SmsAuthenticationToken,并返回给SmsAuthenticationFilter。
filter 根据上一步的验证结果,跳转到成功或者失败的处理逻辑。

二、代码实现

1、SmsAuthenticationToken

首先我们编写 SmsAuthenticationToken,这里直接参考 UsernamePasswordAuthenticationToken 源码,直接粘过来,改一改。

说明

principal 原本代表用户名,这里保留,只是代表了手机号码。
credentials 原本代码密码,短信登录用不到,直接删掉。
SmsCodeAuthenticationToken() 两个构造方法一个是构造没有鉴权的,一个是构造有鉴权的。
剩下的几个方法去除无用属性即可。

代码

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
* 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
* 在这里就代表登录的手机号码
*/
private final Object principal; /**
* 构建一个没有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
} /**
* 构建拥有鉴权的 SmsCodeAuthenticationToken
*/
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
// must use super, as we override
super.setAuthenticated(true);
} @Override
public Object getCredentials() {
return null;
} @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");
} super.setAuthenticated(false);
} @Override
public void eraseCredentials() {
super.eraseCredentials();
}
}

2、SmsAuthenticationFilter

然后编写 SmsAuthenticationFilter,参考 UsernamePasswordAuthenticationFilter 的源码,直接粘过来,改一改。

说明

原本的静态字段有 usernamepassword,都干掉,换成我们的手机号字段。

SmsCodeAuthenticationFilter() 中指定了这个 filter 的拦截 Url,我指定为 post 方式的 /sms/login

剩下来的方法把无效的删删改改就好了。

代码

public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
/**
* form表单中手机号码的字段name
*/
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = "mobile";
/**
* 是否仅 POST 方式
*/
private boolean postOnly = true; public SmsCodeAuthenticationFilter() {
//短信验证码的地址为/sms/login 请求也是post
super(new AntPathRequestMatcher("/sms/login", "POST"));
} @Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
} String mobile = obtainMobile(request);
if (mobile == null) {
mobile = "";
} mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property
setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest);
} protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(mobileParameter);
} protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
} public String getMobileParameter() {
return mobileParameter;
} public void setMobileParameter(String mobileParameter) {
Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
this.mobileParameter = mobileParameter;
} public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
}

3、SmsAuthenticationProvider

这个方法比较重要,这个方法首先能够在使用短信验证码登陆时候被 AuthenticationManager 挑中,其次要在这个类中处理验证逻辑。

说明

实现 AuthenticationProvider 接口,实现 authenticate() 和 supports() 方法。

代码

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    /**
* 处理session工具类
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS"; @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String mobile = (String) authenticationToken.getPrincipal(); checkSmsCode(mobile); UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
// 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult;
} private void checkSmsCode(String mobile) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 从session中获取图片验证码
SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX);
String inputCode = request.getParameter("smsCode");
if(smsCodeInSession == null) {
throw new BadCredentialsException("未检测到申请验证码");
} String mobileSsion = smsCodeInSession.getMobile();
if(!Objects.equals(mobile,mobileSsion)) {
throw new BadCredentialsException("手机号码不正确");
} String codeSsion = smsCodeInSession.getCode();
if(!Objects.equals(codeSsion,inputCode)) {
throw new BadCredentialsException("验证码错误");
}
} @Override
public boolean supports(Class<?> authentication) {
// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
} public UserDetailsService getUserDetailsService() {
return userDetailsService;
} public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}

4、SmsCodeAuthenticationSecurityConfig

既然自定义了拦截器,可以需要在配置里做改动。

代码

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private SmsUserService smsUserService;
@Autowired
private AuthenctiationSuccessHandler authenctiationSuccessHandler;
@Autowired
private AuthenctiationFailHandler authenctiationFailHandler; @Override
public void configure(HttpSecurity http) {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
//需要将通过用户名查询用户信息的接口换成通过手机号码实现
smsCodeAuthenticationProvider.setUserDetailsService(smsUserService); http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}

5、SmsUserService

因为用户名,密码登陆最终是通过用户名查询用户信息,而手机验证码登陆是通过手机登陆,所以这里需要自己再实现一个SmsUserService

@Service
@Slf4j
public class SmsUserService implements UserDetailsService { @Autowired
private UserMapper userMapper; @Autowired
private RolesUserMapper rolesUserMapper; @Autowired
private RolesMapper rolesMapper; /**
* 手机号查询用户
*/
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
log.info("手机号查询用户,手机号码 = {}",mobile);
//TODO 这里我没有写通过手机号去查用户信息的sql,因为一开始我建user表的时候,没有建mobile字段,现在我也不想临时加上去
//TODO 所以这里暂且写死用用户名去查询用户信息(理解就好)
User user = userMapper.findOneByUsername("小小");
if (user == null) {
throw new UsernameNotFoundException("未查询到用户信息");
}
//获取用户关联角色信息 如果为空说明用户并未关联角色
List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId());
if (CollectionUtils.isEmpty(userList)) {
return user;
}
//获取角色ID集合
List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList());
List<Roles> rolesList = rolesMapper.findByIdIn(ridList);
//插入用户角色信息
user.setRoles(rolesList);
return user;
}
}

6、总结

到这里思路就很清晰了,我这里在总结下。

1、首先从获取验证的时候,就已经把当前验证码信息存到session,这个信息包含验证码和手机号码。

2、用户输入验证登陆,这里是直接写在SmsAuthenticationFilter中先校验验证码、手机号是否正确,再去查询用户信息。我们也可以拆开成用户名密码登陆那样一个
过滤器专门验证验证码和手机号是否正确,正确在走验证码登陆过滤器。 3、在SmsAuthenticationFilter流程中也有关键的一步,就是用户名密码登陆是自定义UserService实现UserDetailsService后,通过用户名查询用户名信息而这里是
通过手机号查询用户信息,所以还需要自定义SmsUserService实现UserDetailsService后。

三、测试

1、获取验证码

获取验证码的手机号是 15612345678 。因为这里没有接第三方的短信SDK,只是在后台输出。

向手机号为:15612345678的用户发送验证码:254792

2、登陆

1)验证码输入不正确

发现登陆失败,同样如果手机号码输入不对也是登陆失败

2)登陆成功

当手机号码 和 短信验证码都正确的情况下 ,登陆就成功了。

参考

1、Spring Security技术栈开发企业级认证与授权(JoJo)

2、SpringBoot 集成 Spring Security(8)——短信验证码登录

别人骂我胖,我会生气,因为我心里承认了我胖。别人说我矮,我就会觉得好笑,因为我心里知道我不可能矮。这就是我们为什么会对别人的攻击生气。
攻我盾者,乃我内心之矛(21)

SpringSceurity(5)---短信验证码登陆功能的更多相关文章

  1. SpringSceurity(4)---短信验证码功能实现

    SpringSceurity(4)---短信验证码功能实现 有关SpringSceurity系列之前有写文章 1.SpringSecurity(1)---认证+授权代码实现 2.SpringSecur ...

  2. JavaWeb-SpringSecurity使用短信验证码登陆

    相关博文 JavaWeb-SpringBoot_一个类实现腾讯云SDK发送短信 传送门 系列博文 项目已上传至guthub 传送门 JavaWeb-SpringSecurity初认识 传送门 Java ...

  3. java发送短信验证码的功能实现

    总结一下发送短信验证码的功能实现 (题外话:LZ是在腾讯云买的第三方(山东鼎信)短信服务平台的接口,1块钱20次的套餐来练手,哈哈,给他们打个广告,有需要的可以去购买哈,下面是购买链接短信服务平台购买 ...

  4. SpringBoot + Spring Security 学习笔记(五)实现短信验证码+登录功能

    在 Spring Security 中基于表单的认证模式,默认就是密码帐号登录认证,那么对于短信验证码+登录的方式,Spring Security 没有现成的接口可以使用,所以需要自己的封装一个类似的 ...

  5. Java调用WebService接口实现发送手机短信验证码功能,java 手机验证码,WebService接口调用

    近来由于项目需要,需要用到手机短信验证码的功能,其中最主要的是用到了第三方提供的短信平台接口WebService客户端接口,下面我把我在项目中用到的记录一下,以便给大家提供个思路,由于本人的文采有限, ...

  6. Android Studio精彩案例(五)《JSMS短信验证码功能实现》

    转载本专栏文章,请注明出处,尊重原创 .文章博客地址:道龙的博客 很多应用刚打开的时候,让我们输入手机号,通过短信验证码来登录该应用.那么,这个场景是怎么实现的呢?其实是很多开放平台提供了短信验证功能 ...

  7. php实现的IMEI限制的短信验证码发送类

    php实现的IMEI限制的短信验证码发送类 <?php class Api_Sms{ const EXPIRE_SEC = 1800; // 过期时间间隔 const RESEND_SEC = ...

  8. iOS点击获取短信验证码按钮

    概述 iOS点击获取短信验证码按钮, 由于 Demo整体测试运行效果 , 整个修改密码界面都已展现, 并附送正则表达式及修改密码逻辑. 详细 代码下载:http://www.demodashi.com ...

  9. Android开发之短信验证码示例

    在说Android中的短信验证码这个知识点前,我们首先来了解下聚合数据 聚合数据介绍 聚合数据是一家国内最大的基础数据API提供商,专业从事互联网数据服务.免费提供从天气查询.空气质量.地图坐标到金融 ...

随机推荐

  1. 一次FGC导致CPU飙高的排查过程

    今天测试团队反馈说,服务A的响应很慢,我在想,测试环境也会慢?于是我自己用postman请求了一下接口,真的很慢,竟然要2s左右,正常就50ms左右的. 于是去测试服务器看了一下,发现服务器负载很高, ...

  2. Chisel3 - util - Valid

    https://mp.weixin.qq.com/s/L5eAwv--WzZdr-CfW2-XNA   Chisel提供的Valid接口.如果valid为置1,则表明输出的bits有效:反之,则输出无 ...

  3. ASP.NET生成验证码

    首先,添加一个一般处理程序 注释很详细了,有不懂的欢迎评论 using System; using System.Collections.Generic; using System.Drawing; ...

  4. Java实现 蓝桥杯 算法训练 寻找数组中最大值

    算法训练 寻找数组中最大值 时间限制:1.0s 内存限制:512.0MB 提交此题 问题描述 对于给定整数数组a[],寻找其中最大值,并返回下标. 输入格式 整数数组a[],数组元素个数小于1等于10 ...

  5. Java实现 LeetCode 561 数组拆分 I(通过排序算法改写PS:难搞)

    561. 数组拆分 I 给定长度为 2n 的数组, 你的任务是将这些数分成 n 对, 例如 (a1, b1), (a2, b2), -, (an, bn) ,使得从1 到 n 的 min(ai, bi ...

  6. Java实现 蓝桥杯 历届试题 核桃的数量

    历届试题 核桃的数量 时间限制:1.0s 内存限制:256.0MB 问题描述 小张是软件项目经理,他带领3个开发组.工期紧,今天都在加班呢.为鼓舞士气,小张打算给每个组发一袋核桃(据传言能补脑).他的 ...

  7. Java实现 蓝桥杯VIP 算法提高 欧拉函数

    算法提高 欧拉函数 时间限制:1.0s 内存限制:512.0MB 说明 2016.4.5 已更新试题,请重新提交自己的程序. 问题描述 给定一个大于1,不超过2000000的正整数n,输出欧拉函数,p ...

  8. Arrays.binarySearch和Collections.binarySearch的详细用法

    概述 binarysearch为在指定数组中查找指定值得索引值,该值在范围内找得到则返回该值的索引值,找不到则返回该值的插入位置,如果该值大于指定范围最大值则返回-(maxlength+1),而: i ...

  9. Java实现蓝桥杯有歧义的号码

    描述 小Hi参加了一场大型马拉松运动会,他突然发现面前有一位参赛者背后的号码竟然和自己一样,也是666.仔细一看,原来那位参赛者把自己号码帖反(旋转180度)了,结果号码999看上去变成了号码666. ...

  10. java计算时间从什么时候开始 为什么从1970年开始 java的时间为什么是一大串数字

    Date date = new Date(0); System.out.println(date); 打印出来的结果: Thu Jan 01 08:00:00 CST 1970 也是1970 年 1 ...