简介
在登录界面图形添加验证码,流程1.开发生成图像验证码接口 2.在认证流程中加入图像验证码校验
图形验证码实体类
import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
/**
* 图片的代码
*
* @author hekang
* @date 2020/01/10
*/
@Data
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public boolean isExpire() {
return LocalDateTime.now().isAfter(expireTime);
}
}
我们是做一个可以复用的框架,这些参数也弄到系统配置。
配置系统参数
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1/auto_test?useUnicode=yes&characterEncoding=UTF-8&useSSL=false
username: root
password: 123456
hk:
security:
browser:
loginSucess: /index #登录成功跳转
loginType: JSON #登录成功 失败返回值类型
loginPage: /signIn.html #登录页面
oginFailure: /failure #登录失败跳转
code:
image:
width: 90 # 验证码图片宽度
height: 20 # 验证码图片长度
length: 6 # 验证码位数
expireIn: 60 # 验证码有效时间 60s
url: /login #需要过滤的路径/login,/login/*
注意:code这里多了一层image是因为考虑到后期还有短信验证码。
创建ValidateCodeProperties类,core项目properties目录
package com.spring.security.properties;
import lombok.Data;
/**
* 验证代码属性
*/
@Data
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
}
同目录创建ImageCodeProperties类
package com.spring.security.properties;
import lombok.Data;
/**
* 图像编码属性
*/
@Data
public class ImageCodeProperties {
// 验证码图片宽度
private int width = 60;
// 验证码图片长度
private int height = 20;
// 验证码位数
private int length = 4;
// 验证码有效时间 60s
private int expireIn = 60;
//要拦截的路径
private String url;
}
生成图像验证码接口
同目录创建接口ValidateCodeGenerator类
package com.spring.security.validate.code;
import org.springframework.web.context.request.ServletWebRequest;
/**
* 验证代码生成器
*
*/
public interface ValidateCodeGenerator {
ImageCode createImageCode(ServletWebRequest request);
}
同目录创建接口ValidateCodeGenerator实现ImageCodeGenerator类
package com.spring.security.validate.code;
import com.spring.security.properties.SecurityProperties;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;
/**
* 图像代码生成器
*/
@Data
public class ImageCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
/**
* 创建图片的代码
*
* @param request
* @return {@link ImageCode}
*/
@Override
public ImageCode createImageCode(ServletWebRequest request) {
//先从用户获取 如果取不到再获取配置
// 验证码图片宽度
int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width", securityProperties.getCode().getImage().getWidth());
// 验证码图片长度
int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height", securityProperties.getCode().getImage().getHeight());
// 验证码位数
int length = securityProperties.getCode().getImage().getLength();
// 验证码有效时间 60s
int expireIn = securityProperties.getCode().getImage().getExpireIn();
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);
}
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(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(image, sRand.toString(), expireIn);
}
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);
}
}
图像验证码访问接口
同目录创建ValidateController控制器
package com.spring.security.validate.code;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 验证控制器
*/
@RestController
public class ValidateController {
public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
/**
* 创建验证码
*
* @param request 请求
* @param response 响应
* @throws IOException IOException
*/
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = imageCodeGenerator.createImageCode(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
}
图形验证码拦截器
同目录创建ValidateCodeFilter拦截器
package com.spring.security.validate.code;
import com.spring.security.properties.SecurityProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
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;
import java.util.HashSet;
import java.util.Set;
/**
* 验证代码过滤
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
private Set<String> urls = new HashSet<>();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
//加入固定提交地址
urls.add("/authentication/form");
}
/**
* 做过滤器内部
*
* @param httpServletRequest http Servlet请求
* @param httpServletResponse
* @param filterChain 过滤器链
* @throws ServletException Servlet异常
* @throws IOException IOException
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
if (StringUtils.startsWithIgnoreCase(url, httpServletRequest.getRequestURI())
&& StringUtils.startsWithIgnoreCase(httpServletRequest.getMethod(), "post")) {
action = true;
}
}
if (action) {
try {
validateCode(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
/**
* 验证代码
*
* @param servletWebRequest servlet的Web请求
* @throws ServletRequestBindingException Servlet请求绑定异常
*/
private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");
if (StringUtils.isEmpty(codeInRequest)) {
throw new ValidateCodeException("验证码不能为空!");
}
if (codeInSession == null) {
throw new ValidateCodeException("验证码不存在!");
}
if (codeInSession.isExpire()) {
sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
throw new ValidateCodeException("验证码已过期!");
}
if (!StringUtils.startsWithIgnoreCase(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("验证码不正确!");
}
sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
}
}
异常处理
同目录创建ValidateCodeException异常处理类
package com.spring.security.validate.code;
import org.springframework.security.core.AuthenticationException;
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = 5022575393500654458L;
public ValidateCodeException(String message) {
super(message);
}
}
配置Bean
同目录创建ValidateCodeBeanConfig配置Bean类
package com.spring.security.validate.code;
import com.spring.security.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 验证代码Bean配置
*/
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
/**
* 不存在imageCodeGenerator再使用下面的bean
* @return
*/
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
public ValidateCodeGenerator imageCodeGenerator(){
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
配置图形验证码拦截器
修改BrowserSecurityConfig类把图形验证码拦截器加入到security拦截器里
package com.spring.security;
import com.spring.security.authentication.HkAuthenticationFailureHandler;
import com.spring.security.authentication.HkAuthenticationSuccessHandler;
import com.spring.security.properties.SecurityProperties;
import com.spring.security.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private HkAuthenticationSuccessHandler hkAuthenticationSuccessHandler;
@Autowired
private HkAuthenticationFailureHandler hkAuthenticationFailureHandler;
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//配置
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // 添加验证码校验过滤器
.formLogin()
.loginPage("/authentication/require")//登录页面路径
// 处理登录请求路径
.loginProcessingUrl("/authentication/form")
.successHandler(hkAuthenticationSuccessHandler) // 处理登录成功
.failureHandler(hkAuthenticationFailureHandler) // 处理登录失败
.and()
.authorizeRequests() // 授权配置
//不需要认证的路径
.antMatchers("/authentication/require","/signIn.html","/code/image",securityProperties.getBrowser().getLoginPage(),"/failure").permitAll()
.anyRequest() // 所有请求
.authenticated() // 都需要认证
.and().csrf().disable();
}
}
登录页面加入图形验证码
改造signIn.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>标准登录页面</title>
</head>
<body>
<h2>标准登录页面</h2>
<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" placeholder="验证码"/>
<img src="/code/image"/></td>
</tr>
<tr>
<td colspan="2">
<button type="submit">登录</button>
</td>
</tr>
</table>
</form>
</body>
</html>
启动项目
访问:http://127.0.0.1:8080/index.html
不输入验证码点击登录:
输入错误验证码点击登录:
输入正确验证码和账号密码:
来源:oschina
链接:https://my.oschina.net/u/1046143/blog/3185342