• Spring Security(二)实现图片验证码和自动登陆(记住我)


    录:

    1、实现图片验证码
        1.1、创建获取图片验证码的 controller
        1.2、编写用于校验图片验证码的过滤器
        1.3、将图片验证码过滤器添加在 UsernamePasswordAuthenticationFilter 之前
        1.4、修改表单登陆页
        1.5、测试
    2、自动登陆(记住我)
        2.1、散列加密方案
        2.2、持久化令牌方案

    1、实现图片验证码    <--返回目录

     1.1、创建获取图片验证码的 controller   <--返回目录

      要想实现图片验证码,首先需要一个用于获取图片验证码的 API。这里使用 kaptchar 勿用于生产)

    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>2.3.2</version>
    </dependency>
    View Code

      配置一个 kaptcha 实例:

    @Bean
    public Producer imageCode() {
        // 配置图形验证码的基本参数
        Properties properties = new Properties();
        properties.setProperty("kaptcha.image.width", "150");//图片宽度
        properties.setProperty("kaptcha.image.height", "50");//图片高度
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");//字符集
        properties.setProperty("kaptcha.textproducer.char.length", "4");//字符长度
        Config config = new Config(properties);
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
    View Code

      创建 ValidateCodeController,用于生成图片验证码

    package com.oy.validate;
    
    import java.awt.image.BufferedImage;
    
    import javax.imageio.ImageIO;
    import javax.servlet.ServletOutputStream;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    
    import com.google.code.kaptcha.Producer;
    
    @Controller
    public class ValidateCodeController {
    
        private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    
        @Autowired
        private Producer kaptchaProducer;
    
        @GetMapping("/code/image")
        public void createCode(HttpServletRequest request, HttpServletResponse response)
                throws Exception {
            response.setContentType("image/jpeg");
            // 创建验证码文本
            String codeText = kaptchaProducer.createText();
            // 将验证码文本设置到 session
            request.getSession().setAttribute(SESSION_KEY, codeText);
            // 根据文本创建图片
            BufferedImage bi = kaptchaProducer.createImage(codeText);
            // 获取响应输出流
            ServletOutputStream out = response.getOutputStream();
            ImageIO.write(bi, "jpg", out);
            // 推送并关闭响应输出流
            try {
                out.flush();
            } finally {
                out.close();
            }
        }
    
    }
    View Code

      访问图片验证码(路径 "/code/image")时不设置权限,在 JavaConfig 配置类中配置

    antMatchers("/app/api/**", "/mylogin.html", "/code/image")
    .permitAll() // 公开权限

      访问 http://localhost:8089/BootDemo/code/image,即可看到返回一张图片验证码。

    1.2、编写用于校验图片验证码的过滤器   <--返回目录

      虽然 Spring Security 的过滤器对过滤器没有特殊要求,只要继承 Filter 即可,但是在 Spring 体系中,推荐使用 OncePerRequestFilter 来实现,它可以确保一次请求只会通过一次该过滤器(Filter 实际上并不能保证这一点)。

      ValidateCodeFIlter

    package com.oy.validate;
    
    import java.io.IOException;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.util.StringUtils;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    public class ValidateCodeFIlter extends OncePerRequestFilter {
    
        private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
        
        private AuthenticationFailureHandler myAuthenticationFailureHandler;
        public void setMyAuthenticationFailureHandler(AuthenticationFailureHandler myAuthenticationFailureHandler) {
            this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
                throws ServletException, IOException {
            System.out.println("ValidateCodeFIlter start, 请求uri:" 
                + request.getRequestURI() + ", servletPath:" + request.getServletPath());
            // 非登陆请求不校验验证码
            if (!"/auth/form".equals(request.getServletPath())) {
                filterChain.doFilter(request, response);
                return;
            }
            
            try {
                doValidateCode(request);
                filterChain.doFilter(request, response);
            } catch(ValidateCodeException e) {
                myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
            }
            System.out.println("ValidateCodeFIlter end...");
        }
        
        private void doValidateCode(HttpServletRequest request) {
            String requestCode = request.getParameter("image_code");
            HttpSession session = request.getSession();
            String sessionCode = (String) session.getAttribute(SESSION_KEY);
                    
            System.out.println("ValidateCodeFIlter, requestCode:" 
                    + requestCode + " sessionCode:" + sessionCode);
            
            if (!StringUtils.isEmpty(sessionCode)) {
                // 随手清除 session 中验证码,无论验证成功还是失败
                session.removeAttribute(SESSION_KEY);
            }
            // 校验不通过,抛出异常
            if (StringUtils.isEmpty(requestCode)) {
                throw new ValidateCodeException("验证码输入为空");
            }
            if (StringUtils.isEmpty(sessionCode)) {
                throw new ValidateCodeException("验证码为空");
            }
            if (!requestCode.equals(sessionCode)) {
                throw new ValidateCodeException("验证码输入错误");
            }
            // 没有异常,表示校验通过
        }
    
    }
    View Code

      ValidateCodeException

    package com.oy.validate;
    
    import org.springframework.security.core.AuthenticationException;
    
    public class ValidateCodeException extends AuthenticationException {
    
        private static final long serialVersionUID = 8369364787664640677L;
    
        public ValidateCodeException(String msg) {
            super(msg);
        }
    
    }
    View Code

      MyAuthenticationFailureHandler

    package com.oy.security;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler  {
    
        //@Autowired
        //private ObjectMapper objectMapper;
        
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                AuthenticationException exception) throws IOException, ServletException {
            
            System.out.println("登录失败," + exception.getMessage());
            super.onAuthenticationFailure(request, response, exception);
            
            /*
             * 根据配置项来确定返回 json 还是 按照 Spring Securiy 原来默认进行跳转
            if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));
            } else {
                super.onAuthenticationFailure(request, response, exception);
            }
            */
        }
    
    }
    View Code

      MyAuthenticationSuccessHandler

    package com.oy.security;
    
    import java.io.IOException;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
        //@Autowired
        //private ObjectMapper objectMapper;
    
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                Authentication authentication) throws IOException, ServletException {
    
            System.out.println("登录成功");
            super.onAuthenticationSuccess(request, response, authentication);
            /*
            if (LoginResponseType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(objectMapper.writeValueAsString(authentication));
            } else {
                super.onAuthenticationSuccess(request, response, authentication);
            }
        */
        }
    
    }
    View Code

    1.3、将图片验证码过滤器添加在 UsernamePasswordAuthenticationFilter 之前   <--返回目录

      WebSecurityConfig

    package com.oy;
    
    import java.io.IOException;
    import java.util.Properties;
    
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    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.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.crypto.password.NoOpPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    
    import com.google.code.kaptcha.Producer;
    import com.google.code.kaptcha.impl.DefaultKaptcha;
    import com.google.code.kaptcha.util.Config;
    import com.oy.validate.ValidateCodeFIlter;
    
    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Autowired
        private AuthenticationFailureHandler myAuthenticationFailureHandler;
        @Autowired
        private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter();
            validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
            
            // 将图片验证码过滤器添加在 UsernamePasswordAuthenticationFilter 之前
            http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                .loginPage("/mylogin.html") // 指定登陆页
                .loginProcessingUrl("/auth/form") // 指定处理登陆请求的路径
                .successHandler(myAuthenticationSuccessHandler)// 指定登陆成功时的处理逻辑
                .failureHandler(myAuthenticationFailureHandler)// 指定登陆失败时的处理逻辑
                .and()
                .authorizeRequests()
                    .antMatchers("/admin/api/**").hasRole("admin")
                    .antMatchers("/user/api/**").hasRole("user")
                    // 登陆页、验证码公开权限
                    .antMatchers("/app/api/**", "/mylogin.html", "/code/image")
                    .permitAll() // 公开权限
                    .anyRequest().authenticated()
                    .and()
                .csrf().disable();
        }
        
        @Bean
        public PasswordEncoder passwordEncoder() {
            return NoOpPasswordEncoder.getInstance();
        }
        
        @Bean
        public Producer imageCode() {
            // 配置图形验证码的基本参数
            Properties properties = new Properties();
            properties.setProperty("kaptcha.image.width", "150");//图片宽度
            properties.setProperty("kaptcha.image.height", "50");//图片高度
            properties.setProperty("kaptcha.textproducer.char.string", "0123456789");//字符集
            properties.setProperty("kaptcha.textproducer.char.length", "4");//字符长度
            Config config = new Config(properties);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    }

    1.4、修改表单登陆页   <--返回目录

    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Insert title here</title>
    </head>
    <body>
    <h2>自定义表单登陆页</h2>
    <form action="auth/form" method="post">
    用户名:<input type="text" name="username" /><br/>&nbsp;&nbsp;&nbsp;码:<input type="text" name="password" /><br/>
    
    验证码:<input type="text" name="image_code" /><br/>
    <img src="code/image" alt="imagecode" height="50px" width="150px" /><br/>
    
    <input type="submit" value="提交" />
    </form>
    </body>
    </html>

    1.5、测试   <--返回目录

      启动项目,访问 http://localhost:8089/BootDemo/admin/api/1, 控制台打印结果

    // 访问:http://localhost:8089/BootDemo/admin/api/1
    ValidateCodeFIlter start, 请求uri:/BootDemo/admin/api/1, servletPath:/admin/api/1
    ValidateCodeFIlter start, 请求uri:/BootDemo/mylogin.html, servletPath:/mylogin.html
    ValidateCodeFIlter start, 请求uri:/BootDemo/code/image, servletPath:/code/image
    
    // 使用 admin/123 登陆,验证码输入错误
    ValidateCodeFIlter start, 请求uri:/BootDemo/auth/form, servletPath:/auth/form
    ValidateCodeFIlter, requestCode:ff sessionCode:9657
    登录失败,验证码输入错误
    ValidateCodeFIlter end...
    
    // 登陆失败后,Spring Security 默认行为:跳转到登陆页面
    ValidateCodeFIlter start, 请求uri:/BootDemo/mylogin.html, servletPath:/mylogin.html
    ValidateCodeFIlter start, 请求uri:/BootDemo/code/image, servletPath:/code/image

    2、自动登陆(记住我)   <--返回目录

       自动登陆时将用户的登陆信息保存在客户端浏览器的 cookie 中,当用户下次访问时,自动实现校验并建立登陆状态的一种机制。

      Spring Security 提供了两种令牌:

      1)用散列算法加密用户必要的登陆信息并生成令牌;

      2)数据库等持久化数据存储机制用的持久化令牌;

    2.1、散列加密方案   <--返回目录

        如下红色字体的配置

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Autowired
        private AuthenticationFailureHandler myAuthenticationFailureHandler;
        @Autowired
        private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
        @Autowired
        private UserDetailsService myUserDetailsService;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter();
            validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
            
            // 将图片验证码过滤器添加在 UsernamePasswordAuthenticationFilter 之前
            http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage("/mylogin.html") // 指定登陆页
                    .loginProcessingUrl("/auth/form") // 指定处理登陆请求的路径
                    .successHandler(myAuthenticationSuccessHandler)// 指定登陆成功时的处理逻辑
                    .failureHandler(myAuthenticationFailureHandler)// 指定登陆失败时的处理逻辑
                    .and()
                .rememberMe()
                    .userDetailsService(myUserDetailsService)
                    .key("rem_key")
                    .and()
                .authorizeRequests()
                    .antMatchers("/admin/api/**").hasRole("admin")
                    .antMatchers("/user/api/**").hasRole("user")
                    // 登陆页、验证码公开权限
                    .antMatchers("/app/api/**", "/mylogin.html", "/code/image")
                    .permitAll() // 公开权限
                    .anyRequest().authenticated()
                    .and()
                .csrf().disable();
        }
        
      // 省略
    }

      表单登陆页,添加 <input type="checkbox" name="remember-me"/> 进行测试。启动项目,访问 http://localhost:8089/BootDemo/admin/api/1, 跳转到登陆页,使用 admin/123 登陆,勾选 “remember me” 复选框。登陆成功后,查看 cookie, 默认过期时间 2 星期。

       将该 cookie 的 value 值进行 base64 解码:

    YWRtaW46MTU4ODEzMTE3ODE5MzplY2RlYWQxOGNhNzcxM2NjZTk2ZmRhZjM4NzI5YTk4YQ==
    === base64 解码 ===>
    admin:1588131178193:ecdead18ca7713cce96fdaf38729a98a

      验证最后那串 hash 字符串,可以看到打印结果符合预期(注:DigestUtils 是 commons-codec.commons-codec.1.14 提供)

    public void demo() {
        String hash = DigestUtils.md5Hex("admin:1588131178193:123:rem_key");
        System.out.println(hash);//ecdead18ca7713cce96fdaf38729a98a
    }

      那么,remember-me 这个 cookie 的 value 值是根据什么规则生成的呢?

    hashInfo = md5Hex(username + ":" + expirationTime + ":" + password + ":" + key)
    rememberCookie = base64(username + ":" + expirationTime  + ":" + hashInfo)

      其中,expirationTime 是过期时间;key 是散列盐值,用于防止令牌被修改(防止用户自行修改,因为用户是知道自己的用户和密码的,如果没有这个 key,用户可以自行修改 expirationTime 的值)。

      通过这中方式生成 cookie 后,在下次登陆时,Spring Security 首先用 base64 解码,得到用户名、过期时间和加密散列值;然后使用用户名得到密码;接着重新以上面的散列算法正向计算,并将计算结果与从浏览器获取的加密散列值进行对比,从而确定该令牌是否有效。

    2.2、持久化令牌方案   <--返回目录

      持久化令牌方案的原理

       Remember Me 过滤器位置

      配置:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Autowired
        private DataSource dataSource;
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //tokenRepository.setCreateTableOnStartup(true);// 启动时创建表,第二次启动项目注释掉
            return tokenRepository;
        }
        @Autowired
        private UserDetailsService myUserDetailsService;
        
        @Autowired
        private AuthenticationFailureHandler myAuthenticationFailureHandler;
        @Autowired
        private AuthenticationSuccessHandler myAuthenticationSuccessHandler;
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFIlter validateCodeFIlter = new ValidateCodeFIlter();
            validateCodeFIlter.setMyAuthenticationFailureHandler(myAuthenticationFailureHandler);
            
            // 将图片验证码过滤器添加在 UsernamePasswordAuthenticationFilter 之前
            http.addFilterBefore(validateCodeFIlter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage("/mylogin.html") // 指定登陆页
                    .loginProcessingUrl("/auth/form") // 指定处理登陆请求的路径
                    .successHandler(myAuthenticationSuccessHandler)// 指定登陆成功时的处理逻辑
                    .failureHandler(myAuthenticationFailureHandler)// 指定登陆失败时的处理逻辑
                    .and()
                .rememberMe()
                    .tokenRepository(persistentTokenRepository()) // 持久化 token
                    .tokenValiditySeconds(3600 * 24 * 7) // 过期时间, 单位秒
                    .userDetailsService(myUserDetailsService) // 使用该 UserDetailsService 校验用户
                    .key("rem_key")
                    .and()
                .authorizeRequests()
                    .antMatchers("/admin/api/**").hasRole("admin")
                    .antMatchers("/user/api/**").hasRole("user")
                    // 登陆页、验证码公开权限
                    .antMatchers("/app/api/**", "/mylogin.html", "/code/image")
                    .permitAll() // 公开权限
                    .anyRequest().authenticated()
                    .and()
                .csrf().disable();
        }
        
        // 省略
    }

    参考:

      1)《Spring Security 实战》-- 陈木鑫

  • 相关阅读:
    LInux 安全测试 2
    LInux 安全测试
    又一款linux提权辅助工具
    shell 生成指定范围随机数与随机字符串 .
    腾讯新浪通过IP地址获取当前地理位置(省份)的接口
    建站指南:百度认为什么样的网站更有抓取和收录价值2012-06-20
    ngx_lua 金山项目 黄雀
    在页面中使用js
    多线程取读文件(别人的,有点晕,先放在这里有时间研究研究)
    factory工厂模式
  • 原文地址:https://www.cnblogs.com/xy-ouyang/p/12703831.html
Copyright © 2020-2023  润新知