Spring 全家桶之 Spring Security(五)
 作者:小白
- 2022 年 8 月 23 日 上海
- 本文字数:5562 字 - 阅读完需:约 18 分钟 
一、自定义验证码
在 Controller 包中创建 CaptchaController,用于生成验证码图像,返回验证码图片,并保存图片中的验证码在 Session 中,方便登录时校验验证码
@Controller@RequestMapping("/captcha")public class CaptchaController {
    // 定义一个值,用来生成验证码的图片    // 宽度    private int width = 120;
    // 高度    private int height = 30;
    // 图片内容在图片的起始位置    private int drawY = 22;
    // 文字的间隔    private int space = 22;
    // 验证码有几个文字    private int charCount = 4;
    // 验证码内容的数组    private String[] chars = {"A", "B", "C", "D", "E", "F", "G", "0", "1", "2", "3", "4", "5", "6", "8"};
    // 生成验证码内容,在一个图片上写入文字    @GetMapping("/create")    public void createCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {        // 需要在内存中绘制图片,向图片中写入文字,将绘制好的图片响应给请求
        // 创建一个背景透明的图片        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // 获取画笔        Graphics graphics = image.getGraphics();
        // 设置背景色        graphics.setColor(Color.white);
        // 给画板涂成白色        graphics.fillRect(0, 0, width, height);
        // 画内容        // 创建一个字体        Font font = new Font("宋体",Font.BOLD,18);        graphics.setFont(font);        graphics.setColor(Color.black);        // 在画布上写字        // graphics.drawString("中",10,drawY);        // 保存验证码的值,登录时校验        StringBuffer buffer = new StringBuffer();        int random = 0;        int len = chars.length;        for (int i = 0; i < charCount ; i++) {            random = new Random().nextInt(len);            buffer.append(chars[random]);            graphics.setColor(makeColor());            graphics.drawString(chars[random],(i+1)*space, drawY);        }
        // 绘制干扰线        for (int i = 0; i < 4; i++) {            graphics.setColor(makeColor());            int[] lines = makeLine();            graphics.drawLine(lines[0],lines[1],lines[2],lines[3]);        }
        // 设置取消缓存        response.setHeader("Pragma","no-cache");        response.setHeader("Cache-Control","no-cache");        response.setDateHeader("Expires",0);        response.setContentType("image/png");        // 生成的验证码存在session中        HttpSession session = request.getSession();        session.setAttribute("captcha",buffer.toString());
        ServletOutputStream outputStream = response.getOutputStream();
        /**         *         */        ImageIO.write(image, "png", outputStream);
        outputStream.flush();        outputStream.close();
    }
    // 获取随机颜色    private Color makeColor(){        Random random = new Random();        int r = random.nextInt(255);        int g = random.nextInt(255);        int b = random.nextInt(255);        return new Color(r,g,b);
    }
    // 获取干扰线    private int[] makeLine(){        Random random = new Random();        int x1 = random.nextInt(width);        int y1 = random.nextInt(height);        int x2 = random.nextInt(width);        int y2 = random.nextInt(height);        return new int[]{x1,x2,y1,y2};    }
}
复制代码
 创建验证码图片的步骤
- 创建图像类 
- 获取画笔,在 Image 上画内容 
- 设置图片背景色 
- 创建 Font,书写文字 
- 文字内容保存在 Session 中 
- 返回 Image 在自定义的安全配置中添加验证码访问权限 
@Overrideprotected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()            // 增加js静态资源的访问权限,验证码访问权限,登录首页访问权限            .antMatchers("/login.html","/index.html","/login","/js/**","/captcha/**").permitAll()            // 给url配置角色访问权限            .antMatchers("/access/user").hasRole("USER")            .antMatchers("/access/read").hasRole("READ")            .antMatchers("/access/admin").hasRole("ADMIN")            .anyRequest().authenticated()            .and()            .formLogin()            .successHandler(custSuccessHandler) //执行验证成功的handler            .failureHandler(custFailureHandler) // 执行验证失败后的handler            // 指定使用自定义的登录界面            .loginPage("/login.html")            .loginProcessingUrl("/login")            .and()            .csrf().disable();}
复制代码
 在登录页面中增加验证码图片和输入框
<div id="validatePanel" class="item" style="width: 137px;">    <input type="text" id="captcha" placeholder="请输入验证码" maxlength="4">    <a href="javascript:void(0)" onclick="refreshCaptcha()"><img id="refreshCaptcha" class="validateImg"  src="/captcha/create" ></a></div>
复制代码
 在 js 中增加刷新验证码的函数,点击验证码图片即可刷新验证码
<script>function refreshCaptcha(){    // 刷新验证码    var url = "/captcha/create?t=" + new Date();    $("#refreshCaptcha").attr("src",url);}</script>
复制代码
  
 修改 ajax 请求,增加发送验证码的内容
<script type="text/javascript">    $(function(){        //juqery的入口函数        $("#btnLogin").click(function(){            var uname = $("#username").val();            var pwd = $("#password").val();            // 获取输入的验证码            var captcha = $("#captcha").val();            $.ajax({                // 发送ajax请求显示状态已取消,通过添加一下代码可以成功获取响应                async: false,                url:"/login",                type:"POST",                data:{                    "username":uname,                    "password":pwd,                    "captcha":captcha                },                dataType:"json",                success:function(resp){                    alert(resp.code + " " + resp.msg)                }            })        })    });</script>    
复制代码
 过滤器验证验证码内容,应该在验证用户名密码之前验证发送的验证码内容,在 Spring Security 的用户名密码过滤器 UsernamePasswordAuthenticationFilter 之前自定义一个过滤器
先在 common 包中定义一个异常类 VerificationException
public class VerificationException extends AuthenticationException {
    public VerificationException(String detail, Throwable ex) {        super(detail, ex);    }
    public VerificationException(String detail) {        super(detail);    }
    public VerificationException(){        super("验证码错误,请重新输入");    }}
复制代码
 修改 CustFailureHandler,增加 Result 属性,在校验验证码失败时输出 json 格式的错误信息
@Componentpublic class CustFailureHandler implements AuthenticationFailureHandler {
    private Result result;
    public Result getResult() {        return result;    }
    public void setResult(Result result) {        this.result = result;    }
    @Override    public void onAuthenticationFailure(HttpServletRequest request,                                        HttpServletResponse response,                                        AuthenticationException e) throws IOException {        //当框架验证用户信息失败时执行的方法        response.setContentType("text/json;charset=utf-8");
        if( result == null){            Result  localResult  = new Result();            localResult.setCode(1);            localResult.setError(1001);            localResult.setMsg("登录失败");            result = localResult;        }
        OutputStream out = response.getOutputStream();        ObjectMapper om = new ObjectMapper();        om.writeValue(out,result );        out.flush();        out.close();
    }}
复制代码
 接着在 common 包中定义一个 Filter
@Componentpublic class VerificationCaptchaFilter extends OncePerRequestFilter {
    @Resource    private CustFailureHandler failureHandler;
    @Override    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 只有login()才需要这个过滤器        String requestURI = httpServletRequest.getRequestURI();        if (!"/login".equals(requestURI)){            // 过滤器正常执行,非登录操作不参与验证码操作            filterChain.doFilter(httpServletRequest,httpServletResponse);        } else {
            try {                // 登录操作,需要验证码                verificationCode(httpServletRequest);                // 如果 验证通过                filterChain.doFilter(httpServletRequest, httpServletResponse);            } catch (VerificationException e) {
                Result result = new Result();                result.setCode(1);                result.setError(1002);                result.setMsg("验证码错误");                failureHandler.setResult(result);                failureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);            }        }    }
    private void verificationCode(HttpServletRequest httpServletRequest) throws VerificationException {        HttpSession session = httpServletRequest.getSession();
        String requestCode = httpServletRequest.getParameter("captcha");        String sessionCode = "";
        String captcha = (String) session.getAttribute("captcha");        if (captcha != null){            sessionCode = captcha;        }        System.out.println(requestCode);
        if (!StringUtils.isEmpty(sessionCode)){            session.removeAttribute("captcha");        }
        if (StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(sessionCode) || !requestCode.equalsIgnoreCase(sessionCode)){            throw new VerificationException();        }    }
}
复制代码
 修改自定义的安全配置,将自定义的过滤器加入到过滤器链中,放在校验用户名密码之前
@Configuration@EnableWebSecuritypublic class CustSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource    private UserDetailsService userDetailsService;
    @Resource    private CustSuccessHandler custSuccessHandler;
    @Resource    private CustFailureHandler custFailureHandler;
    @Resource    private VerificationCaptchaFilter captchaFilter;
    @Override    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());    }
    @Override    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()                // 增加js静态资源的访问权限                .antMatchers("/login.html","/index.html","/login","/js/**","/captcha/**").permitAll()                // 给url配置角色访问权限                .antMatchers("/access/user").hasRole("USER")                .antMatchers("/access/read").hasRole("READ")                .antMatchers("/access/admin").hasRole("ADMIN")                .anyRequest().authenticated()                .and()                .formLogin()                .successHandler(custSuccessHandler) //执行验证成功的handler                .failureHandler(custFailureHandler) // 执行验证失败后的handler                // 指定使用自定义的登录界面                .loginPage("/login.html")                .loginProcessingUrl("/login")                .and()                .csrf().disable();
        // 在框架的过滤器Chain中加入自定义的过滤器        http.addFilterBefore(captchaFilter,UsernamePasswordAuthenticationFilter.class);    }}
复制代码
 重新启动应用
 
 验证码生效
系列完结,撒花🎉!!!
划线
评论
复制
发布于: 刚刚阅读数: 3
版权声明: 本文为 InfoQ 作者【小白】的原创文章。
原文链接:【http://xie.infoq.cn/article/3993cbb132958fede4e385737】。文章转载请联系作者。

小白
关注
QA 2019.08.05 加入
微信号JingnanSJ或者公众号RiemannHypo获取异步和图灵系列书籍









 
    
评论