写点什么

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}; }
}
复制代码


创建验证码图片的步骤


  1. 创建图像类

  2. 获取画笔,在 Image 上画内容

  3. 设置图片背景色

  4. 创建 Font,书写文字

  5. 文字内容保存在 Session 中

  6. 返回 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
用户头像

小白

关注

QA 2019.08.05 加入

微信号JingnanSJ或者公众号RiemannHypo获取异步和图灵系列书籍

评论

发布
暂无评论
Spring 全家桶之 Spring Security(五)_8月月更_小白_InfoQ写作社区