开发生成图形验证码接口
图片实体 ImageCode
public class ImageCode {
/**
* 一个图片验证码包含三个信息
*/
private BufferedImage image; //图片
private String code;//code是一个随机数,图片根据这个随机数生成,这个随机数是要存入到session中的
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 ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(expireTime);
}
图片接口 ValidateCodeController
根据随机数生成图片
将随机数存到session中
将生成的图片写到接口的响应中
@RestController
public class ValidateCodeController implements Serializable {
private static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key
//spring 操作session的工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@RequestMapping("/code/image")
private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1根据请求中的随机数生成图片
ImageCode imageCode = createImageCode(request);
//2将随机数放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//3将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) {
//生成一个图片对象
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(image, sRand, 60);
}
在认证流程中加入图形验证码校验
登录页面
<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>
安全认证配置不拦截图片路径
测试 图片路径无法访问404及解决
图片路径无法访问404,发现对应的model在mave中为灰色
解决
https://blog.youkuaiyun.com/L359389556/article/details/82852244
访问
自定义过滤器 ValidateCodeFilter 校验登录验证码
/**
* 继承spring中的OncePerRequestFilter,确保每次请求调用一次过滤器
*/
public class ValidateCodeFilter extends OncePerRequestFilter {
private AuthenticationFailureHandler authenticationFailureHandler;
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI())
&& StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){
try {
validate(new ServletWebRequest(httpServletRequest));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return;//失败后直接返回,不再走下面的过滤器
}
}
//如果不是登录请求,直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
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);
}
}
自定义异常 ValidateCodeException
/**
* AuthenticationException 是 springframework.security提供的,登录过程中所有异常的基类
*
*/
public class ValidateCodeException extends AuthenticationException {
private static final long serialVersionUID = -7285211528095468156L;
public ValidateCodeException(String msg) {
super(msg);
}
}
配置指定位置中加入此拦截器
protected void configure(HttpSecurity http) throws Exception {
//http.formLogin() //指定身份认证的方式为表单登录
//http.httpBasic()
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//设置错误过滤器
http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class)
.formLogin()
// .loginPage("/signIn.html") //指定登录页面的url
// .loginPage("/anthentication/require") //指定登录页面的url
.loginPage(securityProperties.getBrowser().getLoginPage()) //指定登录页面的url
.loginProcessingUrl("/authentication/form")
.successHandler(whaleAuthenticationSuccessHandler)
.failureHandler(whaleAuthenctiationFailureHandler)
.permitAll()
.and()
.authorizeRequests() //对请求授权
// .antMatchers("/signIn.html","/code/image").permitAll() //加一个匹配器 对匹配的路径不进行身份认证
.antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll() //加一个匹配器 对匹配的路径不进行身份认证
.anyRequest() //任何请求
.authenticated() //安全认证
.and()
.cors().disable().csrf().disable();// 禁用跨站攻击
// 默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉
//任何请求都必须经过表单验证才能进行访问
/* http.csrf().disable().cors().disable().headers().disable()
.authorizeRequests()
.antMatchers("/signIn.html").permitAll() // 配置不需要身份认证的请求地址
.anyRequest().authenticated() // 其他所有访问路径需要身份认证
.and()
.formLogin()
.loginPage("/signIn.html") // 指定登录请求地址
.loginProcessingUrl("/authentication/form")
.permitAll();
*/
}
测试ok
简化认证失败处理信息
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException ) throws IOException, ServletException {
logger.info("登录失败");
if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) {
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
httpServletResponse.setContentType("application/json;charset=UTF-8");
// httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authenticationException));//打印的信息太多 简化如下
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(authenticationException.getMessage())));
}else {
super.onAuthenticationFailure(httpServletRequest,httpServletResponse,authenticationException);
}
}
重构图形验证码接口
验证码基本参数可配置
默认配置
ImageCodeProperties
public class ImageCodeProperties {
private int width = 67;
private int height = 23;
private int length = 4;
private int expireIn = 60;
再封装一层ValidateCodeProperties
public class ValidateCodeProperties {
private ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
SecurityProperties加入ValidateCodeProperties
@ConfigurationProperties(prefix = "whale.security") //这个类会读取以whale.security开头的配置项
public class SecurityProperties {
//浏览器配置
private BrowserProperties browser = new BrowserProperties();
//验证码配置
private ValidateCodeProperties code = new ValidateCodeProperties();
应用级配置
demo application
whale.security.code.image.length = 6#验证码长度、
whale.security.code.image.width = 200
请求级配置
<tr>
<td>验证码:</td>
<td>
<input type="text" name="imageCode">
<img src="/code/image?width=200">
</td>
</tr>
拦截器引用配置
@RestController
public class ValidateCodeController implements Serializable {
public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key
//spring 操作session的工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@RequestMapping("/code/image")
private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1根据请求中的随机数生成图片
// ImageCode imageCode = createImageCode(request);
ImageCode imageCode = createImageCode(new ServletWebRequest(request));
//2将随机数放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//3将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
}
/**
*
* @param request(HttpServletRequest)
* @return
*/
private ImageCode createImageCode(ServletWebRequest request) {
//生成一个图片对象
// int width = 67;
int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth());
// int height =23;
int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight());
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++) {
// 验证码的长度不应该在请求中配置
for (int i = 0; i <securityProperties.getCode().getImage().getLength(); 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(image, sRand, 60);
return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn());
}
/**
* 生成随机背景条纹
*
* @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);
}
}
测试
验证码拦截的接口可配置
ImageCodeProperties加属性url
private String url;
验证码匹配路径以逗号隔开
demo application 中配置路径
whale.security.code.image.url = /user,/user/*
拦截器处理
/**
* 继承spring中的OncePerRequestFilter,确保每次请求调用一次过滤器
*/
//InitializingBean 实现此接口中的 afterPropertiesSet 初始化方法在其中初始化图片验证码拦截路径
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
//验证码路径,需要初始化
private Set<String> urls = new HashSet<>();
//配置
private SecurityProperties securityProperties;
//路径正则匹配工具
private AntPathMatcher pathMatcher = new AntPathMatcher();
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),",");
for (String url : configUrls) {
urls.add(url);
}
//这个路径是默认的
urls.add("/authentication/form");
}
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
System.out.println(httpServletRequest.getRequestURI());
System.out.println(httpServletRequest.getRequestURL());
//如果请求路径满足匹配模式 则需要验证码
boolean action = false;
for (String url : urls) {
if(pathMatcher.match(url,httpServletRequest.getRequestURI())){
action = true;
}
}
// if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI())
// && StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){
if(action){
try {
validate(new ServletWebRequest(httpServletRequest));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return;//失败后直接返回,不再走下面的过滤器
}
}
//如果不是登录请求,直接放行
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
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 SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
BrowserSecurityConfig中配置拦截器
················
@Override
protected void configure(HttpSecurity http) throws Exception {
//http.formLogin() //指定身份认证的方式为表单登录
//http.httpBasic()
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//设置错误过滤器
validateCodeFilter.setSecurityProperties(securityProperties);
validateCodeFilter.afterPropertiesSet();
············
测试
验证码的生成逻辑可配置
创建验证码生成器接口及实现类
把com.whale.security.core.validate.ValidateCodeController#createImageCode中生成图片验证码的逻辑搬到验证码生成器接口及实现类中
如下
ValidateCodeGenerator
public interface ValidateCodeGenerator {
ImageCode generate(ServletWebRequest request);
}
ImageCodeGenerator
public class ImageCodeGenerator implements ValidateCodeGenerator {
/**
* 系统配置
*/
@Autowired
private SecurityProperties securityProperties;
/*
* (non-Javadoc)
*
* @see
* com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org.
* springframework.web.context.request.ServletWebRequest)
*/
@Override
public ImageCode generate(ServletWebRequest request) {
ValidateCodeController中调用验证码生成器接口
@RestController
public class ValidateCodeController implements Serializable {
public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key
//spring 操作session的工具类
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private SecurityProperties securityProperties;
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@RequestMapping("/code/image")
private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//1根据请求中的随机数生成图片
// ImageCode imageCode = createImageCode(request);
// ImageCode imageCode = createImageCode(new ServletWebRequest(request));
ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
//2将随机数放到session中
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
//3将生成的图片写到接口的响应中
ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream());
}
imageCodeGenerator 图片生成器接口实现类的初始化和可配置
imageCodeGenerator是如何注入进去的呢
如下
@Configuration
public class ValidateCodeBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenerator")
//为啥这样配置
//@ConditionalOnMissingBean(name = "imageCodeGenerator")spring初始化这个类之前会判断容器中是否有名字为imageCodeGenerator的bean,
//若果有就用已经初始化的bean,没有的话才初始化当前bean
//这样 这个接口就可被用户覆盖
public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
测试demo模块中覆盖验证码生成器
@Component("imageCodeGenerator")
public class DemoImageCodeGenerator implements ValidateCodeGenerator {
@Override
public ImageCode generate(ServletWebRequest request) {
System.out.println("更高级的图形验证码生成代码");
return null;
}
}
访问报错 ok
主要是设计思想
以增量的方式去适应变化
当需求逻辑发生变化时,我们不是改变原来得代码,而是加一段代码:很重要