木心

毕竟几人真得鹿,不知终日梦为鱼

导航

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 实战》-- 陈木鑫

posted on 2020-04-15 10:54  wenbin_ouyang  阅读(837)  评论(0编辑  收藏  举报