1、功能实现

Security默认提供的是用户名密码登录模式,然后我们参考用户名密码登录自定义实现短信登录模式
这样就多了一种登录模式,在登录的时候可以自行选择登录模式

2、security07 子工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>com.yzm</groupId><artifactId>security</artifactId><version>0.0.1-SNAPSHOT</version><relativePath>../pom.xml</relativePath> <!-- lookup parent from repository --></parent><artifactId>security07</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging><name>security07</name><description>Demo project for Spring Boot</description><dependencies><dependency><groupId>com.yzm</groupId><artifactId>common</artifactId><version>0.0.1-SNAPSHOT</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

application.yml

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.192.128:3306/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaiusername: rootpassword: 1234main:allow-bean-definition-overriding: truemybatis-plus:mapper-locations: classpath:/mapper/*Mapper.xmltype-aliases-package: com.yzm.security07.entityconfiguration:map-underscore-to-camel-case: truelog-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3、短信认证过滤器

用户名密码的认证过滤器是 UsernamePasswordAuthenticationFilter
那么我们就仿照它实现用于短信认证的过滤器

package com.yzm.security07.config;import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** 短信认证过滤器*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {// form表单中手机号码的字段namepublic static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";// 拦截/sms/loginprivate static final AntPathRequestMatcher DEFAULT_SMS_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");private String mobileParameter = "mobile";private boolean postOnly = true;public SmsAuthenticationFilter() {super(DEFAULT_SMS_PATH_REQUEST_MATCHER);}public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {super(DEFAULT_SMS_PATH_REQUEST_MATCHER, authenticationManager);}@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {if (this.postOnly && !request.getMethod().equals("POST")) {throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());}String mobile = this.obtainMobile(request);mobile = mobile != null ? mobile.trim() : "";SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);this.setDetails(request, authRequest);return this.getAuthenticationManager().authenticate(authRequest);}@Nullableprotected String obtainMobile(HttpServletRequest request) {return request.getParameter(mobileParameter);}protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));}public void setMobileParameter(String mobileParameter) {Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");this.mobileParameter = mobileParameter;}public String getMobileParameter() {return this.mobileParameter;}public void setPostOnly(boolean postOnly) {this.postOnly = postOnly;}
}

对应的UsernamePasswordAuthenticationToken 改造成 SmsAuthenticationToken

package com.yzm.security07.config;import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;import java.util.Collection;/*** 短信登录 AuthenticationToken*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {private static final long serialVersionUID = 554008100412847685L;/*** 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,* 在这里就代表登录的手机号码*/private final Object principal;/*** 构建一个没有鉴权的 SmsCodeAuthenticationToken*/public SmsAuthenticationToken(Object principal) {super(null);this.principal = principal;setAuthenticated(false);}/*** 构建拥有鉴权的 SmsCodeAuthenticationToken*/public SmsAuthenticationToken(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) throws IllegalArgumentException {if (isAuthenticated) {throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");}super.setAuthenticated(false);}@Overridepublic void eraseCredentials() {super.eraseCredentials();}}

4、短信认证 Provider

自定义短信认证 Provider
把AbstractUserDetailsAuthenticationProvider跟其子类,DaoAuthenticationProvider的作用结合一起写了

package com.yzm.security07.config;import com.yzm.common.utils.HttpUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;import javax.servlet.http.HttpServletRequest;
import java.util.Map;/*** 短信认证 Provider*/
public class SmsAuthenticationProvider implements AuthenticationProvider {private UserDetailsService userDetailsService;public SmsAuthenticationProvider(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}@Overridepublic boolean supports(Class<?> authentication) {// 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口return SmsAuthenticationToken.class.isAssignableFrom(authentication);}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;String mobile = (String) authenticationToken.getPrincipal();checkSmsCode(mobile);// 相当于DaoAuthenticationProvider的retrieveUser()UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);// 相当于AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication()SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());authenticationResult.setDetails(authenticationToken.getDetails());return authenticationResult;}// 检查验证码private void checkSmsCode(String mobile) {HttpServletRequest request = HttpUtils.getHttpServletRequest();String inputCode = request.getParameter("smsCode");Map<String, Object> smsMap = (Map<String, Object>) request.getSession().getAttribute("smsCode");if (smsMap == null) {throw new BadCredentialsException("未检测到申请验证码");}String applyMobile = (String) smsMap.get("mobile");int code = (int) smsMap.get("code");if (!applyMobile.equals(mobile)) {throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");}if (code != Integer.parseInt(inputCode)) {throw new BadCredentialsException("验证码错误");}}public void setUserDetailsService(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}protected UserDetailsService getUserDetailsService() {return this.userDetailsService;}
}

5、SecurityConfig 配置类

将自定义的SmsAuthenticationFilter添加到Security框架的过滤链中

package com.yzm.security07.config;import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {private final UserDetailsService userDetailsService;public SecurityConfig(@Qualifier("secUserDetailsServiceImpl") UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}/*** 密码编码器* passwordEncoder.encode是用来加密的,passwordEncoder.matches是用来解密的*/@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}/*** 配置用户* 指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 从数据库获取用户auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}/*** 自定义SmsAuthenticationFilter需要注入AuthenticationManager,否则报空指针*/@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}//配置资源权限规则@Overrideprotected void configure(HttpSecurity http) throws Exception {http// 关闭CSRF跨域.csrf().disable()// addFilterAfter 在过滤链中的指定Filter(第二个参数)之后,添加Filter.addFilterAfter(new SmsAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class).authenticationProvider(new SmsAuthenticationProvider(userDetailsService))// 登录.formLogin().loginPage("/auth/login") //指定登录页的路径,默认/login.loginProcessingUrl("/login") //指定自定义form表单请求的路径(必须跟login.html中的form action=“url”一致).defaultSuccessUrl("/home", true) // 登录成功后的跳转url地址.failureUrl("/auth/login?error") // 登录失败后的跳转url地址.permitAll().and().exceptionHandling().accessDeniedPage("/401") // 拒接访问跳转页面.and()// 退出登录.logout().permitAll().and()// 访问路径URL的授权策略,如注册、登录免登录认证等.authorizeRequests().antMatchers("/", "/home", "/register", "/auth/login").permitAll() //指定url放行.antMatchers("/sms/code").permitAll() //获取短信验证码.anyRequest().authenticated() //其他任何请求都需要身份认证;}
}

6、验证码接口

此次演示只是模拟手机短信登录,手机号既是用户名,验证码是随机数并存储到session中的
在HomeController新增接口

    @GetMapping("/sms/code")public String sms(String mobile, HttpSession session) {int code = (int) Math.ceil(Math.random() * 9000 + 1000);Map<String, Object> map = new HashMap<>(16);map.put("mobile", mobile);map.put("code", code);session.setAttribute("smsCode", map);log.info("{}:为 {} 设置短信验证码:{}", session.getId(), mobile, code);return "redirect:/auth/login";}
.antMatchers("/sms/code").permitAll() //获取短信验证码

7、登录页面

提供两种登录模式

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>登录页</title>
</head>
<body>
<h1 th:if="${param.error}">Invalid username or password.</h1><h2>用户名密码登录</h2>
<form action="/login" method="post"><p><label for="username">Username</label><input type="text" id="username" name="username" placeholder="Username"></p><p><label for="password">Password</label><input type="password" id="password" name="password" placeholder="Password"></p><p><label><input type="checkbox" name="remember-me"></label> Remember me on this computer.</p><button type="submit">Sign in</button>
</form><h2>短信登录</h2>
<form method="post" action="/sms/login"><div>手机号:<label for="mobile"></label><input type="text" id="mobile" name="mobile" value="yzm"></div><div>验证码:<label><input type="text" name="smsCode"></label><a href="javascript:;" onclick="sendSms()">获取验证码</a></div><div><button type="submit">立即登陆</button></div>
</form><script>function sendSms() {window.location.href = '/sms/code?mobile=' + document.getElementById("mobile").value;}
</script>
</body>
</html>

8、测试

启动项目,登录页

用户名密码登录yzm,没问题
退出/logout 换短信登录,先获取验证码

输入验证码进行登录,也是没问题的

9、短信认证流程

1 首先入口还是 AbstractAuthenticationProcessingFilter#doFilter

// addFilterAfter 在过滤链中的指定Filter(第二个参数)之后,添加Filter
.addFilterAfter(new SmsAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)new AntPathRequestMatcher("/sms/login", "POST");
new AntPathRequestMatcher("/login", "POST");

这里我们是把短信认证放在用户名密码认证之后,
SmsAuthenticationFilter 拦截路径是/sms/login,而UsernamePasswordAuthenticationFilter是/login
短信登录发送请求/sms/login,
不是UsernamePasswordAuthenticationFilter需要拦截的,所以放行
是被我们自定义的SmsAuthenticationFilter拦截了,所以走了SmsAuthenticationFilter过滤器

2 AbstractAuthenticationProcessingFilter调用SmsAuthenticationFilter#attemptAuthentication()方法
3 SmsAuthenticationFilter构造未认证的SmsAuthenticationToken并提交给认证管理器AuthenticationManager
4 由认证管理器的子类ProviderManager遍历查找有没有支持SmsAuthenticationToken的AuthenticationProvider,发现我们自定义的SmsAuthenticationProvider支持
5 SmsAuthenticationProvider获取用户信息,并构造完全认证的Authentication对象返回
6 AbstractAuthenticationProcessingFilter接收的返回的完全认证Authentication对象之后,进行其他处理

相关链接

首页
上一篇:授权流程篇
下一篇:整合JWT篇

Security之短信登录篇相关推荐

  1. Spring Security实现短信登录

    文章目录 一.理论说明 1.1 用户名密码登录逻辑 1.2 短信验证码登录逻辑 二.代码实战 2.1 SmsAuthenticationToken 2.2 SmsAuthenticationFilte ...

  2. Spring Security之短信登录

    实现短信验证码登录 前面实现了 用户名+密码 的登录方式,现在实现一下短信验证码登录. 开发短信验证码接口 短信验证码和图形验证码类似,用户从手机短信得到验证码和从图片得到验证码类似. 校验短信验证码 ...

  3. Spring Security系列(22)- Security实现手机短信登录功能

    准备 需求 采用手机号+短信验证码登录方式是很常见的一种需求. 那我们如何在Spring Security实现这种功能呢? 表单登录流程 首先再回顾一下用户名密码表单登录流程. 登录请求进入过滤器 调 ...

  4. 《Redis实战篇》一、短信登录

    1.1.导入黑马点评项目 1.1.1 .导入SQL 1.1.2.有关当前模型 手机或者app端发起请求,请求我们的nginx服务器,nginx基于七层模型走的事HTTP协议,可以实现基于Lua直接绕开 ...

  5. 技术基础篇(二)之用户短信登录

    需求分析 1.登录效果图 2.登录需求 登录方式:手机号码+手机验证码 3.从登录model中获取手机号和验证码,首先判断手机号或验证码是否为空,其次判断验证码与输入验证码是否一致, 4.如果没有注册 ...

  6. 黑马点评项目-短信登录功能

    一.导入黑马点评项目 1.代码下载 视频资源链接:P25 实战篇-02.短信登录-导入黑马点评项目 代码可以直接去黑马微信公众号上搜索,或者从下面的网盘链接中下载:链接: https://pan.ba ...

  7. 不就是个短信登录API嘛,有这么复杂吗?(转载)

    重要声明: 本篇文章转载自公众号ThoughtWorks洞见(ID:TW-Insights),个人觉得文章分析问题的思路还行,如有侵权,请联系删除,谢谢. 上联:这个需求很简单,下联:怎么实现我不管, ...

  8. 不就是个短信登录API嘛,有这么复杂吗?

    引子 上联:这个需求很简单 下联:怎么实现我不管 横批:今晚上线 Part 1:暴力破解 早上开完站会,小李领了张新卡,要对登录功能做升级改造,在原来只支持用户名密码登录模式的基础上,新增手机号和短信 ...

  9. 青龙2.11.3版本对接傻妞+go-cqhttp+短信登录(Maiark)(兔子)+本地服务器直连GitHub

    ​ 目录 ​青龙面板常用指令 装面板前的准备 安装青龙面板 傻妞机器人安装教程: 芝士配置和命令 对接nodebot机器人 2.安装pm2 4.安装go -cqhttp 服务器下载go-cqhttp ...

最新文章

  1. Linux系统状态检测及进程控制--2
  2. cursor.execute(sql) 执行结果集是有记录的 但是num=cursor.rownumber 返回值为0
  3. spring p2p项目html,springboot2.x项目实战视频教程p2p金融中等项目
  4. 棋牌搭建,APP新手教程
  5. 最佳牛栏(前缀和+二分)
  6. 苹果域对应关系 Manifest.db文件解析
  7. 如何修改网易邮箱大师电脑端的消息提示音?超简单
  8. 基于RFM模型的用户价值分析——PythonTableau
  9. 如何用一张影像动图来对比成都市2019年和2020年油菜花分布状态?
  10. 苹果6s解除耳机模式_苹果耳机戴着耳朵痛
  11. 将整数字符串转成整数值
  12. dos命令行choice命令使用详解
  13. 家装灯线走线图_家装电路布线施工图文并茂详细解说
  14. php文件aes128解密工具,aes加密解密
  15. Linux TTY驱动--Uart_driver底层
  16. matlab中连续信号的卷积,连续时间信号卷积运算的MATLAB实现
  17. iOS自定义相机界面
  18. Docker镜像与仓库(四)
  19. PLC编程与应用培训课程_PLC系统认知
  20. springboot邮件发送以及thyemleaf生成邮件模板

热门文章

  1. 双目相机图像校正(五)
  2. Node 中的 Buffer 对象
  3. 转换文件大小(单位转换)
  4. 使从铃声管理(RingtoneManage)获取到的Ringtone重复播放
  5. AMiner必读论文推荐#IJCV近年引用最高论文#
  6. Python爬虫学习进阶
  7. HTTPS免费证书的申请与配置
  8. 按八个小时算,一天骑行多少公里合适?
  9. 使用IDEA创建Docker镜像,Docker容器,并发布项目
  10. 多进程android webview,Android 9.0 WebView多进程闪退问题