9.图形验证码
图形验证码
图形验证码一般是防止恶意,人眼看起来都费劲,何况是机器。不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰, 也有目前需要手动滑动的图形验证码. 这种可以有专门去做的第三方平台. 比如极验(https://www.geetest.com/), 那么本次课程讲解主要针对图形验证码.
spring security添加验证码大致可以分为三个步骤:
1. 根据随机数生成验证码图片;
2. 将验证码图片显示到登录页面;
3. 认证流程中加入验证码校验。
Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下:

代码实现:
验证码生成类
package com.po.controller;
import com.po.domain.ImageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* 处理生成验证码的请求
*/
@RestController
@RequestMapping("/code")
public class ValidateCodeController {
public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
public final static int expireIn = 60; // 验证码有效时间 60s
//使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。
@Autowired
public StringRedisTemplate stringRedisTemplate;
@RequestMapping("/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//获取访问IP
String remoteAddr = request.getRemoteAddr();
//生成验证码对象
ImageCode imageCode = createImageCode();
//生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr)
.set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
//通过IO流将生成的图片输出到登录页面上
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
/**
* 用于生成验证码对象
*
* @return
*/
private ImageCode createImageCode() {
int width = 100; // 验证码图片宽度
int height = 36; // 验证码图片长度
int length = 4; // 验证码位数
//创建一个带缓冲区图像对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//获得在图像上绘图的Graphics对象
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());
}
/**
* 获取随机演示
*
* @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);
}
}
自定义验证码过滤器ValidateCodeFilter
package com.po.filter;
import com.po.controller.ValidateCodeController;
import com.po.exception.ValidateCodeException;
import com.po.service.impl.MyAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
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 一次请求只会经过一次过滤器
*/
@Service
public class ValidateCodeFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 判断是否是登录请求
if (request.getRequestURI().equals("/login") &&
request.getMethod().equalsIgnoreCase("post")) {
String imageCode = request.getParameter("imageCode");
System.out.println(imageCode);
// 具体的验证流程
try {
validate(request, imageCode);
} catch (ValidateCodeException e) {
myAuthenticationService.onAuthenticationFailure(request, response, e);
return;
}
}
// 如果不是登录请求,直接放行
filterChain.doFilter(request, response);
}
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
MyAuthenticationService myAuthenticationService;
private void validate(HttpServletRequest request, String imageCode) {
// 从redis中获取验证码
String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + request.getRemoteAddr();
String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();
// 验证码的判断
if (!StringUtils.hasText(redisImageCode)) {
throw new ValidateCodeException("验证码的值不能为空");
}
if (redisImageCode == null) {
throw new ValidateCodeException("验证码已过期");
}
if (!redisImageCode.equals(imageCode)) {
throw new ValidateCodeException("验证码不正确");
}
// 从redis中删除验证码
stringRedisTemplate.delete(redisKey);
}
}
自定义验证码异常类
package com.po.exception;
import org.springframework.security.core.AuthenticationException;
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
}
security配置类(里面添加我们自定义过滤器的顺序)
package com.po.config;
import com.po.filter.ValidateCodeFilter;
import com.po.service.impl.MyAuthenticationService;
import com.po.service.impl.MyUserDetailsService;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
ValidateCodeFilter validateCodeFilter;
/**
* http请求方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/** http.httpBasic() //开启httpBasic认证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问
*/
// 将验证码过滤器添加在UsernamePasswordAuthenticationFilter过滤器的前面
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
/* http.formLogin().loginPage("/login.html")//开启表单认证
// .and().authorizeRequests() //放行登录页面
// .anyRequest().authenticated();
// .and().authorizeRequests().antMatchers("/login.html").permitAll() //放行登录页面
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated();*/
http.formLogin() //开启表单认证
.loginPage("/toLoginPage") // 自定义登陆页面
.loginProcessingUrl("/login") //表单提交路径
.usernameParameter("username").passwordParameter("password") //自定义input额name值和password
.successForwardUrl("/") //登录成功之后跳转的路径
.successHandler(myAuthenticationService) // 登录成功处理
.failureHandler(myAuthenticationService) //登录失败处理
.and().logout().logoutUrl("/logout") //退出
.logoutSuccessHandler(myAuthenticationService) //退出后处理
.and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
.anyRequest().authenticated()
.and().rememberMe() //开启记住我功能
.tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
.rememberMeParameter("remember-me") // 自定义表单name值
.tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
.and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
.and().csrf().disable();//关闭csrf防护
}
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**","/images/**","/js/**","/code/**");
}
/**
*身份安全管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
@Autowired
DataSource dataSource;
/**
* 负责token与数据库之间的操作
* @return
*/
@Bean
public PersistentTokenRepository getPersistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource); //设置数据源
tokenRepository.setCreateTableOnStartup(false); //启动时帮助我们自动创建一张表,第一次启动设置为true,第二次启动程序的时候设置false或者注释掉;
return tokenRepository;
}
@Autowired
private MyAuthenticationService myAuthenticationService;
}

浙公网安备 33010602011771号