Spring Security自定校验逻辑,支持图片验证码、短信验证码登录。支持JSON、表单登录。

Overview

Spring Security 实现短信验证码和图片验证码登录

参考文章:

1. 实现原理

实现原理

  • ImageAuthenticationFilter、SmsAuthenticationFilter 在这两个过滤器分别校验了图片验证码和短信验证码。
  • 由于Spring Security 默认支持的是表单登录,项目中采用的是 InputStream 的形式来读取 POST 请求中的 JSON,所以就直接在这里校验了(流只能获取一次,如果在这两个过滤前再加验证码过滤器,使用流获读JSON,就会报错)。
  • 图片验证码只需要自定义ImageAuthenticaionFilter即可,然后将 filter 加入到 Spring Security 过滤器链中。
  • 短信验证码需要自定义SmsAuthenticationFilter、SmsAuthenticationToken、SmsAuthenticationProvider,然后将 filter 和 provider 加入到 Spring Security 过滤器链中。

自定义的 AuthenticationFilter 继承自 AbstractAuthenticationProcessingFilter

  • filter 必须要设置 AuthenticationManager 属性;
  • requiresAuthenticationRequestMatcher 中保存 filter 拦截的请求路径;
  • 当校验通过时就会进入到 AuthenticationSuccessHandler 中;
  • 校验失败时就会进入到 AuthenticationFailureHandler。

更多信息请看 AbstractAuthenticationProcessingFilter 源码注释。

2. 验证码实现

验证码实现

  • ValidateCode: 抽象方法,保存【验证码】和 【过期时间】。
  • SmsCode: 短信验证码,直接继承 ValidateCode。
  • ImageCode: 图片验证码,继承 ValidateCode,并且添加了 BufferedImage 属性,用于生成图片。
  • ValidateCodeGenerator: 抽象方法,依赖了 ValidateCode,有验证码 生成逻辑和存储逻辑(项目中使用 Redis 存储)。

注意:图片验证码存储的 key 不是使用的用户名,而是【验证码类型 + "_" + 本次获取验证码的随机字符串(id)】。短信验证码使用【验证码类型 + "__" + 手机号】作为key来存储。

  • 前端在获取图片验证码之前要生成一个随机字符串(唯一性),代表本次获取图片验证码的唯一标识。然后在请求带着这个字符串(项目中用参数 id 来标识)去请求获取图片验证码。
  • 当校验图片验证码时,前端需要将这个 id 一起传过来,用于从 Redis 中获取存储的图片验证码。
/**
* @param id   前端传过来的随机字符串(用于生成key)
* @param type 枚举类型, 表示验证码类型
* @return 存在Redis中的key。如: sms_code_qwead、image_code_asdqwe
*/
public static String keyGenerator(String id, ValidateCodeType type) {
    return type.value + "_" + id;
}

3. 如何使用?

3.1. 开发环境

  • JDK 1.8
  • MySQL 5.7
  • Redis 6.0.4
  • Idea 2019.3.3 必须安装 lombok
  • spring boot 2.4.1

3.1. 下载配置

# 1、下载项目
git clone https://github.com/RingoTangs/spring-security-login-demo.git

# 2、修改配置文件。idea 需要安装 lombok
# 只需要修改 datasource 和 reids 的配置

# 3、图片验证码可以手动开启和关闭(默认开启)
validate.code.image.enabled=true

# 4、创建数据库表(查看项目中的user.sql文件)

# 5、启动项目访问 /JsonLogin.html 即可。

注意:

  • 存储在数据库中的密码需要是密文,项目中使用的是 BCryptPasswordEncoder
  • 准备数据库测试数据之前需要先将明文编码。
// 本项目 SecurityConfig 中注入了该组件
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

// 在测试类中调用 passwordEncoder.encode("123") 方法就可以生成密文了。

3.2. 接口描述

接口地址:

(1)/smsCode:获取短信验证码。

接口参数 描述
mobile 手机号。必填。
expireIn 验证码过期时间(单位: 秒)。选填。默认60s。
length 验证码的位数(长度)。选填。默认6位。

注意

项目中短信验证码并没有接入通信运营商,验证码信息会以 JSON 的形式返回,请注意查看 ~

(2)/imageCode:获取图片验证码。

例如:<img src="http://localhost:8081/imageCode?id=qwead" /> 即可获取验证码。

接口参数 描述
id 本次获取图片验证码的唯一标识。必填。
expireIn 同上。
length 同上。

(3)/doLogin:使用 username、password登录的地址。

详细信息请看 ImageAuthenticationFilter

接口参数 描述
username 用户名。必填。如不填写,按空字符串匹配。
password 密码。必填。如不填写,按空字符串匹配。
id 本次获取图片验证码的唯一标识。必填。如不填写,按空字符串匹配。
imageCode 图片验证码。必填。如不填写,按空字符串匹配。

(4)/login/mobile:使用手机号登录的地址。

详细信息请看 SmsAuthenticationFilter

接口参数 描述
mobile 手机号。必填。如不填写,按空字符串匹配。
smsCode 短信验证码。必填。如不填写,按空字符串匹配。

(5)/JsonLogin.html:H5页面,用于测试登录。也可以用 Postman 等工具测试。

(6)/logout:注销本次登录。

4. 更新记录

5月14日更新

5.14日更新:配置多个UserDetailsService

先看原来的UserDetailsService实现类:

@Service
public class UserService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    /**
     * 该方法在 {@link SmsAuthenticationProvider} 中被调用。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 1: 先按照 username 查询, 用户名查不到再按照 mobile(手机号) 查
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("mobile", username).or().eq("username", username);
        User user = userMapper.selectOne(wrapper);

        // 2: username和mobile都查不到直接抛出异常
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在, 请先注册~");
        }

        // 4: 查到用户信息
        // 设置角色 user.setAuthorities(List<>) ....

        return user;
    }
}

UserDetailsService 是在 AuthenticationProvider 中被调用的,目的就是去查看用户是否存在。显然我们这里发的SQL是 select * from t_user where username = ? or mobile = ?。众所周知,SQL中使用 OR 会影响MySQL的性能,所以第一个解决办法是再写一个UserDetailsService。

  • UserService 只用于查询用户名。
  • UserMobileService 只用户查询手机号。

以上两个 UserDetailsService 更改业务逻辑非常简单,这里就不再展示了~

第一步DaoAuthenticationProvider 调用 UserService 用于查询用户名是否存在。但是源码中并不知道我们定义了新的 UserDetailsService。

// DaoAuthenticationProvider 源码
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { 
    // 这里定义的是接口
    private UserDetailsService userDetailsService;
    
    // ....
    // 以下方法中直接调用 userDetailsService.loadUserByUsername(String username)
}

因此,需要重新设置 DaoAuthenticaionProvider。配置如下:

// 项目中Spring Security的主配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter { 
    
    /**
     * 重新设置 DaoAuthenticationProvider
     * 
     * DaoAuthenticationProvider 配置 UsernameNotFoundException 向上抛出。
     */
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userService);
        provider.setHideUserNotFoundExceptions(false);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    /**
     * Spring Security 原生的 AuthenticationProvider 需要在这里配置才会生效!
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加自定义的 AuthenticationProvider
        auth.authenticationProvider(daoAuthenticationProvider());
    }
}

第二步:我们自定义的 SmsAuthenticationProvider 也不知道 UserMobileService 的存在,也需要配置。

// 项目中短信验证码的配置类
@Configuration
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    // 注入 UserMobileService
    @Resource
    private UserMobileService userMobileService;

    // 配置 AuthenticationProvider 需要有 UserDetailsService。
    @Bean
    public SmsAuthenticationProvider smsAuthenticationProvider() {
        SmsAuthenticationProvider provider = new SmsAuthenticationProvider();
        
        // 注意:这里添加的是 userMobileService
        provider.setUserDetailsService(userMobileService);
        return provider;
    }
    
    // 其他代码可以在项目中看到
    // 将 SmsAuthenticationProvider 加入到 Spring Security 中 省略
    // .....
}

OK大功告成,定义多个 UserDeatilsService 搞定 ~

但是,能不能就定义一个 UserDetailsService 就解决问题呢?

答案是肯定的,那就在 UserSevice 这个实现类中使用正则表达式即可~

项目本次更新用的也是该方法!

@Slf4j
@Service
public class UserService implements UserDetailsService {

    @Resource
    private UserMapper userMapper;

    /**
     * 该方法在 {@link SmsAuthenticationProvider} 中被调用。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        QueryWrapper<User> wrapper = new QueryWrapper<>();

        if (ReUtil.isMatch(MOBILE_REGEX, username)) {
            // 参数 username 是手机号
            log.info("手机号登录...UserService");
            wrapper.eq("mobile", username);
        } else {
            // 参数 username 是用户账号
            log.info("用户名登录...UserService");
            wrapper.eq("username", username);
        }

        // 1: 手机号登录就去查手机号,用户名登录就去查用户名~ 只要能确定用户是否存在即可
        
        // 用户名 + 密码 登录模式 <==> username/mobile + password 模式
        // 即: 前端用户名的输出框, 既可以填 username 也可以填 mobile
        User user = userMapper.selectOne(wrapper);

        // 2: username和mobile都查不到直接抛出异常
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在, 请先注册~");
        }

        // 4: 查到用户信息
        // 设置角色 user.setAuthorities(List<>) ....

        return user;
    }
}

5月16日更新

5月16日更新:remember-me功能

Spring Security的记住我功能包含两方面:

  1. 登录校验成功后,token分别存储到数据库和浏览器的cookie中(RememberMeServices)。
  2. 再次登录,不用输入密码,需要进行校验(RememberMeAuthenticationFilter)。

第一步:如何登录?

// AbstractAuthenticationProcessingFilter 源码 
public abstract class AbstractAuthenticationProcessingFilter { 

    // 默认的RememberMeServices
    // 需要我们重新配置
    private RememberMeServices rememberMeServices = new NullRememberMeServices();


    // 用户名密码校验成功之后会调用这个方法
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {
        SecurityContextHolder.getContext().setAuthentication(authResult);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult));
        }
        
        // 登录时 remrember-me 的逻辑
        this.rememberMeServices.loginSuccess(request, response, authResult);
        
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }
        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }
}

由此可见,SmsAuthenticationFilter、ImageAuthenticationFilter 都需要设置 RememberMeServices。

详细配置请看 RememberMeConfig

数据库表请看 persistent_logins.sql

注意

  • 使用记住我登录功能,前端必须传remember-me参数。
  • 由于remember-me参数的获取是直接从 request 中获取,所以post请求中的JSON要转换成表单登录的形式。
// AbstractRememberMeServices#rememberMeRequested(HttpServletRequest, String) 源码
String paramValue = request.getParameter(parameter);
if (paramValue != null) {
    if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
        || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
        return true;
    }
}
// 前端可以这样传数据。
postRequest('http://localhost:8081/login/mobile?' +
            'spring-security-remember-me=' + this.isRemember2, {
    'mobile': this.mobile,
    'smsCode': this.smsCode,
})

第二步:关闭浏览器再次登录的校验。

// RememberMeAuthenticationFilter 源码
public class RememberMeAuthenticationFilter {
    
    private RememberMeServices rememberMeServices;
    
    public void doFilter() {
        // RememberMeAuthenticationFilter 中需要使用我们自己的 RememberMeServices
        Authentication rememberMeAuth 
            = this.rememberMeServices.autoLogin(request, response);
    }
}

配置如下

// SecurityConfig 中的配置
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // 开启 remember-me 功能
        .rememberMe()
        // 会在RememberMeAuthenticationFilter中加入rememberMeServices
        .rememberMeServices(rememberMeServices)       
		...
}

第三步:数据库的过期登录信息需要自动删除,配置定时任务即可。

详细请看 RememberMeTask

5. more ~

欢迎您对本项目提出宝贵的意见。如果本项目对您的学习有帮助,请收藏 ~

联系QQ:1466637477。

更多学习笔记

You might also like...

mall-swarm是一套微服务商城系统,采用了 Spring Cloud Hoxton & Alibaba、Spring Boot 2.3、Oauth2、MyBatis、Docker、Elasticsearch、Kubernetes等核心技术,同时提供了基于Vue的管理后台方便快速搭建系统。mall-swarm在电商业务的基础集成了注册中心、配置中心、监控中心、网关等系统功能。文档齐全,附带全套Spring Cloud教程。

mall-swarm是一套微服务商城系统,采用了 Spring Cloud Hoxton & Alibaba、Spring Boot 2.3、Oauth2、MyBatis、Docker、Elasticsearch、Kubernetes等核心技术,同时提供了基于Vue的管理后台方便快速搭建系统。mall-swarm在电商业务的基础集成了注册中心、配置中心、监控中心、网关等系统功能。文档齐全,附带全套Spring Cloud教程。

mall-swarm 友情提示 快速体验项目:在线访问地址。 全套学习教程:《mall学习教程》。 Spring Cloud全套教程:《SpringCloud学习教程》。 专属学习路线:学习不走弯路,整理了套非常不错的《mall专属学习路线》。 项目交流:想要加群交流项目的朋友,可以加入mall项目

Jan 3, 2023

芋道 mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

芋道 mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。

[toc] 友情提示:近期在升级和优化该项目,建议先 Star 本项目。主要在做几个事情: 1、微服务技术选型以 Spring Cloud Alibaba 为中心。 2、修改项目分层,并合并部分服务,简化整体服务的复杂性。 3、将管理后台从 React 重构到 Vue 框架。 交流群:传送门 前言

Jan 6, 2023

about learning Spring Boot via examples. Spring Boot 教程、技术栈示例代码,快速简单上手教程。

about learning Spring Boot via examples. Spring Boot 教程、技术栈示例代码,快速简单上手教程。

Spring Boot 学习示例 Spring Boot 使用的各种示例,以最简单、最实用为标准,此开源项目中的每个示例都以最小依赖,最简单为标准,帮助初学者快速掌握 Spring Boot 各组件的使用。 Spring Boot 中文索引 | Spring Cloud学习示例代码 | Spring

Jan 1, 2023

spring boot 实践学习案例,是 spring boot 初学者及核心技术巩固的最佳实践。另外写博客,用 OpenWrite。

spring boot 实践学习案例,是 spring boot 初学者及核心技术巩固的最佳实践。另外写博客,用 OpenWrite。

推荐工具: 微信公众号 Markdown 编辑器 - OpenWrite:Markdown 微信编辑器是一款专业强大的微信公众平台在线编辑排版工具,提供手机预览功能,让用户在微信图文 、文章、内容排版、文本编辑、素材编辑上更加方便。 - 更多介绍 博客群发平台 一、支持泥瓦匠 Spring Boot

Jan 5, 2023

Spring Boot基础教程,Spring Boot 2.x版本连载中!!!

Spring Boot基础教程,Spring Boot 2.x版本连载中!!!

Spring Boot基础教程 本项目内容为《Spring Boot基础教程》的程序样例。 专题目标:打造全网内容最全,比收费教程更好的Spring Boot免费教程! 加入社群:如果你正在学习Spring Boot,不妨加入我们的Spring技术交流群,一起成长! 如何支持: 关注我的公众号”程序

Jan 6, 2023

Not only Spring Boot but also important knowledge of Spring(不只是SpringBoot还有Spring重要知识点)

Not only Spring Boot but also important knowledge of Spring(不只是SpringBoot还有Spring重要知识点)

在线阅读 : https://snailclimb.gitee.io/springboot-guide (上面的地址访问速度缓慢的建议使用这个路径访问) 重要知识点 基础 Spring Boot 介绍 第一个 Hello World 第一个 RestFul Web 服务 Spring 如何优雅读取配

Jan 3, 2023

Spring-Boot-Plus is a easy-to-use, high-speed, high-efficient,feature-rich, open source spring boot scaffolding

Spring-Boot-Plus is a easy-to-use, high-speed, high-efficient,feature-rich, open source spring boot scaffolding

Everyone can develop projects independently, quickly and efficiently! What is spring-boot-plus? A easy-to-use, high-speed, high-efficient, feature-ric

Dec 31, 2022

开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap

开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 Spring、Spring Boot、MyBatis、MySQL/H2、Bootstrap

码问社区 在线演示地址 www.mawen.co 功能列表 开源论坛、问答系统,现有功能提问、回复、通知、最新、最热、消除零回复功能。功能持续更新中…… 技术栈 技术 链接 Spring Boot http://projects.spring.io/spring-boot/#quick-start

Dec 30, 2022
Owner
null
该仓库中主要是 Spring Boot 的入门学习教程以及一些常用的 Spring Boot 实战项目教程,包括 Spring Boot 使用的各种示例代码,同时也包括一些实战项目的项目源码和效果展示,实战项目包括基本的 web 开发以及目前大家普遍使用的线上博客项目/企业大型商城系统/前后端分离实践项目等,摆脱各种 hello world 入门案例的束缚,真正的掌握 Spring Boot 开发。

Spring Boot Projects 该仓库中主要是 Spring Boot 的入门学习教程以及一些常用的 Spring Boot 实战项目教程,包括 Spring Boot 使用的各种示例代码,同时也包括一些实战项目的项目源码和效果展示,实战项目包括基本的 web 开发以及目前大家普遍使用的前

十三 4.5k Dec 30, 2022
一个涵盖六个专栏:Spring Boot 2.X、Spring Cloud、Spring Cloud Alibaba、Dubbo、分布式消息队列、分布式事务的仓库。希望胖友小手一抖,右上角来个 Star,感恩 1024

友情提示:因为提供了 50000+ 行示例代码,所以艿艿默认注释了所有 Maven Module。 胖友可以根据自己的需要,修改 pom.xml 即可。 一个涵盖六个主流技术栈的正经仓库: 《Spring Boot 专栏》 《Spring Cloud Alibaba 专栏》 《Spring Clou

芋道源码 15.7k Dec 31, 2022
参考 DDD/Clean Architecture 设计理念,整合 Spring Boot/Spring Security/Mybatis Plus/Vavr 的 Spring Realworld 应用案例

Demo · 更多项目 · 参考资料 ms-spring-ddd-examples Unified Domain-driven Layered Architecture for MicroService Apps,试图探索一套切实可行的应用架构规范,可以复制、可以理解、可以落地、可以控制复杂性的指导

王下邀月熊 19 Sep 23, 2022
Spring Kurulumundan Başlayarak, Spring IOC ve Dependency Injection, Hibernate, Maven ve Spring Boot Konularına Giriş Yapıyoruz.

Spring Tutorial for Beginners File Directory Apache Tomcat Apache Tomcat - Eclipse Bağlantısı Spring Paketlerinin İndirilmesi ve Projeye Entegrasyonu

İbrahim Can Erdoğan 11 Apr 11, 2022
Spring Boot JdbcTemplate example with SQL Server: CRUD Rest API using Spring Data JDBC, Spring Web MVC

Spring Boot JdbcTemplate example with SQL Server: Build CRUD Rest API Build a Spring Boot CRUD Rest API example that uses Spring Data Jdbc to make CRU

null 7 Dec 20, 2022
Spring Boot & MongoDB Login and Registration example with JWT, Spring Security, Spring Data MongoDB

Spring Boot Login and Registration example with MongoDB Build a Spring Boot Auth with HttpOnly Cookie, JWT, Spring Security and Spring Data MongoDB. Y

null 15 Dec 30, 2022
Spring Boot Login and Registration example with MySQL, JWT, Rest Api - Spring Boot Spring Security Login example

Spring Boot Login example with Spring Security, MySQL and JWT Appropriate Flow for User Login and Registration with JWT Spring Boot Rest Api Architect

null 58 Jan 5, 2023
Demo microservice architecture with Spring ,Spring Cloud Gateway , Spring Cloud config server , Eureuka , keycloak and Docker.

spring-microservice Demo microservice architecture with Spring ,Spring Cloud Gateway , Spring Cloud config server , Eureuka , keycloak and Docker. Arc

null 4 Sep 13, 2022
Spring Boot JWT Authentication example with Spring Security & Spring Data JPA

Spring Boot JWT Authentication example with Spring Security & Spring Data JPA

null 1 Jan 26, 2022
Spring REST service built with Spring initializr and Spring Data.

Spring REST Service Generated with start.spring.io, using Spring Data. Documented using Spring REST Docs. Spring Initializr - Generate new Spring Rest

null 1 Jan 28, 2022