图片验证码登录验证
- 1.验证码流程详解
- 2.验证码生成
- 3.验证码校验
1.验证码流程详解
验证码流程图解析:
- 客户端打开登陆页的时候就要发送一个生成图片验证码的请求
- 服务端接受请求,就要随机生成验证码图片,将图片验证码响应给前端页面,并且要将生成的验证码保存到session中,以便登录验证校验
- 客户端收到验证码图片后,填入表单信息后,发送登录请求
- 服务端在接受到前端传来的验证码参数,要先与session中的比对,如果相同,则响应正确,如果不匹配,则返回相应错误信息,如验证码不匹配
本次系统测试效果:
本次的系统是继上一篇springsecurity的案例系统,添加了图片验证码的功能,如果想做参考:上篇博客入口
可以看到测试效果,如果验证码为空,则提示验证码为空,如果验证码错误,则提示验证码不匹配,如果匹配成功,则可以登录,如果已经登录过后,直接返回刚刚的登录页面,再次使用之前的验证码登录,会提示验证码不存在,保证了验证码的一次使用性(一个验证码只能登录一次)!
2.验证码生成
可以用controller处理生成验证码的请求:
@RestController
public class CheckCodeController {
@RequestMapping("/getCode")
public void getCode(HttpServletRequest request, HttpServletResponse response){
//生成对应宽高的初始图片
int width=130;
int height=45;
BufferedImage img = new BufferedImage(width,height,BufferedImage.TYPE_INT_BGR);
//美化图片
Graphics g = img.getGraphics();
g.setColor(Color.white); //设置画笔颜色-验证码背景色
g.fillRect(0, 0, width, height);//填充背景
Random ran = new Random();
//产生4个随机验证码,12Ey
String checkCode = getCheckCode();
//将验证码放入HttpSession中
request.getSession().setAttribute("checkCode_session",checkCode);
Color color = new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));//随机生成颜色
g.setColor(color);
//设置字体的小大
g.setFont(new Font("微软雅黑", Font.BOLD, 40) );
//向图片上写入验证码
g.drawString(checkCode,15,33);
//画干扰线
for (int i = 0; i <6; i++) {
// 设置随机颜色
Color color1 = new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));//随机生成颜色
g.setColor(color1);
// 随机画线
g.drawLine(ran.nextInt(width), ran.nextInt(height),
ran.nextInt(width), ran.nextInt(height));
}
//添加噪点
for(int i=0;i<30;i++){
int x1 = ran.nextInt(width);
int y1 = ran.nextInt(height);
Color color2 = new Color(ran.nextInt(256),
ran.nextInt(256), ran.nextInt(256));//随机生成颜色
g.setColor(color2);
g.fillRect(x1, y1, 2,2);
}
//将图片输出页面展示
try {
ImageIO.write(img,"png",response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
}
//生成随机验证码方法
private String getCheckCode() {
String base = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
int size = base.length();
Random r = new Random();
StringBuffer sb = new StringBuffer();
for(int i=1;i<=4;i++){
//产生0到size-1的随机值
int index = r.nextInt(size);
//在base字符串中获取下标为index的字符
char c = base.charAt(index);
//将c放入到StringBuffer中去
sb.append(c);
}
return sb.toString();
}
}
前端验证码部分的代码:
//表单数据
<span>验证码:<input id="checkCode" type="text" name="checkCode">
<img src="/getCode" style="width: 130px;height: 40px" onclick="changeCheckCode(this)" ></span>
//图片点击事件
function changeCheckCode(img) {
img.src="/getCode?"+new Date().getTime();
//拼接时间,是为了可以一直刷新验证码,也可以用其他随机数
}
//Ajax请求,将表单提交,提交按钮点击事件出发login()方法就行
function login() {
var username=$("#username").val();
var password=$("#password").val();
var checkCode=$("#checkCode").val();
var rememberMe=$("#remember-me").is(":checked");
if(username == "" || password == ""){
alert("用户名或密码不能为空")
}
$.post("/login",{"username":username,"password":password,"checkCode":checkCode,"remember":rememberMe},function (data) {
if (data.isok){
//成功
location.href="/index";
}else {
//失败
alert(data.msg);
location.href="/login.html"
}
})
}
springsecurity还需要配置验证码的请求权限:
.authorizeRequests()
.antMatchers("/login.html","/login","/getCode").permitAll()
3.验证码校验
首先写一个验证码校验的过滤器:
import com.xuhao.springsecurity.auth.MyFailureHandler;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thymeleaf.util.StringUtils;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;
@Component
public class CheckCodeFilter extends OncePerRequestFilter {
@Resource
private MyFailureHandler myFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
if(StringUtils.equals("/login",request.getRequestURI())
&& StringUtils.equalsIgnoreCase(request.getMethod(),"post")){
try{
//验证谜底与用户输入是否匹配
validate(request);
}catch(AuthenticationException e){
myFailureHandler.onAuthenticationFailure(
request,response,e //产生异常交给myFailureHandler处理
);
return; //产生异常就不执行后面的过滤器链
}
}
filterChain.doFilter(request,response);
}
//校验规则
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
HttpSession session = request.getSession();
String checkCode = request.getParameter("checkCode");
if(StringUtils.isEmpty(checkCode)){
throw new SessionAuthenticationException("验证码不能为空");
}
// 获取session池中的验证码谜底,session中不存在的情况
String checkCode_session = (String) session.getAttribute("checkCode_session");
if(Objects.isNull(checkCode_session)) {
throw new SessionAuthenticationException("验证码不存在");
}
// 请求验证码校验
if(!StringUtils.equalsIgnoreCase(checkCode_session, checkCode)) {
throw new SessionAuthenticationException("验证码不匹配");
}
}
}
在自定义的登录失败类中处理验证码验证异常:
@Component
public class MyFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Value("${spring.security.loginType}")
private String loginType;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
String errorMsg = "用户名或者密码输入错误!";//返回的错误信息,默认是登录的错误
if(exception instanceof SessionAuthenticationException){ //如果异常属于验证码session的异常,则获取异常的信息
errorMsg = exception.getMessage();
}
if (loginType.equalsIgnoreCase("json")) {
//将返回的对象转换成json数据
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(Result.fail(errorMsg));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
}else {
//重新跳转到登录页面
super.onAuthenticationFailure(request, response, exception);
}
}
}
在自定义的登录成功类中移除session中的验证码,做到登录成功后就不能用这个验证码了,保证一个验证码只能用一次:
@Component
public class MySuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Value("${spring.security.loginType}")
private String loginType;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
if (loginType.equalsIgnoreCase("json")){
HttpSession session = request.getSession();
//登录成功,删除session中验证码,保证验证码的一次性使用
session.removeAttribute("checkCode_session");
//将返回的对象转换成json数据
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(Result.success(null));
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(json);
}else{
//跳转到登录之前请求的页面
super.onAuthenticationSuccess(request, response, authentication);
}
}
}
最后在securityConfig中配置验证码过滤器:
//先注入
@Resource
private CheckCodeFilter checkCodeFilter;
//在用户名密码登录验证过滤器前先执行验证码过滤器
http.addFilterBefore(checkCodeFilter, UsernamePasswordAuthenticationFilter.class);
至此,基于SpringSecurity的图片验证码登录功能已完全实现~~