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 一下就知道流程了哈哈哈哈哈,不明白也可以留言

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值