图片验证码
在登录界面图形添加验证码
- 开发生成图像验证码接口
- 在认证流程中加入图像验证码校验
- 重构代码
开发生成图像验证码接口
思路:
* 根据随机数生成图片
* 将随机数存到session中
* 将生成的图片写入响应中
这里相当于功能,生成图片什么的不记录了。网上一大堆,记录下这里的一些代码思路
由于是公用的,把该服务写在core中
图片验证码信息类
package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* 图形验证码
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/3 22:44
*/
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime; // 过期时间
/**
* @param image
* @param code
* @param expireIn 过期时间,单位秒
*/
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
// 是否过期
public boolean isExpried() {
return this.expireTime.isBefore(LocalDateTime.now());
}
验证码服务
package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
/**
* 验证码服务
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/3 22:48
*/
@RestController
public class ValidateCodeController {
private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 这里又使用了spring的工具类来操作session
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
response.setContentType("image/jpeg");
//禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) throws IOException {
String code = RandomStringUtils.randomAlphanumeric(4);
BufferedImage image = createImageCode(80, 40, code);
return new ImageCode(image, code, 60);
}
需要把该服务路径在 cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig
中配置放行。
在认证流程中加入图像验证码校验
步骤:
由之前的源码的探索发现,只要把过滤器添加到spring现有的过滤器链上就可以了;
- 编写验证码过滤器
- 放在UsernamePasswordAuthenticationFilter过滤器之前
package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
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;
/**
* 图片验证码验证过滤器
* OncePerRequestFilter spring提供的,保证在一个请求中只会被调用一次
* @author : zhuqiang
* @version : V1.0
* @date : 2018/8/3 23:24
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
// 在初始化本类的地方进行注入
// 一般在配置security http的地方进行添加过滤器
private AuthenticationFailureHandler failureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 为登录请求,并且为post请求
if (StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
try {
validate(request);
} catch (ValidateCodeException e) {
failureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
filterChain.doFilter(request, response);
}
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 拿到之前存储的imageCode信息
ServletWebRequest swr = new ServletWebRequest(request);
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
// 又是一个spring中的工具类,
// 试问一下,如果不看源码怎么可能知道有这些工具类可用?
String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getFailureHandler() {
return failureHandler;
}
public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
this.failureHandler = failureHandler;
}
}
把过滤器添加到现有认证流程中
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
http
// 由源码得知,在最前面的是UsernamePasswordAuthenticationFilter
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
// 定义表单登录 - 身份认证的方式
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
还需要注意的一个地方就是myAuthenticationFailureHandler中。因为失败会调用这个处理器;
这里和视频中演示的不一样。不会再把异常信息打印到前段页面了。
后补:视频中不知道什么时候把LoginType变成了json类型,所以会抛出异常
if (securityProperties.getBrowser().getLoginType() == LoginType.JSON) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.getWriter().write(objectMapper.writeValueAsString(exception));
} else {
// 在这里失败跳转不回去了。而且异常信息也没有打印出来。父类默认打印了死的一句话
// 在这里就不往上面扔了,这里就先当做 defaultFailureUrl 不存在吧
// 模拟打印异常信息
response.setContentType("text/html;charset=UTF-8");
response.sendError(HttpStatus.UNAUTHORIZED.value(),
exception.getLocalizedMessage());
// super.onAuthenticationFailure(request, response, exception);
}
注意: 之前我一直在说,异常了但是不会显示,不知道去哪里了。
这几次调试发现日志不断访问 /error ,好像这个在自己设置了BrowserSecurityConfig的拦截放行路径后,就一致这样了
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。