1.SpringSecurity并没有给我们提供图形验证码,因为spring security他的基本原理就是一个过滤器链。对于springSceurity而言,验证码的执行校验顺序肯定是在UsernamePasswordAuthenticationFilter之前的,因为如果验证码都不对,那么 根本都不需要验证账号密码。在这个链上我们可以加入自己写的过滤器。我们在UsernamePasswordAuthticationFilter前加一个自定义的过滤器。 extends OncePerRequestFilter。实现:doFilterInternal方法。
<!--图形验证码--> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> </dependency>
2.生成验证码配置类
import com.google.code.kaptcha.Constants; import com.google.code.kaptcha.impl.DefaultKaptcha; import com.google.code.kaptcha.util.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Properties; /** * 生成验证码配置类 */ @Configuration public class KaptchaVerifyCodeConfig { @Bean public DefaultKaptcha getDefaultKaptcha(){ DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); Properties properties = new Properties(); properties.setProperty(Constants.KAPTCHA_BORDER, "yes"); properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192"); properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110"); properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "36"); properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "blue"); properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "28"); properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_NAMES, "宋体"); properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4"); // 图片效果 properties.setProperty(Constants.KAPTCHA_OBSCURIFICATOR_IMPL, "com.google.code.kaptcha.impl.ShadowGimpy"); Config config = new Config(properties); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
3.验证码生成处理接口
import com.google.code.kaptcha.impl.DefaultKaptcha; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import javax.imageio.ImageIO; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.awt.image.BufferedImage; import java.io.IOException; @Controller public class VerifyCodeController { Logger logger = LoggerFactory.getLogger(getClass()); public static final String SESSION_VERIFY_CODE = "SESSION_KEY_VERIFY_CODE"; @Autowired DefaultKaptcha defaultKaptcha; /** * 验证码请求接口 * @param request * @param response * @throws IOException */ @RequestMapping(value = "/code/image", method = RequestMethod.GET) public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException { //1.获取验证码字符串 String code = defaultKaptcha.createText(); logger.info("生产的验证码:"+code); // 2. 字符串把它放到session中 HttpSession session = request.getSession(); session.setAttribute(SESSION_VERIFY_CODE , code); // 3. 获取验证码图片 BufferedImage image = defaultKaptcha.createImage(code); // 4. 将验证码图片把它写出去 // 禁止图片缓存 response.setHeader("Pragma","no-cache"); response.setHeader("Cache-Control","no-cache"); response.setDateHeader("Expires",0); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "jpg", out); } }
4.前端获取验证码
<img onclick="this.src='/code/image?'+Math.random()" src="/code/image" alt="验证码" />
5.验证码校验过滤器
/** * * 验证码校验过滤器 * OncePerRequestFilter: 所有请求之前被调用一次 */ @Component("imageCodeValidateFilter") public class VerifyCodeValidateFilter extends OncePerRequestFilter { @Autowired SecurityProperties securityProperties; @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 1. 如果是post方式 的登录请求,则校验输入的验证码是否正确 if (securityProperties.getLoginProcessingUrl().equals(request.getRequestURI())&&request.getMethod().equalsIgnoreCase("post")){ try { //校验验证码合法性 validate(request); } catch (AuthenticationException e) { //将验证失败的抛出的异常交给自定义认证失败处理器处理异常 customAuthenticationFailureHandler.onAuthenticationFailure(request,response,e); return; } } filterChain.doFilter(request,response); } private void validate(HttpServletRequest request) { //先获取session中的验证码 String sessionImageCode = (String) request.getSession().getAttribute(VerifyCodeController.SESSION_VERIFY_CODE); //获取用户输入的验证码 String inputCode = request.getParameter("code"); if (StringUtils.isBlank(inputCode)){ throw new VerifyCodeExpection("验证码不能为空"); } if (!inputCode.equalsIgnoreCase(sessionImageCode)){ throw new VerifyCodeExpection("验证码输入错误"); } } }
/** * 自定义验证码异常类 */ public class VerifyCodeExpection extends AuthenticationException { public VerifyCodeExpection(String msg, Throwable t) { super(msg, t); } public VerifyCodeExpection(String msg) { super(msg); } }
6.在认证流程安全配置类SpringSecurityConfig中加入图形验证码校验 过滤器(需要把自定义的过滤器加到UsernamePasswordAuthenticationFilter前面去。)
@Autowired CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; @Autowired CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Autowired VerifyCodeValidateFilter verifyCodeValidateFilter; /** * 资源权限配置(过滤器链): * 1、被拦截的资源 * 2、资源所对应的角色权限 * 3、定义认证方式:httpBasic 、httpForm * 4、定制登录页面、登录请求地址、错误处理方式 * 5、自定义 spring security 过滤器 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { //http.httpBasic()//采用httpBasic 认证方式 /*http.formLogin() .loginPage("/login/page")// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl("/login/form") // 登录表单提交处理Url, 默认是 /login .usernameParameter("name") // 默认用户名的属性名是 username .passwordParameter("pwd") // 默认密码的属性名是 password .and() .authorizeRequests()//认证请求 .antMatchers("/login/page").permitAll()//自定义登录页不需要认证 .anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证*/ http.addFilterBefore(verifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)//将校验过滤器 imageCodeValidateFilter 添加到 UsernamePasswordAuthenticationFilter 前面 .formLogin() .loginPage(securityProperties.getLoginPage())// 交给 /login/page 响应认证(登录)页面 .loginProcessingUrl(securityProperties.getLoginProcessingUrl()) // 登录表单提交处理Url, 默认是 /login .usernameParameter(securityProperties.getUsernameParameter()) // 默认用户名的属性名是 username .passwordParameter(securityProperties.getPasswordParameter()) // 默认密码的属性名是 password .successHandler(customAuthenticationSuccessHandler)//自定义认证成功处理器 .failureHandler(customAuthenticationFailureHandler)//自定义认证失败处理器 .and() .authorizeRequests()//认证请求 .antMatchers(securityProperties.getLoginPage(),"/code/image").permitAll()//自定义登录页不需要认证,生成图片验证码也不需要验证 .anyRequest().authenticated();// 所有进入应用的HTTP请求都要进行认证 }
完整代码地址:https://gitee.com/zhechaochao/security-parent.git