美文网首页SpringBoot极简教程 · Spring Boot Spring-Boot
Spring Security项目基于表单登陆(三)

Spring Security项目基于表单登陆(三)

作者: 郭少华 | 来源:发表于2019-05-27 19:32 被阅读16次

    SpringSecurity基本原理

    application.yml注释掉关闭security的配置

    #先关闭security默认配置
    #security:
    #  basic:
    #    enabled: false
    

    创建配置文件

    image.png

    表单登陆方式

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin() //表单登陆页
            //http.httpBasic()   //security默认方式
                .and()
                .authorizeRequests()//下面授权配置
                .anyRequest()//所有请求
                .authenticated();//都需要身份认证
        }
    }
    
    image.png

    账号是user密码是程序启动时生成的密码

    默认登陆方式

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //http.formLogin() //表单登陆页
            http.httpBasic()   //security默认方式
                .and()
                .authorizeRequests()//下面授权配置
                .anyRequest()//所有请求
                .authenticated();//都需要身份认证
        }
    }
    
    image.png

    过滤器链

    自定义认证用户逻辑

    打开密码加密


    image.png
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.formLogin() //表单登陆页
            //http.httpBasic()   //security默认方式
                .and()
                .authorizeRequests()//下面授权配置
                .anyRequest()//所有请求
                .authenticated();//都需要身份认证
        }
    }
    
    
    @Component
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        //认证登陆
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            //isAccountNonExpired 账号是否过期
            //isCredentialsNonExpired 密码是否过期
            //isAccountNonLocked 账号是否锁定用于冻结状态
            //isEnabled 账号是否可用用于删除状态
    
            //第一个参数是传进来的账号,第二个参数是数据库根据账号查询到到密码
            //第三个参数是账号是否可用,第四个参数是账号是否过期
            //第五个参数是密码是否过期,第六个参数账号是否锁定
            //第七个参数账号到权限
            return new User(username,userRepository.findByUsername(username).getPassword(),
                    true,true,true,userRepository.findByUsername(username).getIsAccountNonLocked(),
                    AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
        }
        //模拟注册
        public void register(){
            com.guosh.security.browser.domain.User user=new com.guosh.security.browser.domain.User();
            user.setUsername("ceshi");
            //存入加密后的密码
            user.setPassword(passwordEncoder.encode("123456"));
            user.setIsEnabled(true);
            user.setIsAccountNonLocked(true);
            userRepository.save(user);
        }
    
    }
    

    个性化用户认证流程

    自定义登陆页面并实现form表单提交

    创建登陆页面

    image.png
    <!DOCTYPE html>
    <html>  
    <head>
    <title>Login</title>
    </head>
    <body>
     <h1>Login Form</h1>
        <form action="/guoshsecurity/authentication/form" method="post">
            <input type="text" class="text" value="" name="username">
            <input type="password" value="" name="password">
            <input type="submit" value="Login" >
        </form>
    </body>
    </html>
    

    添加登陆页面地址,并把登陆页面地址设置不需要认证,自定义form表单提交地址

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()//关闭跨站防护
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/defaultLogin.html").permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                    .and()
                        .formLogin() //表单登陆页
                        .loginPage("/defaultLogin.html")//登陆页面
                        .loginProcessingUrl("/authentication/form");//自定义form表单登陆提交地址默认是/login
    
    
    
        }
    }
    

    处理不同类型的请求

    image.png
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private SecurityProperties securityProperties;
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()//关闭跨站防护
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/login",securityProperties.getBrowser().getLoginPage()).permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                    .and()
                        .formLogin() //表单登陆页
                        .loginPage("/login")//登陆页面
                        .loginProcessingUrl("/authentication/form");//自定义form表单登陆提交地址默认是/login
    
    
    
        }
    }
    

    处理登陆请求是跳转到默认登陆页面或者用户自定义登陆页面

    @RestController
    public class BrowserSecurityController {
    
        private RequestCache requestCache=new HttpSessionRequestCache();
    
        private RedirectStrategy redirectStrategy=new DefaultRedirectStrategy();
    
        @Autowired
        private SecurityProperties securityProperties;
        /**
         * 需要身份认证时跳转地址
         * @param request
         * @param response
         * @return
         */
        @RequestMapping("/login")
        @ResponseStatus(code = HttpStatus.UNAUTHORIZED)
        public SimpleResponse requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
            SavedRequest savedRequest=requestCache.getRequest(request,response);
            if(savedRequest!=null){
                String targetUrl = savedRequest.getRedirectUrl();
                //System.out.println(savedRequest);
                if(StringUtils.endsWithIgnoreCase(targetUrl,".html")){
                    redirectStrategy.sendRedirect(request,response,securityProperties.getBrowser().getLoginPage());
                }
            }
            return new SimpleResponse("访问服务需要身份认证,请引导用户到登陆页");
        }
    }
    

    自定义登陆页面

    image.png
    @Configuration
    @EnableConfigurationProperties(SecurityProperties.class)
    public class SecurityCoreConfig {
    }
    
    
    public class BrowserProperties {
    
        private String loginPage="/defaultLogin.html"; //如果用户没有配置登陆页面走默认
    
        public String getLoginPage() {
            return loginPage;
        }
    
        public void setLoginPage(String loginPage) {
            this.loginPage = loginPage;
        }
    }
    
    
    
    @ConfigurationProperties(prefix = "guosh.security")
    public class SecurityProperties {
    
        private BrowserProperties browser=new BrowserProperties();
    
        public BrowserProperties getBrowser() {
            return browser;
        }
    
        public void setBrowser(BrowserProperties browser) {
            this.browser = browser;
        }
    }
    
    

    用户如果在application.yml指定了登陆页面会跳转指定登陆页

    #自定义登陆页面
    guosh:
      security:
        browser:
          loginPage: /demoLogin.html
    

    自定义登陆成功处理

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Autowired
        private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
    
        @Autowired
        private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .csrf().disable()//关闭跨站防护
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/login",securityProperties.getBrowser().getLoginPage()).permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                    .and()
                        .formLogin() //表单登陆页
                        .loginPage("/login")//登陆页面
                        .loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
                        .successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
                        .failureHandler(browserAuthenticationFailureHandler);//自定义登陆失败返回json
    
        }
    }
    

    创建自定义成功失败的实现方法


    image.png
    @Component
    public class BrowserAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        @Autowired
        private ObjectMapper objectMapper;
    
        //自定义登陆成功
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    
            logger.info("登陆成功");
    
            response.setContentType("application/json;charset=UTF-8");
            //转json字符串返回
            response.getWriter().write(objectMapper.writeValueAsString(authentication));
    
        }
    }
    
    @Component
    public class BrowserAuthenticationFailureHandler implements AuthenticationFailureHandler {
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        @Autowired
        private ObjectMapper objectMapper;
    
        //自定义登陆失败
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            logger.info("登陆失败");
            //401状态码
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json;charset=UTF-8");
            //转json字符串返回
            response.getWriter().write(objectMapper.writeValueAsString(e));
        }
    }
    
    

    自定义配置支持同步请求与异步请求

    创建可配置的请求方式


    image.png
    public enum LoginType {
        REDITECT,
        JSON
    }
    
    public class BrowserProperties {
    
        //自定义登陆页面
        private String loginPage="/defaultLogin.html"; //如果用户没有配置登陆页面走默认
    
        //自定义处理登陆请求默认异步
        private LoginType loginType=LoginType.JSON;
    
        public String getLoginPage() {
            return loginPage;
        }
    
        public void setLoginPage(String loginPage) {
            this.loginPage = loginPage;
        }
    
        public LoginType getLoginType() {
            return loginType;
        }
    
        public void setLoginType(LoginType loginType) {
            this.loginType = loginType;
        }
    }
    

    创建自定义成功失败的实现方法


    image.png
    
    @Component
    public class BrowserAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Autowired
        private SecurityProperties securityProperties;
    
        //自定义登陆成功
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
    
            logger.info("登陆成功");
            //判断异步请求还是同步请求
            if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
                response.setContentType("application/json;charset=UTF-8");
                //转json字符串返回
                response.getWriter().write(objectMapper.writeValueAsString(authentication));
            }
            else {
                super.onAuthenticationSuccess(request,response,authentication);
            }
    
    
        }
    }
    
    @Component
    public class BrowserAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
        private Logger logger= LoggerFactory.getLogger(getClass());
    
        @Autowired
        private ObjectMapper objectMapper;
    
        @Autowired
        private SecurityProperties securityProperties;
    
        //自定义登陆失败
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
            logger.info("登陆失败");
            if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
                //401状态码
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.setContentType("application/json;charset=UTF-8");
                //转json字符串返回
                response.getWriter().write(objectMapper.writeValueAsString(e));
            }else{
                super.onAuthenticationFailure(request,response,e);
            }
    
        }
    }
    
    

    用户如果在application.yml指定了请求方式会按照指定的方式执行

    guosh:
      security:
        browser:
          loginPage: /demoLogin.html
          #JSON异步登陆 REDITECT同步登陆
          loginType: JSON
    

    获取登陆用户信息

        @RequestMapping(value = "/me",method = RequestMethod.GET)
        public Object getLoginUser() {
            //获取详细登陆信息
            //SecurityContextHolder.getContext().getAuthentication();
            //获取用户的登陆信息
            return ((UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal());
    
        }
    

    图片验证码

    图形验证码接口

    image.png

    验证码实体类

    public class ImageCode {
    
        //验证码图片
        private BufferedImage image;
        //随机数
        private String code;
        //失效时间
        private LocalDateTime exireTime;
    
        public ImageCode(BufferedImage image, String code, int exireIn) {
            this.image = image;
            this.code = code;
            //当前时间加上过期秒数
            this.exireTime = LocalDateTime.now().plusSeconds(exireIn);
        }
        //判断时间是否过期
        public boolean isExpried(){
            return LocalDateTime.now().isAfter(exireTime);
        }
    
        public BufferedImage getImage() {
            return image;
        }
    
        public void setImage(BufferedImage image) {
            this.image = image;
        }
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public LocalDateTime getExireTime() {
            return exireTime;
        }
    
        public void setExireTime(LocalDateTime exireTime) {
            this.exireTime = exireTime;
        }
    }
    
    

    验证码接口实现

    @RestController
    public class ValidateCodeController {
    
        public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE";
    
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
    
        @RequestMapping(value = "/code/image",method = RequestMethod.GET)
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            //生成验证码对象
            ImageCode imageCode=caeateImageCode(request);
            //将ImageCode对象保存在session中
            sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
            //返回到页面
            ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
        }
    
        private ImageCode caeateImageCode(HttpServletRequest request) {
            int width = 67;
            int height = 23;
            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);
            }
    
            String sRand = "";
            for (int i = 0; i < 4; i++) {
                String rand = String.valueOf(random.nextInt(10));
                sRand += 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, 60);
        }
    
        /**
         * 生成随机背景条纹
         *
         * @param fc
         * @param bc
         * @return
         */
        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);
        }
    
    
    }
    

    验证码的异常处理器

    public class ValidateCodeException extends AuthenticationException {
    
    
        public ValidateCodeException(String msg) {
            super(msg);
        }
    }
    

    在登陆之前验证验证码的拦截

    public class ValidateCodeFilter extends OncePerRequestFilter {
    
        //登陆失败的处理器
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            //必须是登陆请求并且是post请求
            if(StringUtils.equals("/guoshsecurity/authentication/form",request.getRequestURI())&&StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
                try {
                    //校验验证码
                    validate(new ServletWebRequest(request));
                }catch (ValidateCodeException e){
                    authenticationFailureHandler.onAuthenticationFailure(request,response,e);
                    return;
                }
            }
            filterChain.doFilter(request,response);
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            //从session获取验证码
            ImageCode codeInSession=(ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
            //从请求里获取验证码
            String codeInRequest=ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
    
            if(StringUtils.isBlank(codeInRequest)){
                throw new ValidateCodeException("验证码不能为空");
            }
            if(codeInSession == null){
                throw new ValidateCodeException("验证码不存在");
            }
            //判断验证码时间是否过期
            if(codeInSession.isExpried()){
                sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
                throw new ValidateCodeException("验证码已经过期");
            }
            //比对验证码
            if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
                throw new ValidateCodeException("验证码不匹配");
            }
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
        }
    
        public AuthenticationFailureHandler getAuthenticationFailureHandler() {
            return authenticationFailureHandler;
        }
    
        public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
            this.authenticationFailureHandler = authenticationFailureHandler;
        }
    }
    

    修改WebSecurityConfig配置文件

    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Autowired
        private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
    
        @Autowired
        private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
    
    
        @Autowired
        private SessionRegistry sessionRegistry;
    
        @Bean
        public SessionRegistry sessionRegistry() {
            return new SessionRegistryImpl();
        }
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
            validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
    
    
            http
                    .csrf().disable()//关闭跨站防护
                    .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                    .and()
                        .formLogin() //表单登陆页
                        .loginPage("/login")//登陆页面
                        .loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
                        .successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
                        .failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
                    .and()
                        .sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
    
        }
    }
    

    图形验证码重构

    验证码基本参数,验证码拦截接口可配置

    image.png
    public class ImageCodeProperties {
        //默认验证码宽度
        private int width = 67;
        //默认验证码高度
        private int height = 23;
        //默认验证码位数
        private int length = 4;
        //默认验证码失效时间
        private int expreIn= 60;
        //哪些url地址需要验证码拦截
        private String url;
        public int getWidth() {
            return width;
        }
    
        public void setWidth(int width) {
            this.width = width;
        }
    
        public int getHeight() {
            return height;
        }
    
        public void setHeight(int height) {
            this.height = height;
        }
    
        public int getLength() {
            return length;
        }
    
        public void setLength(int length) {
            this.length = length;
        }
    
        public int getExpreIn() {
            return expreIn;
        }
    
        public void setExpreIn(int expreIn) {
            this.expreIn = expreIn;
        }
    
        public String getUrl() {
            return url;
        }
    
        public void setUrl(String url) {
            this.url = url;
        }
    }
    
    
    public class ValidateCodeProperties {
    
        private ImageCodeProperties image=new ImageCodeProperties();
    
        public ImageCodeProperties getImage() {
            return image;
        }
    
        public void setImage(ImageCodeProperties image) {
            this.image = image;
        }
    }
    
    @ConfigurationProperties(prefix = "guosh.security")
    public class SecurityProperties {
        //浏览器登陆配置
        private BrowserProperties browser=new BrowserProperties();
        //验证码配置
        private ValidateCodeProperties code=new ValidateCodeProperties();
    
        public BrowserProperties getBrowser() {
            return browser;
        }
    
        public void setBrowser(BrowserProperties browser) {
            this.browser = browser;
        }
    
        public ValidateCodeProperties getCode() {
            return code;
        }
    
        public void setCode(ValidateCodeProperties code) {
            this.code = code;
        }
    }
    
    public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    
        //登陆失败的处理器
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
    
        private Set<String>urls = new HashSet<>();
    
        private SecurityProperties securityProperties;
    
        private AntPathMatcher antPathMatcher=new AntPathMatcher();
    
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            //分割转换数组
            String[]configUrls=StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
            for (String configUrl:configUrls) {
                urls.add(configUrl);
            }
            urls.add("/authentication/form");
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            boolean action = false;
            //用antPathMatcher工具栏判断路径是否符合
            for (String url:urls) {
                if(antPathMatcher.match(url,request.getServletPath())){
                    action=true;
                }
            }
    
            //必须是登陆请求并且是post请求
            if(action){
                try {
                    //校验验证码
                    validate(new ServletWebRequest(request));
                }catch (ValidateCodeException e){
                    authenticationFailureHandler.onAuthenticationFailure(request,response,e);
                    return;
                }
            }
            filterChain.doFilter(request,response);
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            //从session获取验证码
            ImageCode codeInSession=(ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
            //从请求里获取验证码
            String codeInRequest=ServletRequestUtils.getStringParameter(request.getRequest(),"imageCode");
    
            if(StringUtils.isBlank(codeInRequest)){
                throw new ValidateCodeException("验证码不能为空");
            }
            if(codeInSession == null){
                throw new ValidateCodeException("验证码不存在");
            }
            //判断验证码时间是否过期
            if(codeInSession.isExpried()){
                sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
                throw new ValidateCodeException("验证码已经过期");
            }
            //比对验证码
            if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
                throw new ValidateCodeException("验证码不匹配");
            }
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
        }
    
        public AuthenticationFailureHandler getAuthenticationFailureHandler() {
            return authenticationFailureHandler;
        }
    
        public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
            this.authenticationFailureHandler = authenticationFailureHandler;
        }
    
    
        public SecurityProperties getSecurityProperties() {
            return securityProperties;
        }
    
        public void setSecurityProperties(SecurityProperties securityProperties) {
            this.securityProperties = securityProperties;
        }
    }
    
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Autowired
        private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
    
        @Autowired
        private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
    
    
        @Autowired
        private SessionRegistry sessionRegistry;
    
        @Bean
        public SessionRegistry sessionRegistry() {
            return new SessionRegistryImpl();
        }
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
            //处理失败异常
            validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
            //把配置放进去
            validateCodeFilter.setSecurityProperties(securityProperties);
            //初始化方法
            validateCodeFilter.afterPropertiesSet();
    
            http
                    .csrf().disable()//关闭跨站防护
                    .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                    .and()
                        .formLogin() //表单登陆页
                        .loginPage("/login")//登陆页面
                        .loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
                        .successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
                        .failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
                    .and()
                        .sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
    
        }
    }
    

    配置文件

        code:
          #image下面的属性有width,height,length,expreIn,url
          image:
            length: 5
            url: /user/*
    

    验证码生成逻辑可配置

    public interface ValidateCodeGenerator {
        ImageCode generate(ServletWebRequest request);
    }
    
    public class ImageCodeGenerator implements ValidateCodeGenerator{
    
        @Autowired
        private SecurityProperties securityProperties;
    
        @Override
        public ImageCode generate(ServletWebRequest request) {
            //先从请求中获取如果获取不到从配置文件获取
            int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
            int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
    
            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);
            }
    
            String sRand = "";
            for (int i = 0; i < securityProperties.getCode().getImage().getLength(); i++) {
                String rand = String.valueOf(random.nextInt(10));
                sRand += 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, securityProperties.getCode().getImage().getExpreIn());
        }
    
        /**
         * 生成随机背景条纹
         *
         * @param fc
         * @param bc
         * @return
         */
        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);
        }
    
        public SecurityProperties getSecurityProperties() {
            return securityProperties;
        }
    
        public void setSecurityProperties(SecurityProperties securityProperties) {
            this.securityProperties = securityProperties;
        }
    }
    
    
    @Configuration
    public class ValidateCodeBeanConfig {
        @Autowired
        private SecurityProperties securityProperties;
    
        @Bean
        @ConditionalOnMissingBean(name = "imageCodeGenerator")//在spring加载bean时会先查找有没有名字为imageCodeGenerator
        public ValidateCodeGenerator imageCodeGenerator(){
            ImageCodeGenerator codeGenerator=new ImageCodeGenerator();
            codeGenerator.setSecurityProperties(securityProperties);
            return codeGenerator;
        }
    }
    
    @RestController
    public class ValidateCodeController {
    
    
        public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE";
    
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
    
    
        @Autowired
        private ValidateCodeGenerator imageCodeGenerator;
    
        @RequestMapping(value = "/code/image",method = RequestMethod.GET)
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            //生成验证码对象
            ImageCode imageCode=imageCodeGenerator.generate(new ServletWebRequest(request));
            //将ImageCode对象保存在session中
            sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
            //返回到页面
            ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
        }
    }
    

    如果想创建自己的验证码生成逻辑


    image.png
    /**
     * 从类可以重写生成验证码的逻辑替换默认逻辑
     */
    @Component(value = "imageCodeGenerator")
    public class demoImageCodeGenerator implements ValidateCodeGenerator {
        @Override
        public ImageCode generate(ServletWebRequest request) {
            System.out.printf("自定义验证码");
            return null;
        }
    }
    

    记住我功能实现

    基本原理

    image.png

    前端登陆页面传值名称必须叫emember-me

    <input  type="checkbox" value="true" name="remember-me" >记住密码
    

    配置可配置记住密码过期时间

    public class BrowserProperties {
    
        //自定义登陆页面
        private String loginPage = "/defaultLogin.html"; //如果用户没有配置登陆页面走默认
    
        //自定义处理登陆请求默认异步
        private LoginType loginType = LoginType.JSON;
    
        //设置记住密码过期时间默认1小时
        private int rememberMeSeconds = 3600;
    
    
        public String getLoginPage() {
            return loginPage;
        }
    
        public void setLoginPage(String loginPage) {
            this.loginPage = loginPage;
        }
    
        public LoginType getLoginType() {
            return loginType;
        }
    
        public void setLoginType(LoginType loginType) {
            this.loginType = loginType;
        }
    
        public int getRememberMeSeconds() {
            return rememberMeSeconds;
        }
    
        public void setRememberMeSeconds(int rememberMeSeconds) {
            this.rememberMeSeconds = rememberMeSeconds;
        }
    }
    
    
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        //自定义配置文件
        @Autowired
        private SecurityProperties securityProperties;
    
        //处理登陆成功
        @Autowired
        private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
    
        //处理登陆失败
        @Autowired
        private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
    
        //数据源
        @Autowired
        private DataSource dataSource;
    
        //登陆实现
        @Autowired
        private UserDetailsService userDetailsService;
    
    
        @Autowired
        private SessionRegistry sessionRegistry;
    
        @Bean
        public SessionRegistry sessionRegistry() {
            return new SessionRegistryImpl();
        }
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        //记住密码
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //启动自动创建persistent_logins表
            //tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //验证码拦截器
            ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
            //处理失败异常
            validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
            //把配置放进去
            validateCodeFilter.setSecurityProperties(securityProperties);
            //初始化方法
            validateCodeFilter.afterPropertiesSet();
    
            http
                    .csrf().disable()//关闭跨站防护
                    .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                        .and()
                    .formLogin() //表单登陆
                        .loginPage("/login")//登陆判断页面
                        .loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
                        .successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
                        .failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
                        .and()
                    .rememberMe() //记住密码
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效时间
                        .userDetailsService(userDetailsService)
                        .and()
                    .sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
    
        }
    }
    

    创建数据库表记录token与过期时间

    CREATE TABLE `persistent_logins` (
      `username` varchar(64) NOT NULL,
      `series` varchar(64) NOT NULL,
      `token` varchar(64) NOT NULL,
      `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
      PRIMARY KEY (`series`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    

    短信验证码

    短信验证码接口开发

    public class ValidateCode implements Serializable {
        //随机数
        private String code;
        //失效时间
        private LocalDateTime exireTime;
    
        public ValidateCode(String code, int exireIn) {
            this.code = code;
            //当前时间加上过期秒数
            this.exireTime = LocalDateTime.now().plusSeconds(exireIn);
        }
    
        public ValidateCode(String code, LocalDateTime exireTime) {
            this.code = code;
            //当前时间加上过期秒数
            this.exireTime = exireTime;
    
        }
        //判断时间是否过期
        public boolean isExpried(){
            return LocalDateTime.now().isAfter(exireTime);
        }
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public LocalDateTime getExireTime() {
            return exireTime;
        }
    
        public void setExireTime(LocalDateTime exireTime) {
            this.exireTime = exireTime;
        }
    }
    
    

    短信验证码生成

    @Component("smsCodeGenerator")
    public class SmsCodeGenerator implements ValidateCodeGenerator{
    
        @Autowired
        private SecurityProperties securityProperties;
    
        /**
         * 生成验证码
         * @param request
         * @return
         */
        @Override
        public ValidateCode generate(ServletWebRequest request) {
            String code= RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getLength());
    
            return new ValidateCode(code,securityProperties.getCode().getSms().getExpreIn());
        }
    
    
    }
    

    短信验证码发送

    //短信验证码发送
    public interface SmsCodeSender {
        void send(String mobile,String code);
    }
    
    /**
     * 默认短信发送器
     */
    public class DefaultSmsCodeSender implements SmsCodeSender{
    
        private Logger logger = LoggerFactory.getLogger(getClass());
        @Override
        public void send(String mobile, String code) {
            logger.info("向手机"+mobile+"发送短信验证码"+code);
        }
    }
    

    接口生成

    @RestController
    public class ValidateCodeController {
    
    
        public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE";
    
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
    
    
        @Autowired
        private ValidateCodeGenerator imageCodeGenerator;
    
        @Autowired
        private ValidateCodeGenerator smsCodeGenerator;
    
        //发送短信
        @Autowired
        private SmsCodeSender smsCodeSender;
    
        //图形验证码
        @RequestMapping(value = "/code/image",method = RequestMethod.GET)
        public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
            //生成验证码对象
            ImageCode imageCode= (ImageCode) imageCodeGenerator.generate(new ServletWebRequest(request));
            //将ImageCode对象保存在session中
            sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
            //返回到页面
            ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
        }
    
        //短信验证码
        @RequestMapping(value = "/code/sms",method = RequestMethod.GET)
        public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
            //生成验证码对象
            ValidateCode validateCode=smsCodeGenerator.generate(new ServletWebRequest(request));
            //将validateCode对象保存在session中
            sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,validateCode);
            String mobile= ServletRequestUtils.getRequiredStringParameter(request,"mobile");
            //发送短信
            smsCodeSender.send(mobile,validateCode.getCode());
    
        }
    }
    

    短信登录开发

    image.png
    
    public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    
        private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    
        // ~ Instance fields
        // ================================================================================================
    
        private final Object principal;
    
        // ~ Constructors
        // ===================================================================================================
    
        /**
         * This constructor can be safely used by any code that wishes to create a
         * <code>UsernamePasswordAuthenticationToken</code>, as the {@link #isAuthenticated()}
         * will return <code>false</code>.
         *
         */
        public SmsCodeAuthenticationToken(String mobile) {
            super(null);
            this.principal = mobile;
            setAuthenticated(false);
        }
    
        /**
         * This constructor should only be used by <code>AuthenticationManager</code> or
         * <code>AuthenticationProvider</code> implementations that are satisfied with
         * producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
         * authentication token.
         *
         * @param principal
         * @param authorities
         */
        public SmsCodeAuthenticationToken(Object principal,
                                          Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            super.setAuthenticated(true); // must use super, as we override
        }
    
        // ~ Methods
        // ========================================================================================================
    
        public Object getCredentials() {
            return null;
        }
    
        public Object getPrincipal() {
            return this.principal;
        }
    
        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();
        }
    }
    
    
    public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    
        public static final String GUOSH_FORM_MOBILE_KEY = "mobile";
    
        private String mobileParameter = GUOSH_FORM_MOBILE_KEY;
        private boolean postOnly = true;
    
        public SmsCodeAuthenticationFilter() {
            super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
        }
    
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            if (this.postOnly && !request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
            } else {
                String mobile = this.obtainMobile(request);
    
                if (mobile == null) {
                    mobile = "";
                }
                mobile = mobile.trim();
                SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
                this.setDetails(request, authRequest);
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
    
        /**
         * 获取手机号
         */
        protected String obtainMobile(HttpServletRequest request) {
            return request.getParameter(this.mobileParameter);
        }
    
        protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
            authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
        }
    
        public void setMobileParameter(String mobileParameter) {
            Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
            this.mobileParameter = mobileParameter;
        }
    
    
    
        public void setPostOnly(boolean postOnly) {
            this.postOnly = postOnly;
        }
    
        public final String getMobileParameter() {
            return this.mobileParameter;
        }
    
    
    }
    
    
    
    public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    
        private UserDetailsService userDetailsService;
    
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            SmsCodeAuthenticationToken authenticationToken=(SmsCodeAuthenticationToken)authentication;
            UserDetails user=userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
            if(user==null){
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            }
            SmsCodeAuthenticationToken authenticationResult=new SmsCodeAuthenticationToken(user,user.getAuthorities());
            authenticationResult.setDetails(authenticationToken.getDetails());
            return authenticationResult;
        }
    
        @Override
        public boolean supports(Class<?> aClass) {
            return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
        }
    
        public UserDetailsService getUserDetailsService() {
            return userDetailsService;
        }
    
        public void setUserDetailsService(UserDetailsService userDetailsService) {
            this.userDetailsService = userDetailsService;
        }
    }
    
    @Component
    public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
        @Autowired
        private AuthenticationSuccessHandler authenticationSuccessHandler;
    
        @Autowired
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        public void configure(HttpSecurity http) throws Exception {
            //短信验证过滤器
            SmsCodeAuthenticationFilter smsCodeAuthenticationFilter=new SmsCodeAuthenticationFilter();
            smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
            //成功处理器
            smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
            //失败处理器
            smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
    
    
            SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
            smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
    
            http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        }
    
    }
    
    
    package com.guosh.security.core.validate.code.web.filter;
    
    import com.guosh.security.core.properties.SecurityProperties;
    import com.guosh.security.core.validate.code.ValidateCode;
    import com.guosh.security.core.validate.code.image.ImageCode;
    import com.guosh.security.core.validate.code.web.exception.ValidateCodeException;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.social.connect.web.HttpSessionSessionStrategy;
    import org.springframework.social.connect.web.SessionStrategy;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.bind.ServletRequestBindingException;
    import org.springframework.web.bind.ServletRequestUtils;
    import org.springframework.web.context.request.ServletWebRequest;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.Set;
    
    public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {
    
        //登陆失败的处理器
        private AuthenticationFailureHandler authenticationFailureHandler;
    
        private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy();
    
        private Set<String>urls = new HashSet<>();
    
        private SecurityProperties securityProperties;
    
        private AntPathMatcher antPathMatcher=new AntPathMatcher();
    
        //初始化加载
        @Override
        public void afterPropertiesSet() throws ServletException {
            super.afterPropertiesSet();
            //把要拦截的分割转换数组
            if(StringUtils.isNotBlank(securityProperties.getCode().getSms().getUrl())){
                String[]configUrls=StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(),",");
                for (String configUrl:configUrls) {
                    urls.add(configUrl);
                }
            }
            urls.add("/authentication/mobile");
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
                boolean action = false;
                //用antPathMatcher工具栏判断路径是否符合
                for (String url : urls) {
                    if (antPathMatcher.match(url, request.getServletPath())) {
                        action = true;
                    }
                }
    
                //必须是登陆请求并且是post请求
                if (action) {
                    try {
                        //校验验证码
                        validate(new ServletWebRequest(request));
                    } catch (ValidateCodeException e) {
                        authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                        return;
                    }
                }
    
            filterChain.doFilter(request,response);
        }
    
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            //从session获取验证码
            ValidateCode codeInSession= (ValidateCode) sessionStrategy.getAttribute(request, "SESSION_KEY_FOR_CODE_SMS");
            //从请求里获取验证码
            String codeInRequest=ServletRequestUtils.getStringParameter(request.getRequest(),"smsCode");
    
            if(StringUtils.isBlank(codeInRequest)){
                throw new ValidateCodeException("验证码不能为空");
            }
            if(codeInSession == null){
                throw new ValidateCodeException("验证码不存在");
            }
            //判断验证码时间是否过期
            if(codeInSession.isExpried()){
                sessionStrategy.removeAttribute(request, "SESSION_KEY_FOR_CODE_SMS");
                throw new ValidateCodeException("验证码已经过期");
            }
            //比对验证码
            if(!StringUtils.equalsIgnoreCase(codeInSession.getCode(),codeInRequest)){
                throw new ValidateCodeException("验证码不匹配");
            }
            sessionStrategy.removeAttribute(request, "SESSION_KEY_FOR_CODE_SMS");
        }
    
        public AuthenticationFailureHandler getAuthenticationFailureHandler() {
            return authenticationFailureHandler;
        }
    
        public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
            this.authenticationFailureHandler = authenticationFailureHandler;
        }
    
    
        public SecurityProperties getSecurityProperties() {
            return securityProperties;
        }
    
        public void setSecurityProperties(SecurityProperties securityProperties) {
            this.securityProperties = securityProperties;
        }
    }
    
    
    @Configuration
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        //自定义配置文件
        @Autowired
        private SecurityProperties securityProperties;
    
        //处理登陆成功
        @Autowired
        private BrowserAuthenticationSuccessHandler browserAuthenticationSuccessHandler;
    
        //处理登陆失败
        @Autowired
        private BrowserAuthenticationFailureHandler browserAuthenticationFailureHandler;
    
    
        @Autowired
        private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    
        //数据源
        @Autowired
        private DataSource dataSource;
    
        //登陆实现
        @Autowired
        private UserDetailsService userDetailsService;
    
    
        @Autowired
        private SessionRegistry sessionRegistry;
    
        @Bean
        public SessionRegistry sessionRegistry() {
            return new SessionRegistryImpl();
        }
    
        //密码加密
        @Bean
        public PasswordEncoder passwordEncoder(){
            return new BCryptPasswordEncoder();
        }
    
        //记住密码
        @Bean
        public PersistentTokenRepository persistentTokenRepository(){
            JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
            tokenRepository.setDataSource(dataSource);
            //启动自动创建persistent_logins表
            //tokenRepository.setCreateTableOnStartup(true);
            return tokenRepository;
        }
    
    
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            //验证码拦截器
            ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter();
            //处理失败异常
            validateCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
            //把配置放进去
            validateCodeFilter.setSecurityProperties(securityProperties);
            //初始化方法
            validateCodeFilter.afterPropertiesSet();
    
            //验证码拦截器
            SmsCodeFilter smsCodeFilter=new SmsCodeFilter();
            //处理失败异常
            smsCodeFilter.setAuthenticationFailureHandler(browserAuthenticationFailureHandler);
            //把配置放进去
            smsCodeFilter.setSecurityProperties(securityProperties);
            //初始化方法
            smsCodeFilter.afterPropertiesSet();
    
    
            http
                    .csrf().disable()//关闭跨站防护
                    .apply(smsCodeAuthenticationSecurityConfig) //短信登陆配置
                        .and()
                    .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
                    .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)//在登陆拦截之前添加验证码拦截器
                    .authorizeRequests()//下面授权配置
                        .antMatchers("/login",securityProperties.getBrowser().getLoginPage(),"/code/*").permitAll()//login请求除外不需要认证
                        .anyRequest().authenticated()//所有请求都需要身份认证
                        .and()
                    .formLogin() //表单登陆
                        .loginPage("/login")//登陆判断页面
                        .loginProcessingUrl("/authentication/form")//自定义form表单登陆提交地址默认是/login
                        .successHandler(browserAuthenticationSuccessHandler)//自定义登陆成功后返回json信息
                        .failureHandler(browserAuthenticationFailureHandler)//自定义登陆失败返回json
                        .and()
                    .rememberMe() //记住密码
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) //失效时间
                        .userDetailsService(userDetailsService)
                        .and()
                    .sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry).expiredUrl("/login");//用户只能登陆一次
    
        }
    }
    

    相关文章

      网友评论

        本文标题:Spring Security项目基于表单登陆(三)

        本文链接:https://www.haomeiwen.com/subject/sissbqtx.html