上一篇博文已经介绍过了SpringSecurity的表单登录,这里我们基于上一篇的基础上,添加一个验证码进行登录,登录页面效果图,如图所示:
首先我们需要创建验证码的生成规则,首先创建一个验证码的实体:
public class ImageCode {
/** 验证码 */
private String code;
/** 判断过期时间 */
private LocalDateTime expireTime;
/** 生成的图片验证码 */
private BufferedImage image;
public ImageCode(String code, int expireIn, BufferedImage image) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
this.image = image;
}
//判断验证码是否过期
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
//省略get/set方法
}
在定义一个controller用来处理我们验证码生成的流程:
@RestController
public class ValidateCodeController {
//定义存入session的key
public static final String SESSION_KEY = "SESSION_IMAGE_CODE";
/** 处理session */
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
private ImageCode createImageCode(ServletWebRequest servletWebRequest) {
int width = 67;
int height = 23;
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
g.setColor(getRandColor(200,250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160,200));
for(int i=0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
String sRand = "";
for(int i =0; i < 4; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(sRand, 60, image);
}
/**
* 生成随机背景条纹
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt((bc - fc));
int g = fc + random.nextInt((bc - fc));
int b = fc = random.nextInt((bc - fc));
return new Color(r, g, b);
}
}
然后我们需要定义一个验证码的拦截器来判断我们验证码的流程:
/**
* 定义一个验证码的拦截器
* @author hdd
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
private DemoAuthenticationFailureHandler demoAuthenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (StringUtils.equals("/authentication/form", request.getRequestURI()) &&
StringUtils.endsWithIgnoreCase(request.getMethod(), "post")) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
demoAuthenticationFailureHandler.onAuthenticationFailure(request,response,e);
return;
}
}
filterChain.doFilter(request,response);
}
//具体的验证流程
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在");
}
if (codeInSession.isExpried()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("验证码已过期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public DemoAuthenticationFailureHandler getDemoAuthenticationFailureHandler() {
return demoAuthenticationFailureHandler;
}
public void setDemoAuthenticationFailureHandler(DemoAuthenticationFailureHandler demoAuthenticationFailureHandler) {
this.demoAuthenticationFailureHandler = demoAuthenticationFailureHandler;
}
}
然我我们需要定义一个验证码的异常让SpringSecurity可以捕获到:
/**
* 用于抛出验证码错误的异常,集成AuthenticationException可被SpringSecurity捕获到
* @author hdd
* @date 2018/12/11 0011 14:55
* @param
* @return
*/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
最后将我们定义的拦截器注入到SpringSecurity的拦截器链中:
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setDemoAuthenticationFailureHandler(demoAuthenticationFailureHandler);
http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)//在UsernamePasswordAuthenticationFilter添加新添加的拦截器
.formLogin()//表示使用form表单提交
.loginPage("/login.html")//我们定义的登录页
.loginProcessingUrl("/authentication/form")//因为SpringSecurity默认是login请求为登录请求,所以需要配置自己的请求路径
.successHandler(demoAuthenticationSuccessHandler)//登录成功的操作
.failureHandler(demoAuthenticationFailureHandler)//登录失败的操作
.and()
.authorizeRequests()//对请求进行授权
.antMatchers("/login.html","/code/image").permitAll()//表示login.html路径不会被拦截
.anyRequest()//表示所有请求
.authenticated()//需要权限认证
.and()
.csrf().disable();//这是SpringSecurity的安全控制,我们这里先关掉
}
最后在页面中添加验证码:
<h3>表单登录</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用户名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>图形验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
当我们填写错误的验证码是,提示我们验证码错误:
完整项目代码请从git上拉取git地址