生成图形验证码
1.根据随机数生成图片
2.将随机数放入session
3.将生成的图片写到响应中,显示到前端
第一步: 创建ValidateCodeGenerator 接口 generate方法
package com.imooc.security.core.validate.code; import org.springframework.web.context.request.ServletWebRequest; public interface ValidateCodeGenerator { ValidateCode generate(ServletWebRequest request); }
第二步: 创建ImageCodeGenerator implements ValidateCodeGenerator 验证码的具体实现逻辑
package com.imooc.security.core.validate.code; import com.imooc.security.core.properties.SecurityProperties; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.context.request.ServletWebRequest; import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; public class ImageCodeGenerator implements ValidateCodeGenerator { private SecurityProperties securityProperties; @Override public ImageCode generate(ServletWebRequest request) { // 先在请求中取, 取不到使用默认值 int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width", securityProperties.getCode().getImage().getWidth()); 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 < 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,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); } public SecurityProperties getSecurityProperties() { return securityProperties; } public void setSecurityProperties(SecurityProperties securityProperties) { this.securityProperties = securityProperties; } }
第三步: 创建验证码的配置类 ValidateCodeBeanConfig.java
package com.imooc.security.core.validate.code; import com.imooc.security.core.properties.SecurityProperties; import com.imooc.security.core.validate.code.sms.DefaultSmsCodeSender; import com.imooc.security.core.validate.code.sms.SmsCodeSender; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ValidateCodeBeanConfig { @Autowired private SecurityProperties securityProperties; /** * ConditionalOnMissingBean 作用是先查找这个bean 如果没有找到再用默认配置 * @return */ @Bean @ConditionalOnMissingBean(name = "imageCodeGenerator") public ValidateCodeGenerator imageCodeGenerator() { ImageCodeGenerator codeGenerator = new ImageCodeGenerator(); codeGenerator.setSecurityProperties(securityProperties); return codeGenerator; } @Bean @ConditionalOnMissingBean(SmsCodeSender.class) public SmsCodeSender smsCodeSender() { DefaultSmsCodeSender defaultSmsCodeSender = new DefaultSmsCodeSender(); return defaultSmsCodeSender; } }
第四步: 创建ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean
package com.imooc.security.core.validate.code; import com.imooc.security.core.properties.SecurityProperties; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.social.connect.web.HttpSessionSessionStrategy; import org.springframework.social.connect.web.SessionStrategy; import org.springframework.util.AntPathMatcher; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.context.request.ServletWebRequest; 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; import java.util.HashSet; import java.util.Set; /** * OncePerRequestFilter 只会调用一次Filter */ public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); private Set<String> urls = new HashSet<>(); private SecurityProperties securityProperties; public void setSecurityProperties(SecurityProperties securityProperties) { this.securityProperties = securityProperties; } private AuthenticationFailureHandler authenticationFailureHandler; private AntPathMatcher pathMatcher = new AntPathMatcher(); // 加载完配置文件之后,获取所有需要使用验证码的url @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ","); if (configUrls != null) { for (String url : configUrls) { urls.add(url); } } urls.add("/authentication/form"); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { boolean action = false; for (String url : urls) { if (pathMatcher.match(url, request.getRequestURI())) action = true; } if (action) { try { validate(new ServletWebRequest(request)); } catch (ValidateCodeException e) { authenticationFailureHandler.onAuthenticationFailure(request, response, e); return; } } filterChain.doFilter(request, response); } public 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 (codeInRequest == 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 void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { this.authenticationFailureHandler = authenticationFailureHandler; } }
第五步: 将Filter 加入到过滤器链中
.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
)
package com.imooc.security.browser; import com.imooc.security.core.authentication.mobile.SmsCodeAuthenticationSecurityConfig; import com.imooc.security.core.properties.SecurityProperties; import com.imooc.security.core.validate.code.SmsCodeFilter; import com.imooc.security.core.validate.code.ValidateCodeFilter; 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.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import org.springframework.social.security.SpringSocialConfigurer; import javax.sql.DataSource; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler; @Autowired private AuthenticationFailureHandler imoocAuthenctiationFailureHandler; @Bean public PasswordEncoder passwordEncoder() { return new SCryptPasswordEncoder(); } @Autowired private SecurityProperties securityProperties; // PersistentTokenRepository @Autowired private DataSource dataSource; @Autowired private UserDetailsService userDetailsService; // MyUserDetailsService @Autowired private SpringSocialConfigurer imoocSocialSecurityConfig; /** * 记住我功能 * 1. 创建PersistentTokenRepository * 2. 设置过期时间 * 3. 获取UserDetailsService 用户登录信息 * 4. 配置rememberMe 生效 * @return */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // 配置的dataSource // jdbcTokenRepository.setCreateTableOnStartup(true); // 自动创建存放记住我的表,如果存在会报错 return jdbcTokenRepository; } @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Override public void configure(HttpSecurity http) throws Exception { // 图片验证码过滤器 ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setAuthenticationFailureHandler(imoocAuthenctiationFailureHandler); validateCodeFilter.setSecurityProperties(securityProperties); validateCodeFilter.afterPropertiesSet(); // 验证码过滤器 SmsCodeFilter smsCodeFilter = new SmsCodeFilter(); smsCodeFilter.setAuthenticationFailureHandler(imoocAuthenctiationFailureHandler); smsCodeFilter.setSecurityProperties(securityProperties); smsCodeFilter.afterPropertiesSet(); // 添加一个图片验证filter, 在UsernamePasswordAuthenticationFilter之前执行 http .apply(smsCodeAuthenticationSecurityConfig) .and() .apply(imoocSocialSecurityConfig) // 添加过滤器SocialAuthenticationFilter .and() .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // .httpBasic() // 默认方式 .formLogin() // 设置认证的登录方式 表单方式 .loginPage("/authentication/require") // 自定义登录页面 .loginProcessingUrl("/authentication/form") // 自定义表单提交的url, 默认是login .successHandler(imoocAuthenticationSuccessHandler) // 不适用默认的认证成功处理器 .failureHandler(imoocAuthenctiationFailureHandler) // 登录失败处理器 // .failureForwardUrl("/authentication/require") // .failureUrl("/authentication/require") .and() .rememberMe() .tokenRepository(persistentTokenRepository()) // rememberME 有效期 .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) .userDetailsService(userDetailsService) .and() .authorizeRequests() // 需要授权 // 当匹配到这个页面时,不需要授权 .antMatchers( "/authentication/require", "/qqLogin/*", "/auth/*", securityProperties.getBrowser().getLoginPage(), "/code/*").permitAll() .anyRequest() // 所有请求 .authenticated() .and() // 关闭csrf .csrf() .disable(); } }
controller
package com.imooc.security.core.validate.code; import com.imooc.security.core.properties.SecurityConstants; import com.imooc.security.core.properties.SecurityProperties; import com.imooc.security.core.validate.code.sms.SmsCodeSender; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.web.HttpSessionSessionStrategy; import org.springframework.social.connect.web.SessionStrategy; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.ServletWebRequest; 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; @RestController public class ValidateCodeController { public static String SESSION_KEY = "SESSION_KEY_IMAGE_CODE"; public static String SESSION_SMS_KEY = "SESSION_KEY_SMS_CODE"; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private ValidateCodeGenerator imageCodeGenerator; @Autowired private ValidateCodeGenerator smsCodeGenerator; @Autowired private SmsCodeSender smsCodeSender; @GetMapping("/code/image") public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = (ImageCode) imageCodeGenerator.generate(new ServletWebRequest(request)); // 放入session sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode); ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream()); } @GetMapping("/code/sms") public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException { ValidateCode smsCode = smsCodeGenerator.generate(new ServletWebRequest(request)); // 放入session sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_SMS_KEY, smsCode); // 通过短信服务商发送短信验证码到手机 String mobile = ServletRequestUtils.getStringParameter(request, "mobile"); smsCodeSender.send(mobile, smsCode.getCode()); } }
异常类:
package com.imooc.security.core.validate.code; import org.springframework.security.core.AuthenticationException; /** * AuthenticationException 认证异常的基类 */ public class ValidateCodeException extends AuthenticationException { private static final long serialVersionUID = -7285211528095468156L; public ValidateCodeException(String msg) { super(msg); } }