图形验证码一般是防止恶意,人眼看起来都费劲,何况是机器。不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰, 也有目前需要手动滑动的图形验证码. 这种可以有专门去做的第三方平台. 比如极验(https://www.geetest.com/), 那么本次课程讲解主要针对图形验证码.
spring security添加验证码大致可以分为三个步骤:
1. 根据随机数生成验证码图片;
2. 将验证码图片显示到登录页面;
3. 认证流程中加入验证码校验。
Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下:
代码实现:
验证码生成类
package com.lagou.controller;import com.lagou.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流将生成的图片输出到登录页面上。@Autowiredpublic StringRedisTemplate stringRedisTemplate;@RequestMapping("/image")public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {//获取访问IPString 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.lagou.filter;import com.lagou.controller.ValidateCodeController;
import com.lagou.exception.ValidateCodeException;
import com.lagou.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 {@Overrideprotected 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);}@AutowiredStringRedisTemplate stringRedisTemplate;@AutowiredMyAuthenticationService 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.lagou.exception;import org.springframework.security.core.AuthenticationException;/*** 验证码异常类*/
public class ValidateCodeException extends AuthenticationException {public ValidateCodeException(String msg) {super(msg);}
}
security配置类(里面添加我们自定义过滤器的顺序)
package com.lagou.config;import com.lagou.filter.ValidateCodeFilter;
import com.lagou.service.impl.MyAuthenticationService;
import com.lagou.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;/*** spring security配置类*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@AutowiredMyUserDetailsService myUserDetailsService;@AutowiredMyAuthenticationService myAuthenticationService;/*** 身份安全管理器** @param auth* @throws Exception*/@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService);}@Overridepublic void configure(WebSecurity web) throws Exception {// 解决静态资源被拦截的问题web.ignoring().antMatchers("/css/**", "/images/**", "/js/**", "/code/**");}@AutowiredValidateCodeFilter validateCodeFilter;/*** http请求方法** @param http* @throws Exception*/@Overrideprotected void configure(HttpSecurity http) throws Exception {/* http.httpBasic() // 开启httpBasic认证.and().authorizeRequests().anyRequest().authenticated(); // 所有请求都需要认证后才能访问 */// 将验证码过滤器添加在UsernamePasswordAuthenticationFilter过滤器的前面http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);http.formLogin() // 开启表单认证(默认方式).loginPage("/toLoginPage") // 设置自定义的登录页面.loginProcessingUrl("/login") // 表单提交的路径.usernameParameter("username").passwordParameter("password") // 自定义input的name值.successForwardUrl("/") // 登录成功之后跳转的路径.successHandler(myAuthenticationService) // 登录成功后的处理.failureHandler(myAuthenticationService) // 登录失败后的处理.and().logout().logoutUrl("/logout") // 指定退出路径,默认为/logout.logoutSuccessHandler(myAuthenticationService) // 退出之后的处理逻辑.and().rememberMe() // 开启记住我功能.tokenValiditySeconds(1209600) // token失效时间,默认是两周.rememberMeParameter("remember-me") // 表示表单input里面的name值,不写默认就是remember-me.tokenRepository(getPersistentTokenRepository()).and().authorizeRequests().antMatchers("/toLoginPage").permitAll() // 放行登录页面.anyRequest().authenticated();// 关闭csrf防护http.csrf().disable();// 加载同源域名下的iframe页面http.headers().frameOptions().sameOrigin();}/*** 负责token与数据库之间的操作** @return*/@AutowiredDataSource dataSource;@Beanpublic PersistentTokenRepository getPersistentTokenRepository() {JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();tokenRepository.setDataSource(dataSource); // 设置数据源tokenRepository.setCreateTableOnStartup(false); // 启动时自动帮我们创建一张表,第一次启动设置true,第二次启动设置为false或者注释掉return tokenRepository;}
}
将MyAuthenticationService的异常信息进行自动获取,而不是我们固定死
/*** 登录失败后的处理逻辑*/@Overridepublic void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {System.out.println("登录失败后继续处理...............");// 重定向到login页面// redirectStrategy.sendRedirect(request, response, "/toLoginPage");Map<Object, Object> result = new HashMap<>();result.put("code", HttpStatus.UNAUTHORIZED.value()); // 401result.put("message", exception.getMessage()); // 修改的地方,现在异常信息从exception中获取response.setContentType("application/json;charset=UTF-8");response.getWriter().write(objectMapper.writeValueAsString(result));}