SpringSecurity 学习笔记分享 记录历程开篇

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
函数计算FC,每月15万CU 3个月
简介: SpringSecurity 学习笔记分享 记录历程开篇

基础翻译篇

官方文档地址

GitHub demo 代码

简介

  1. Spring Security 是一个提供身份验证,授权,保护以及防止常见攻击的框架,由于同时支持响应式和命令式,是spring框架的安全标准。
  2. 前提条件
    jdk1.8或者更高的版本
  3. Spring Security是一个独立的包含所有的独立容器,在java运行环境中不需添加任何特殊的配置文件,尤其是我们不需要配置特殊的Java授权和认证JAAS策略文件,也不需要将Spring Security的信息放入公共的类路径
    同样的,如果你使用的是EJB或者Servlet容器,不需要在任意的地方做任何特殊的配置,也不需要在服务类加载容器中包含Spring Security,所有必须的文件都已经包含在了应用程序中
    这种设计提供了最大的部署时间灵活性,可以拷贝我们的(jar,war,ear)从一个系统到另一个系统并可以轻松使用
  4. Spring Security源代码
  5. 简单demo启动
    添加pom
<dependencies>
 <!-- ... other dependency elements ... -->
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
</dependencies>

由于springboot提供 了Maven Bom来管理版本,因此不需制定版本,如果我们需要覆盖Spring Security版本,可以通过提供Maven属性来配置

<properties>
 <!-- ... -->
 <spring-security.version>5.2.2.BUILD-SNAPSHOT</spring-security.version>
</properties>

由于Spring Security在主版本进行重大更改,因此可以使用较新版本的Spring Security与SpringBoot一起使用是比较安全的,但是有时还是需要指定Spring Framework的版本

<properties>
 <spring.version>5.2.1.RELEASE</spring.version>
</properties>
  1. 如果在没有使用SpringBoot的情况下,我们使用Spring Security的BOM来确定SpringSecurity,来确保项目中使用一致的SpringSecurity版本
<dependencyManagement>
 <dependencies>
     <!-- ... other dependency elements ... -->
     <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-bom</artifactId>
         <version>5.2.2.BUILD-SNAPSHOT</version>
         <type>pom</type>
         <scope>import</scope>
     </dependency>
 </dependencies>
</dependencyManagement>

7.Spring Security最小依赖必须的如下

<dependencies>
 <!-- ... other dependency elements ... -->
 <dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.security</groupId>
     <artifactId>spring-security-config</artifactId>
 </dependency>
</dependencies>
  1. 所有的RELEASE版本都已经上传到Maven中心仓库,无需在pom中指定其他maven库,如果使用SNAPSHOT版本则需要定义Spring Snapshot存储库
<repositories>
 <!-- ... possibly other repository elements ... -->
 <repository>
     <id>spring-snapshot</id>
     <name>Spring Snapshot Repository</name>
     <url>https://repo.spring.io/snapshot</url>
 </repository>
</repositories>

9.如果使用里程碑版本或者候选的版本,则需要指定Spring Milestone存储库

<repositories>
 <!-- ... possibly other repository elements ... -->
 <repository>
     <id>spring-milestone</id>
     <name>Spring Milestone Repository</name>
     <url>https://repo.spring.io/milestone</url>
 </repository>
</repositories>

认证

Spring Security 支持内置的用户验证,基于用户名密码,用户输入用户名密码访问特定的资源,此时会触发身份验证,进行授权

密码认证

常用密码编码器

  • DelegatingPasswordEncoder 委派授权密码编码器
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
  • BCryptPasswordEncoder 使用bcrypt算法对密码进行hash
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
  • Argon2PasswordEncoder 使用argon2算法对密码进行hash
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
  • Pbkdf2PasswordEncoder 使用bkdf2算法 对密码进行hash
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
  • SCryptPasswordEncoder 使用scrypt 算法进行hash
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();

DelegatingPasswordEncoder 详解

A password encoder that delegates to another PasswordEncoder based upon a prefixed identifier.

本质上还是使用的另一个密码编码器,只是加了一个前缀来区分使用的是什么类型的密码编码器

此处扩展一下,在 Spring Security 5.0 之前,默认的 PasswordEncoder 是NoOpPasswordEncoder,纯文本密码的对比.现在已经被标记为不安全,使用 DelegatingPasswordEncoder 代替

DelegatingPasswordEncoder 编码格式

{id}encodedPassword

id 就是编码类型标识符,encodedPassword 是原密码经过 PasswordEncoder 编码之后的密码,id 必须在密码的开始,以{id}的形式存在.如果 id 找不到,id 就是 null,以下是使用不同的 编码器对原密码 password 编码之后的结果

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password 
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

解读如下

第一个密码的PasswordEncoderID为bcrypt,编码密码为$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配时将委托给BCryptPasswordEncoder
第二个密码的PasswordEncoderID为noop,编码密码为password。匹配时将委托给NoOpPasswordEncoder
第三个密码的PasswordEncoderID为pbkdf2,编码密码为5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配时将委托给Pbkdf2PasswordEncoder
第四个密码的PasswordEncoderID为scrypt且编码密码为,$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=
匹配时将委派给SCryptPasswordEncoder
最后密码的PasswordEncoderID为sha256,编码的密码为97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配时将委托给StandardPasswordEncoder
为什么推荐使用 DelegatingPasswordEncoder?
  • 许多应用程序使用旧的密码编码无法轻松迁移
  • 密码存储的最佳做法将再次更改。
  • 作为一个框架,Spring Security不能经常进行重大更改
可以解决什么问题?
  • 使用当前密码推荐的存储建议编码
  • 兼容旧密码和新密码
  • 允许将来升级编码
自定义创建实例
String idForEncode = "bcrypt";
  Map<String,PasswordEncoder> encoders = new HashMap<>();
  encoders.put(idForEncode, new BCryptPasswordEncoder());
  encoders.put("noop", NoOpPasswordEncoder.getInstance());
  encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
  encoders.put("scrypt", new SCryptPasswordEncoder());
  encoders.put("sha256", new StandardPasswordEncoder());
 
  PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder(idForEncode, encoders);

实战篇

SpringBoot 开启 Spring Security

文章地址

SpringBoot + Spring Security 自定义用户认证 专业篇

项目使用上一篇文章创建 demo,demo 实现自定义用户登录,记住我功能,图片验证码.

本文项目地址

通过本文你可以学到什么?

  • SpringBoot 如何 集成 Spring Security?
  • SpringSecurity 自定义用户认证如何实现? 关键词: 自定义用户认证
  • SpringSecurity 怎么实现记住我功能? 关键词: 记住我
  • SpringSecurity 怎么实现图片验证码登录拦截? 关键词: 图片验证码
    对于以上问题,本篇文章将为你揭开神秘面纱.问题后面关键词为相应问题的代码实现.文章没有单独 拆分出来,代码标记处理.可根据关键词在本文中分解对应关键代码片段

自定义用户登录过程

  • 自定义用户认证 创建自定义用户类 MyUser
@Data
public class MyUser implements Serializable {
    private static final long serialVersionUID = 3497935890426858541L;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 密码
     */
    private String password;
    /**
     * 账号是否过期
     */
    private boolean accountNonExpired = true;
    /**
     * 账号是否未锁定
     */
    private boolean accountNonLocked = true;
    /**
     * 用户密码凭证是否未过期
     */
    private boolean credentialsNonExpired = true;
    /**
     * 用户账户是否可用
     */
    private boolean enabled = true;
}
  • 自定义用户认证 自定义 MyUserDetailsService
@Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 模拟一个用户,替代数据库获取逻辑
        MyUser user = new MyUser();
        user.setUserName(s);
        user.setPassword(BCrypt.hashpw("123", BCrypt.gensalt()));
        // 输出加密后的密码
        System.out.println(user.getPassword());
        // 此处返回一个任何用户名,密码都是 123 的账号
        return new User(s, user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("p1,p2"));
    }
  • 返回的 UserDetails 也是一个接口,源码如下
public interface UserDetails extends Serializable {
    //  获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象;
    Collection<? extends GrantedAuthority> getAuthorities();
    // 获取密码
    String getPassword();
    // 获取用户名
    String getUsername();
    // 判断账户是否未过期,未过期返回true反之返回false
    boolean isAccountNonExpired();
    // 判断账户是否未锁定
    boolean isAccountNonLocked();
    // 判断用户凭证是否没过期,即密码是否未过期
    boolean isCredentialsNonExpired();
    // 判断用户是否可用
    boolean isEnabled();
}

替换默认的登录页面

  • **自定义用户认证 ** 记住我 创建简单版本登录页面
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link rel="stylesheet" href="css/login.css" type="text/css">
</head>
<body>
<form class="login-page" action="/login" method="post">
    <div class="form">
        <h3>账户登录</h3>
        <input type="text" placeholder="用户名" name="username" required="required"/>
        <input type="password" placeholder="密码" name="password" required="required"/>
        <span style="display: inline">
            <input type="text" name="imageCode" placeholder="验证码" style="width: 50%;"/>
            <img src="/code/image"/>
        </span>
        <input type="checkbox" name="remember-me"/> 记住我
        <button type="submit">登录</button>
    </div>
</form>
</body>
</html>
  • 自定义用户认证 登录页面样式 css 见 src/main/resources/static/css/login.css

配置登录页面跳转,验证码校验,处理成功失败

  • Spring Security 配置
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private DataSource dataSource;
    @Resource
    private MyUserDetailService userDetailService;
    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // **记住我**
                .rememberMe()
                .tokenRepository(persistentTokenRepository())
                // remember 过期时间,单为秒
                .tokenValiditySeconds(3600)
                // 处理自动登录逻辑
                .userDetailsService(userDetailService)
                .and()
                // **图片验证码** 添加验证码拦截器在 UsernamePasswordAuthenticationFilter 之前
                .addFilterBefore(validateCodeFilter(), UsernamePasswordAuthenticationFilter.class)
                // **自定义用户认证**  表单登录
                .formLogin()
                // HTTP Basic
//                . http.httpBasic()
                // **自定义用户认证**  登录跳转 URL
                .loginPage("/authentication/require")
                // **自定义用户认证**   处理表单登录 URL
                .loginProcessingUrl("/login")
                // 处理登录成功
                .successHandler(myAuthenticationSuccessHandler())
                // 处理登录失败
                .failureHandler(myAuthenticationFailureHandler())
                .and()
                // **自定义用户认证**  授权配置
                .authorizeRequests()
                // 登录跳转 URL 无需认证
                .antMatchers("/authentication/require",
                        "/login.html",
                        "/code/image").permitAll()
                .antMatchers("/r/r1").hasAuthority("p1")
                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()
                // 所有请求放行
                .anyRequest().permitAll()
                // 都需要认证
//                .authenticated()
                .and().csrf().disable();
    }
    /**
     * **自定义用户认证** 
     * 密码验证方式
     * NoOpPasswordEncoder.getInstance() 字符串校验
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /**
     * 认证成功处理
     * @return
     */
    @Bean
    public MyAuthenticationSuccessHandler myAuthenticationSuccessHandler() {
        return new MyAuthenticationSuccessHandler();
    }
    /**
     * 认证失败处理
     * @return
     */
    @Bean
    public MyAuthenticationFailureHandler myAuthenticationFailureHandler() {
        return new MyAuthenticationFailureHandler();
    }
    /**
     * **图片验证码**
     * 验证码过滤器
     * @return
     */
    @Bean
    public ValidateCodeFilter validateCodeFilter() {
        return new ValidateCodeFilter();
    }
    /**
     * **记住我**
     * 记住我持久化使用
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        jdbcTokenRepository.setCreateTableOnStartup(false);
        return jdbcTokenRepository;
    }
}
  • 自定义用户认证 统一的登录拦截跳转
@RestController
public class BrowserSecurityController {
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    /**
     * 其中HttpSessionRequestCache为Spring Security提供的用于缓存请求的对象,
     * 通过调用它的getRequest方法可以获取到本次请求的HTTP信息。
     * DefaultRedirectStrategy的sendRedirect为Spring Security提供的用于处理重定向的方法。
     * <p>
     * 上面代码获取了引发跳转的请求,根据请求是否以.html为结尾来对应不同的处理方法。
     * 如果是以.html结尾,那么重定向到登录页面,否则返回”访问的资源需要身份认证!”信息,并且HTTP状态码为401(HttpStatus.UNAUTHORIZED)。
     *
     * @param request
     * @param response
     * @return
     * @throws IOException
     */
    @GetMapping("/authentication/require")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public String requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            String targetUrl = savedRequest.getRedirectUrl();
            if (StringUtils.endsWithIgnoreCase(targetUrl, ".html")) {
                redirectStrategy.sendRedirect(request, response, "/login");
            }
        }
        return "访问的资源需要身份认证!";
    }
}

处理成功与失败逻辑

  • 处理成功的逻辑,对象在 config 中配置
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    /**
     * json 处理登陆成功逻辑
     */
//    @Autowired
//    private ObjectMapper objectMapper;
//    @Override
//    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
//        httpServletResponse.setContentType("application/json;charset=utf-8");
//        httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authentication));
//    }
    /**
     * 成功页面跳转
     */
    private RequestCache requestCache = new HttpSessionRequestCache();
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);
//        savedRequest.getRedirectUrl();
        redirectStrategy.sendRedirect(request, response, "/index");
    }
}
  • 处理失败的逻辑
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper mapper;
    /**
     * 不同的失败原因对应不同的异常,
     * 比如用户名或密码错误对应的是BadCredentialsException
     * 用户不存在对应的是UsernameNotFoundException,
     * 用户被锁定对应的是LockedException
     *
     * @param request
     * @param response
     * @param exception
     * @throws IOException
     */
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
    }
}

记住我功能相关

  • pom.xml
<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
  • application.properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost:3306/springboot-security?useUnicode=true&characterEncoding=utf-8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=root
  • 数据库持久化语句,可在这个类中复制org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl#CREATE_TABLE_SQL
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null)

图片验证码相关

  • pom.xml
<!-- https://mvnrepository.com/artifact/org.springframework.social/spring-social-config -->
        <dependency>
            <groupId>org.springframework.social</groupId>
            <artifactId>spring-social-config</artifactId>
            <version>1.1.6.RELEASE</version>
        </dependency>
  • ImageCode 类
@Data
public class ImageCode {
    /**
     * 图片
     */
    private BufferedImage image;
    /**
     * 验证码
     */
    private String code;
    /**
     * 过期时间
     */
    private LocalDateTime expireTime;
    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }
    public ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
        this.image = image;
        this.code = code;
        this.expireTime = expireTime;
    }
    /**
     * 校验是否过期
     *
     * @return
     */
    public boolean isExpire() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}
  • 生成验证码
@RestController
public class ValidateController {
    public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode();
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
    }
    private ImageCode createImageCode() {
        int width = 100; // 验证码图片宽度
        int height = 36; // 验证码图片长度
        int length = 4; // 验证码位数
        int expireIn = 60; // 验证码有效时间 60s
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();
        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(rand);
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }
        g.dispose();
        return new ImageCode(image, sRand.toString(), expireIn);
    }
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}
  • 验证码校验 filter
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    /**
     * Spring Security实际上是由许多过滤器组成的过滤器链,
     * 处理用户登录逻辑的过滤器为UsernamePasswordAuthenticationFilter,
     * 而验证码校验过程应该是在这个过滤器之前的,即只有验证码校验通过后采去校验用户名和密码。
     * 由于Spring Security并没有直接提供验证码校验相关的过滤器接口,
     * 所以我们需要自己定义一个验证码校验的过滤器ValidateCodeFilter:
     * 在doFilterInternal方法中我们判断了请求URL是否为/login,
     * 该路径对应登录form表单的action路径,请求的方法是否为POST,是的话进行验证码校验逻辑,
     * 否则直接执行filterChain.doFilter让代码往下走。
     * 当在验证码校验的过程中捕获到异常时,调用Spring Security的校验失败处理器AuthenticationFailureHandler进行处理。
     *
     * @param httpServletRequest
     * @param httpServletResponse
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {
        if (StringUtils.equalsIgnoreCase("/login", httpServletRequest.getRequestURI())
                && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) {
            try {
                validateCode(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
    /**
     * 我们分别从Session中获取了ImageCode对象和请求参数imageCode(对应登录页面的验证码<input>框name属性),
     * 然后进行了各种判断并抛出相应的异常。
     * 当验证码过期或者验证码校验通过时,我们便可以删除Session中的ImageCode属性了。
     *
     * @param servletWebRequest
     * @throws ServletRequestBindingException
     */
    private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
        String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码不能为空!");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在!");
        }
        if (codeInSession.isExpire()) {
            sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
            throw new ValidateCodeException("验证码已过期!");
        }
        if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不正确!");
        }
        sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
    }
}
  • 异常处理
public class ValidateCodeException extends AuthenticationException {
    public ValidateCodeException(String message) {
        super(message);
    }
}

到了这里 代码基本分享 完毕 了.具体的效果等你来亲自验证了

Spring Security 加入短信验证码

只做代码记录,具体的可下载代码之后查看,源码点击原文或者下方源码链接

源码链接

  • 定义成功响应
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        redirectStrategy.sendRedirect(request, response, "/index");
    }
}
  • 定义失败响应
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Autowired
    private ObjectMapper mapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        response.setContentType("application/json;charset=utf-8");
        response.getWriter().write(mapper.writeValueAsString(exception.getMessage()));
    }
}
  • 配置验证码拦截器
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String MOBILE_KEY = "mobile";
    private String mobileParameter = MOBILE_KEY;
    private boolean postOnly = true;
    public SmsAuthenticationFilter() {
        super(new AntPathRequestMatcher("/login/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();
        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    protected void setDetails(HttpServletRequest request,
                              SmsAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
    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;
    }
    public final String getMobileParameter() {
        return mobileParameter;
    }
}
  • 自定义短信验证码 token
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    private final Object principal;
    public SmsAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }
    public SmsAuthenticationToken(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();
    }
}
  • 配置 验证码的配置
@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private UserDetailService userDetailService;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        smsAuthenticationProvider.setUserDetailService(userDetailService);
        http.authenticationProvider(smsAuthenticationProvider)
                .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
  • token的提供认证 SmsAuthenticationProvider
public class SmsAuthenticationProvider implements AuthenticationProvider {
    private UserDetailService userDetailService;
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal());
        if (userDetails == null)
            throw new InternalAuthenticationServiceException("未找到与该手机号对应的用户");
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }
    @Override
    public boolean supports(Class<?> aClass) {
        return SmsAuthenticationToken.class.isAssignableFrom(aClass);
    }
    public UserDetailService getUserDetailService() {
        return userDetailService;
    }
    public void setUserDetailService(UserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }
}
  • 验证码 model
public class SmsCode {
    private String code;
    private LocalDateTime expireTime;
    public SmsCode(String code, int expireIn) {
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }
    public SmsCode(String code, LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }
    boolean isExpire() {
        return LocalDateTime.now().isAfter(expireTime);
    }
    public String getCode() {
        return code;
    }
    public void setCode(String code) {
        this.code = code;
    }
    public LocalDateTime getExpireTime() {
        return expireTime;
    }
    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
}
  • 自定义验证码异常
public class ValidateCodeException extends AuthenticationException {
    private static final long serialVersionUID = 5022575393500654458L;
    public ValidateCodeException(String message) {
        super(message);
    }
}
  • 最关键的安全配置,让短信拦截加入我们的拦截链
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private MyAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    private SmsCodeFilter smsCodeFilter;
    @Autowired
    private SmsAuthenticationConfig smsAuthenticationConfig;
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers("/authentication/require",
                        "/login.html", "/code/image", "/code/sms", "/css/**").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()
                .apply(smsAuthenticationConfig);
    }
}
  • 定义登录页面
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <link rel="stylesheet" href="css/login.css" type="text/css">
</head>
<body>
<form class="login-page" action="/login/mobile" method="post">
    <div class="form">
        <h3>短信验证码登录</h3>
        <input type="text" placeholder="手机号" name="mobile" value="17777777777" required="required"/>
        <span style="display: inline">
            <input type="text" name="smsCode" placeholder="短信验证码" style="width: 50%;"/>
            <a href="/code/sms?mobile=17777777777">发送验证码</a>
        </span>
        <button type="submit">登录</button>
    </div>
</form>
</body>
</html>
  • 配置登录页面拦截 404
@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("login");
        registry.addViewController("/login.html").setViewName("login");
    }
}
  • 到这代码就可以运行了,效果就不演示了,流程 debug 一下就知道流程了哈哈哈哈哈,不明白也可以留言
相关实践学习
【文生图】一键部署Stable Diffusion基于函数计算
本实验教你如何在函数计算FC上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。函数计算提供一定的免费额度供用户使用。本实验答疑钉钉群:29290019867
建立 Serverless 思维
本课程包括: Serverless 应用引擎的概念, 为开发者带来的实际价值, 以及让您了解常见的 Serverless 架构模式
目录
相关文章
|
7月前
|
安全 Java 数据库
后端进阶之路——万字总结Spring Security与数据库集成实践(五)
后端进阶之路——万字总结Spring Security与数据库集成实践(五)
|
7月前
|
安全 架构师 Java
理论实战源码齐飞!架构师社区疯传的SpringSecurity进阶小册真香
安全管理是Java应用开发中无法避免的问题,随着Spring Boot和微服务的流行,Spring Security受到越来越多Java开发者的重视,究其原因,还是沾了微服务的光。作为Spring家族中的一员,其在和Spring家族中的其他产品如SpringBoot、Spring Cloud等进行整合时,是拥有众多同类型框架无可比拟的优势的。
88 0
|
XML Java 数据格式
肝了30天总结,史上最全面透彻的Spring核心原理分析和27道高频面试题
在阅读面试题之前,小伙伴们可以先看看我之前发布的系列文章,Spring核心原理包括源码分析和用30个类手写。面试刷题固然很重要,但是知其然知其所以然更重要。
1575 4
肝了30天总结,史上最全面透彻的Spring核心原理分析和27道高频面试题
|
7月前
|
Java Docker 容器
美团大牛精心整理SpringBoot学习笔记,从Web入门到系统架构
近期慢慢复工,为了准备面试,各路码友们都开始磨拳擦脚,背面试题、知识点。小编最近得一良友赠送了一份关于SpringBoot的学习笔记,简直不要好用,理论解析言简意赅,每一步操作都有图片展示。这么好的东西肯定不能私藏,为了感谢大家在2019年里的支持,我现在将这份笔记赠送给大家,祝大家前程似锦,Offer不断!
|
5月前
|
NoSQL Java 数据库连接
《滚雪球学Spring Boot》教程导航帖(更新于2024.07.16)
📚 《滚雪球学Spring Boot》是由CSDN博主bug菌创作的全面Spring Boot教程。作者是全栈开发专家,在多个技术社区如CSDN、掘金、InfoQ、51CTO等担任博客专家,并拥有超过20万的全网粉丝。该教程分为入门篇和进阶篇,每篇包含详细的教学步骤,涵盖Spring Boot的基础和高级主题。
69 1
《滚雪球学Spring Boot》教程导航帖(更新于2024.07.16)
|
6月前
|
XML 缓存 Java
大厂面试攻略:Spring框架核心要点精讲
Java SPI (Service Provider Interface) 是一种服务发现机制,允许在运行时动态加载和发现服务提供者。在数据库驱动加载中,SPI使得数据库驱动能够自动识别和注册,而无需显式加载。 Spring 是一个广泛应用的轻量级框架,核心功能包括依赖注入(DI)和面向切面编程(AOP)。不使用Spring时,开发人员需要手动管理对象的创建和依赖关系,使用Servlet等基础组件完成Web开发,以及手动处理JDBC操作。Spring通过管理Bean的生命周期和依赖关系,简化了企业级应用的开发,降低了代码的侵入性。
82 1
大厂面试攻略:Spring框架核心要点精讲
|
7月前
|
监控 安全 Java
腾讯T4大牛整理的SpringBoot文档,覆盖你认知中的所有操作
SpringBoot目前的使用已经很普遍了,实际的项目中,我们需要集成各种的插件支持,不仅如此,还有很多可能我们平时不知道,但是很方便的操作。pdf里面的东西还是比较全面的。
|
前端开发 Java 关系型数据库
IT市场寒冬过了!GitHub置顶的Spring微服务架构实战派手册有用了
“今年找工作好难啊” 感觉大家总是陷入市场寒冬这个氛围里面“无法自拔”,虽然今年经济下行,工作的确难找,但是在职友集上可查询到的Java岗位仍有20w+,需求相比其他而言并不算少,更别提还有技术细分下的岗位可选!
|
缓存 监控 Java
膜拜!Alibaba最新发布SprinBoot:进阶原理实战与面试题分析指南
本书对Spring Boot的各项功能特性及其最佳实践、实现原理展开讨论,涵盖了核心容器、Web服务、内置缓存、数据访问、并发编程、监控和扩展等一系列核心主题,这些核心主题也广泛应用于Spring家族中的其他开发框架。
127 0
|
XML JSON Java
腾讯T3-3级资深架构师呕心整理:10万字节springboot教程全解!
可以毫不夸张地说,这篇文章介绍的 SpringBoot 教程基本已经涵盖你工作中遇到的大部分常用的场景。对于每一个springboot的用法,文中都会都会有详细的教程解释。