Spring Security 实现短信验证码登录功能
作者:木兮同学 发布时间:2022-11-02 19:39:30
之前文章都是基于用户名密码登录,第六章图形验证码登录其实还是用户名密码登录,只不过多了一层图形验证码校验而已;Spring Security默认提供的认证流程就是用户名密码登录,整个流程都已经固定了,虽然提供了一些接口扩展,但是有些时候我们就需要有自己特殊的身份认证逻辑,比如用短信验证码登录,它和用户名密码登录的逻辑是不一样的,这时候就需要重新写一套身份认证逻辑。
开发短信验证码接口
获取验证码
短信验证码的发送获取逻辑和图片验证码类似,这里直接贴出代码。
@GetMapping("/code/sms")
public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 创建验证码
ValidateCode smsCode = createCodeSmsCode(request);
// 将验证码放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, smsCode);
String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
// 发送验证码
smsCodeSender.send(mobile, smsCode.getCode());
}
前端代码
<tr>
<td>手机号:</td>
<td><input type="text" name="mobile" value="13012345678"></td>
</tr>
<tr>
<td>短信验证码:</td>
<td>
<input type="text" name="smsCode">
<a href="/code/sms?mobile=13012345678" rel="external nofollow" >发送验证码</a>
</td>
</tr>
短信验证码流程原理
短信验证码登录和用户名密码登录对比
步骤流程
首先点击登录应该会被
SmsAuthenticationFilter
过滤器处理,这个过滤器拿到请求以后会在登录请求中拿到手机号,然后封装成自定义的一个SmsAuthenticationToken(未认证)。这个Token也会传给AuthenticationManager,因为
AuthenticationManager
整个系统只有一个,它会检索系统中所有的AuthenticationProvider,这时候我们要提供自己的SmsAuthenticationProvider
,用它来校验自己写的SmsAuthenticationToken的手机号信息。在校验的过程中同样会调用
UserDetailsService
,把手机号传给它让它去读用户信息,去判断是否能登录,登录成功的话再把SmsAuthenticationToken标记为已认证。到这里为止就是短信验证码的认证流程,上面的流程并没有提到校验验证码信息,其实它的验证流程和图形验证码验证流程也是类似,同样是
在SmsAuthenticationFilter过滤器之前加一个过滤器来验证短信验证码
。
代码实现
SmsCodeAuthenticationToken
作用:封装认证Token
实现:可以继承AbstractAuthenticationToken抽象类,该类实现了Authentication接口
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
/**
* 进入SmsAuthenticationFilter时,构建一个未认证的Token
*
* @param mobile
*/
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
/**
* 认证成功以后构建为已认证的Token
*
* @param principal
* @param authorities
*/
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) 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();
}
}
SmsCodeAuthenticationFilter
作用:处理短信登录的请求,构建Token,把请求信息设置到Token中。
实现:该类可以模仿UsernamePasswordAuthenticationFilter类,继承AbstractAuthenticationProcessingFilter抽象类
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParameter = "mobile";
private boolean postOnly = true;
/**
* 表示要处理的请求路径
*/
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/authentication/mobile", "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);
// 把请求信息设到Token中
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 void setMobileParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.mobileParameter = usernameParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileParameter() {
return mobileParameter;
}
}
SmsAuthenticationProvider
作用:提供认证Token的校验逻辑,配置为能够支持SmsCodeAuthenticationToken的校验
实现:实现AuthenticationProvider接口,实现其两个方法。
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
/**
* 进行身份认证的逻辑
*
* @param authentication
* @return
* @throws AuthenticationException
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
if (user == null) {
throw new InternalAuthenticationServiceException("无法获取用户信息");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
authenticationResult.setDetails(authenticationToken.getDetails());
return authenticationResult;
}
/**
* 表示支持校验的Token,这里是SmsCodeAuthenticationToken
*
* @param authentication
* @return
*/
@Override
public boolean supports(Class<?> authentication) {
return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
}
ValidateCodeFilter
作用:校验短信验证码
实现:和图形验证码类似,继承OncePerRequestFilter接口防止多次调用,主要就是验证码验证逻辑,验证通过则继续下一个过滤器。
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
/**
* 验证码校验失败处理器
*/
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
/**
* 系统配置信息
*/
@Autowired
private SecurityProperties securityProperties;
/**
* 系统中的校验码处理器
*/
@Autowired
private ValidateCodeProcessorHolder validateCodeProcessorHolder;
/**
* 存放所有需要校验验证码的url
*/
private Map<String, ValidateCodeType> urlMap = new HashMap<>();
/**
* 验证请求url与配置的url是否匹配的工具类
*/
private AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 初始化要拦截的url配置信息
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS);
}
/**
* 讲系统中配置的需要校验验证码的URL根据校验的类型放入map
*
* @param urlString
* @param type
*/
protected void addUrlToMap(String urlString, ValidateCodeType type) {
if (StringUtils.isNotBlank(urlString)) {
String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
for (String url : urls) {
urlMap.put(url, type);
}
}
}
/**
* 验证短信验证码
*
* @param request
* @param response
* @param chain
* @throws ServletException
* @throws IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
ValidateCodeType type = getValidateCodeType(request);
if (type != null) {
logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
try {
// 进行验证码的校验
validateCodeProcessorHolder.findValidateCodeProcessor(type)
.validate(new ServletWebRequest(request, response));
logger.info("验证码校验通过");
} catch (ValidateCodeException exception) {
// 如果校验抛出异常,则交给我们之前文章定义的异常处理器进行处理
authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
return;
}
}
// 继续调用后边的过滤器
chain.doFilter(request, response);
}
/**
* 获取校验码的类型,如果当前请求不需要校验,则返回null
*
* @param request
* @return
*/
private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
ValidateCodeType result = null;
if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) {
Set<String> urls = urlMap.keySet();
for (String url : urls) {
if (pathMatcher.match(url, request.getRequestURI())) {
result = urlMap.get(url);
}
}
}
return result;
}
}
添加配置
SmsCodeAuthenticationSecurityConfig
作用:配置SmsCodeAuthenticationFilter,后面需要把这些配置加到主配置类BrowserSecurityConfig
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PersistentTokenRepository persistentTokenRepository;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
// 设置AuthenticationManager
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
// 设置登录成功处理器
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler);
// 设置登录失败处理器
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler);
String key = UUID.randomUUID().toString();
smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository));
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
// 将自己写的Provider加到Provider集合里去
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
BrowserSecurityConfig
作用:主配置类;添加短信验证码配置类、添加SmsCodeAuthenticationSecurityConfig配置
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private SecurityProperties securityProperties;
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
@Autowired
private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 验证码校验过滤器
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
// 将验证码校验过滤器加到 UsernamePasswordAuthenticationFilter 过滤器之前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
// 当用户登录认证时默认跳转的页面
.loginPage("/authentication/require")
// 以下这行 UsernamePasswordAuthenticationFilter 会知道要处理表单的 /authentication/form 请求,而不是默认的 /login
.loginProcessingUrl("/authentication/form")
.successHandler(meicloudAuthenticationSuccessHandler)
.failureHandler(meicloudAuthenticationFailureHandler)
// 配置记住我功能
.and()
.rememberMe()
// 配置TokenRepository
.tokenRepository(persistentTokenRepository())
// 配置Token过期时间
.tokenValiditySeconds(3600)
// 最终拿到用户名之后,使用UserDetailsService去做登录
.userDetailsService(userDetailsService)
.and()
.authorizeRequests()
// 排除对 "/authentication/require" 和 "/meicloud-signIn.html" 的身份验证
.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll()
// 表示所有请求都需要身份验证
.anyRequest()
.authenticated()
.and()
.csrf().disable()// 暂时把跨站请求伪造的功能关闭掉
// 相当于把smsCodeAuthenticationSecurityConfig里的配置加到上面这些配置的后面
.apply(smsCodeAuthenticationSecurityConfig);
}
/**
* 记住我功能的Token存取器配置
*
* @return
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了
tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
}
来源:https://blog.csdn.net/qq_36221788/article/details/106169271
猜你喜欢
- 本文实例讲述了Android MediaPlayer基本使用方法。分享给大家供大家参考,具体如下:使用MediaPlayer播放音频或者视频
- 我们通过学习Java基础知识,让自己正式踏入学习Java语言的行列,这篇博客是用来让我们真正的了解并应用面向对象的思想来实现的。使用简单的J
- 本文研究的主要是Spring的事务机制的相关内容,具体如下。JAVA EE传统事务机制通常有两种事务策略:全局事务和局部事务。全局事务可以跨
- Spring Boot 的启动原理可以概括为以下几个步骤:加载 Spring Boot 应用程序的启动类根据启动类所在的包路径扫描相关的类根
- private string HttpPost(string Url, string postDataStr)
- 一、前言在java中,异常机制是非常有用的构成部分,异常信息对于查找错误来说是必不可少至关重要的信息,因此我们希望在发生错误的时候先看到捕捉
- 限时抢购倒计时实现效果图布局:<LinearLayout android:id="@+id/ll_
- 一、饿汉式单例类public class Singleton { privat
- eclipse导入Spring配置文件约束 Windows-Preference-XML-XMLCatalog点 Add 选Fil
- excel对于下拉框较多选项的,需要使用隐藏工作簿来解决,使用函数取值来做选项选项较少(一般少于5个):private static Dat
- 前言我们在日常开发中,经常会用到一个系统需要链接多个数据库来实现业务的需求,比如多个系统之间数据调用、两个数据之间同步等等。今天给大家分享使
- Jackson,我感觉是在Java与Json之间相互转换的最快速的框架,当然Google的Gson也很不错,但是参照网上有人的性能测试,看起
- rabbitMQ是一个在AMQP协议标准基础上完整的,可服用的企业消息系统。它遵循Mozilla Public License开源协议,采用
- 上期回顾上期我们主要介绍了排序的基本认识,以及四个排序,分别是直接插入排序,希尔排序,选择排序,堆排序,从这些排序中,了解了算法的实现,以及
- 一、了解Spring自动装配的方式采用传统的XML方式配置Bean组件的关键代码如下所示<bean id="userMapp
- 本文参考文档Add Flutter to existing apps。首先有一个可以运行的原生项目第一步:新建Flutter moduleT
- 本文实例讲述了C#获取CPU编号的方法。分享给大家供大家参考。具体如下:/// <summary>/// Gets the cp
- 一,Thread 的几个常见属性Thread 类是 JVM 用来管理线程的一个类,换句话说,每个线程都有一个唯一的 Thread 对象与之关
- 目录启动startServiceAMS的创建start()setSystemProcess后续goingCallBackstartHomeO
- 本文实例讲述了Android数据持久化之File机制。分享给大家供大家参考,具体如下:在使用Java SE平台开发C/S结构的软件中,Fil