SpringSecurity:整合验证码

x33g5p2x  于2021-11-30 转载在 Spring  
字(11.8k)|赞(0)|评价(0)|浏览(332)

SpringSecurity:整合验证码

添加验证码

验证码 Servlet,这里大家不需要关心内部怎么实现的,我也是百度直接copy的

@Component
public class VerifyServlet extends HttpServlet {

    private static final long serialVersionUID = -5051097528828603895L;

    /** * 验证码图片的宽度。 */
    private int width = 100;

    /** * 验证码图片的高度。 */
    private int height = 30;

    /** * 验证码字符个数 */
    private int codeCount = 4;

    /** * 字体高度 */
    private int fontHeight;

    /** * 干扰线数量 */
    private int interLine = 16;

    /** * 第一个字符的x轴值,因为后面的字符坐标依次递增,所以它们的x轴值是codeX的倍数 */
    private int codeX;

    /** * codeY ,验证字符的y轴值,因为并行所以值一样 */
    private int codeY;

    /** * codeSequence 表示字符允许出现的序列值 */
    char[] codeSequence = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
            'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
            'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

    /** * 初始化验证图片属性 */
    @Override
    public void init() throws ServletException {
        // 从web.xml中获取初始信息
        // 宽度
        String strWidth = this.getInitParameter("width");
        // 高度
        String strHeight = this.getInitParameter("height");
        // 字符个数
        String strCodeCount = this.getInitParameter("codeCount");
        // 将配置的信息转换成数值
        try {
            if (strWidth != null && strWidth.length() != 0) {
                width = Integer.parseInt(strWidth);
            }
            if (strHeight != null && strHeight.length() != 0) {
                height = Integer.parseInt(strHeight);
            }
            if (strCodeCount != null && strCodeCount.length() != 0) {
                codeCount = Integer.parseInt(strCodeCount);
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        }
        //width-4 除去左右多余的位置,使验证码更加集中显示,减得越多越集中。
        //codeCount+1 //等比分配显示的宽度,包括左右两边的空格
        codeX = (width-4) / (codeCount+1);
        //height - 10 集中显示验证码
        fontHeight = height - 10;
        codeY = height - 7;
    }

    /** * @param request * @param response * @throws ServletException * @throws java.io.IOException */
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, java.io.IOException {
        // 定义图像buffer
        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D gd = buffImg.createGraphics();
        // 创建一个随机数生成器类
        Random random = new Random();
        // 将图像填充为白色
        gd.setColor(Color.LIGHT_GRAY);
        gd.fillRect(0, 0, width, height);
        // 创建字体,字体的大小应该根据图片的高度来定。
        Font font = new Font("Times New Roman", Font.PLAIN, fontHeight);
        // 设置字体。
        gd.setFont(font);
        // 画边框。
        gd.setColor(Color.BLACK);
        gd.drawRect(0, 0, width - 1, height - 1);
        // 随机产生16条干扰线,使图象中的认证码不易被其它程序探测到。
        gd.setColor(Color.gray);
        for (int i = 0; i < interLine; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            gd.drawLine(x, y, x + xl, y + yl);
        }
        // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
        StringBuffer randomCode = new StringBuffer();
        int red = 0, green = 0, blue = 0;
        // 随机产生codeCount数字的验证码。
        for (int i = 0; i < codeCount; i++) {
            // 得到随机产生的验证码数字。
            String strRand = String.valueOf(codeSequence[random.nextInt(36)]);
            // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
            red = random.nextInt(255);
            green = random.nextInt(255);
            blue = random.nextInt(255);
            // 用随机产生的颜色将验证码绘制到图像中。
            gd.setColor(new Color(red,green,blue));
            gd.drawString(strRand, (i + 1) * codeX, codeY);
            // 将产生的四个随机数组合在一起。
            randomCode.append(strRand);
        }
        // 将四位数字的验证码保存到Session中。
        HttpSession session = request.getSession();
        session.setAttribute("validateCode", randomCode.toString());
        // 禁止图像缓存。
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        response.setContentType("image/jpeg");
        // 将图像输出到Servlet输出流中。
        ServletOutputStream sos = response.getOutputStream();
        ImageIO.write(buffImg, "jpeg", sos);
        sos.close();
    }
}

然后把VerifyServlet注入到spring容器中

@Configuration
public class VerifyConfig {

    /** * 注入验证码servlet */
    @Bean
    public ServletRegistrationBean indexServletRegistration() {
        ServletRegistrationBean registration = new ServletRegistrationBean(new VerifyServlet());
        registration.addUrlMappings("/getVerifyCode");
        return registration;
    }
}

修改login.html页面加上验证码字段

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登陆</title>
</head>
<body>
<h1>登陆</h1>
<form method="post" action="/login">
    <div>
        用户名:<input type="text" name="username">
    </div>
    <div>
        密码:<input type="password" name="password">
    </div>
    <div>
        <input type="text" class="form-control" name="verifyCode" required="required" placeholder="验证码">
        <img src="getVerifyCode" title="看不清,请点我" onclick="refresh(this)" onmouseover="mouseover(this)" />
    </div>
    <div>
        <label><input type="checkbox" name="remember-me"/>自动登录</label>
        <button type="submit">立即登陆</button>
    </div>
</form>
<script> function refresh(obj) { obj.src = "getVerifyCode?" + Math.random(); } function mouseover(obj) { obj.style.cursor = "pointer"; } </script>
</body>
</html>

最后修改WebSecurityConfig代码

不要忘记在 WebSecurityConfig 中允许该 Url 的匿名访问,不然没有登录是没有办法访问的:

实现验证码校验的三种方式

Ajax验证

Ajax校验的思路就是其实就是表单提交前先发个 HTTP 请求验证验证码,验证成功后再验证用户名和密码

过滤器验证

过滤器验证的思路:在 Spring Security 处理登录验证请求前,验证验证码,如果正确,放行;如果不正确,调到异常

编写验证码过滤器:

自定义一个过滤器,实现 OncePerRequestFilter (该 Filter 保证每次请求一定会过滤),在 isProtectedUrl() 方法中拦截了 POST 方式的 /login 请求。

逻辑处理中从 request 中取出验证码,并进行验证,如果验证成功,放行;验证失败,手动生成异常。

public class VerifyFilter extends OncePerRequestFilter {
    private static final PathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if(isProtectedUrl(request)) {
            String verifyCode = request.getParameter("verifyCode");
            if(!validateVerify(verifyCode)) {
                //手动设置异常
                request.getSession().setAttribute("SPRING_SECURITY_LAST_EXCEPTION",new DisabledException("验证码输入错误"));
                // 转发到错误Url
                request.getRequestDispatcher("/login/error").forward(request,response);
            } else {
                filterChain.doFilter(request,response);
            }
        } else {
            filterChain.doFilter(request,response);
        }

    }

    private boolean validateVerify(String inputVerify) {
        //获取当前线程绑定的request对象
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        // 不分区大小写
        // 这个validateCode是在servlet中存入session的名字
        String validateCode = ((String) request.getSession().getAttribute("validateCode")).toLowerCase();
        inputVerify = inputVerify.toLowerCase();

        System.out.println("验证码:" + validateCode + "用户输入:" + inputVerify);
        return validateCode.equals(inputVerify);
    }

    // 拦截 /login的POST请求
    private boolean isProtectedUrl(HttpServletRequest request) {
        return "POST".equals(request.getMethod()) && PATH_MATCHER.match("/login", request.getServletPath());
    }
}

注入过滤器:

修改 WebSecurityConfig 的 configure 方法,添加一个 addFilterBefore() ,具有两个参数,作用是在参数二之前执行参数一设置的过滤器。

Spring Security 对于用户名/密码登录方式是通过 UsernamePasswordAuthenticationFilter 处理的,我们在它之前执行验证码过滤器即可。

测试:

输入一个错误的验证码

Spring Security 验证

虽然使用过滤器验证实现了验证码功能,可是他这个和ajax验证差别不大

  • ajax是提交前发送一个请求,请求成功就提交,否则不提交
  • 过滤器是先校验验证码,验证成功就让Spring Security验证用户名和密码,校验失败就产生异常

如果我们要做的需求是用户登录是需要多个验证字段,不单单是用户名和密码,那么使用过滤器会让逻辑变得复杂,这时候可以考虑自定义 Spring Security 的验证逻辑了

WebAuthenticationDetails:

/** * 获取用户登录时携带的额外信息 */
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
    private static final long serialVersionUID = 6975601077710753878L;
    private final String verifyCode;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        // verifyCode为页面中验证码的name
        verifyCode = request.getParameter("verifyCode");
    }

    public String getVerifyCode() {
        return this.verifyCode;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(super.toString()).append("; VerifyCode: ").append(this.getVerifyCode());
        return sb.toString();
    }
}

AuthenticationDetailsSource:

自定义了WebAuthenticationDetails,我i们还需要将其放入 AuthenticationDetailsSource 中来替换原本的 WebAuthenticationDetails,因此还得实现自定义 AuthenticationDetailsSource

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/** * 该接口用于在Spring Security登录过程中对用户的登录信息的详细信息进行填充 */
@Component
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
   
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new CustomWebAuthenticationDetails(request);
    }
    
}

该类内容将原本的 WebAuthenticationDetails 替换为了我们的 CustomWebAuthenticationDetails

然后我们将 CustomAuthenticationDetailsSource 注入Spring Security中,替换掉默认的 AuthenticationDetailsSource

修改 WebSecurityConfig,将其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法来指定它。

@Autowired
    private CustomAuthenticationDetailsSource authenticationDetailsSource;

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 如果有允许匿名的url,填在下面
                .antMatchers("/getVerifyCode").permitAll()
                .anyRequest().authenticated()
                .and()
                // 设置登陆页
                .formLogin().loginPage("/login")
                // 登录失败Url
                .failureUrl("/login/error")
                // 设置登陆成功页
                .defaultSuccessUrl("/").permitAll()
                // 自定义登陆用户名和密码参数,默认为username和password
// .usernameParameter("username")
// .passwordParameter("password")
                // 指定authenticationDetailsSource
                .authenticationDetailsSource(authenticationDetailsSource)
                .and()
// .addFilterBefore(new VerifyFilter(), UsernamePasswordAuthenticationFilter.class)
                .logout().permitAll()
                //自动登录
                .and().rememberMe()
                .tokenRepository(persistentTokenRepository())
                //有效时间:单位s
                .tokenValiditySeconds(60)
                .userDetailsService(userDetailsService);

        // 关闭CSRF跨域
        http.csrf().disable();
    }

AuthenticationProvider:

至此我们通过自定义WebAuthenticationDetailsAuthenticationDetailsSource将验证码和用户名、密码一起带入了Spring Security中,下面我们需要将它取出来。

这里需要我们自定义AuthenticationProvider,需要注意的是,如果是我们自己实现AuthenticationProvider,那么我们就需要自己做密码校验了。

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // 获取用户输入的用户名和密码
        String inputName = authentication.getName();
        String inputPassword = authentication.getCredentials().toString();

        CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();

        String verifyCode = details.getVerifyCode();
        if(!validateVerify(verifyCode)) {
            throw new DisabledException("验证码输入错误");
        }

        // userDetails为数据库中查询到的用户信息
        UserDetails userDetails = customUserDetailsService.loadUserByUsername(inputName);

        // 如果是自定义AuthenticationProvider,需要手动密码校验
        if(!userDetails.getPassword().equals(inputPassword)) {
            throw new BadCredentialsException("密码错误");
        }

        return new UsernamePasswordAuthenticationToken(inputName, inputPassword, userDetails.getAuthorities());
    }

    private boolean validateVerify(String inputVerify) {
        //获取当前线程绑定的request对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // 不分区大小写
        // 这个validateCode是在servlet中存入session的名字
        String validateCode = ((String) request.getSession().getAttribute("validateCode")).toLowerCase();
        inputVerify = inputVerify.toLowerCase();

        System.out.println("验证码:" + validateCode + "用户输入:" + inputVerify);

        return validateCode.equals(inputVerify);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 这里不要忘记,和UsernamePasswordAuthenticationToken比较
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

最后在 WebSecurityConfig 中将其注入,并在 config 方法中通过 auth.authenticationProvider() 指定使用。

@Autowired
    private CustomAuthenticationProvider customAuthenticationProvider;

@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //使用下面的方法替换掉默认的userDetailsService
// auth.userDetailsService(userDetailsService).passwordEncoder(new PasswordEncoder() {
// @Override
// public String encode(CharSequence charSequence) {
// return charSequence.toString();
// }
//
// @Override
// public boolean matches(CharSequence charSequence, String s) {
// return s.equals(charSequence.toString());
// }
// });
        auth.authenticationProvider(customAuthenticationProvider);
    }

测试:

相关文章

微信公众号

最新文章

更多