一、开发生成图形验证码接口
1.根据随机数生成图片
因为不管是手机APP还是浏览器都可能会用到,所以我写到了core模块。
2.将随机数存到Session中
3.再将生成的图片写到接口的响应中
首先定义一个实体类,封装验证码的信息,其中包含图片信息,验证码,以及过期时间
package com.tinner.security.core.validate.code;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireTime) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
}
public boolean isExpried(){
return LocalDateTime.now().isAfter(expireTime);
}
public BufferedImage getImage() {
return image;
}
public void setImage(BufferedImage image) {
this.image = image;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public void setExpireTime(LocalDateTime expireTime) {
this.expireTime = expireTime;
}
}
注意:在重写构造方法的时候,入参是一个int类型的一个过期时间,就是一个秒,一般过期时间都是60秒,然后在构造方法里面 this.expireTime = LocalDateTime.now().plusSeconds(expireTime);这个代码指的是将过期时间设为一个未来的一个时间。这个类中还有一个判断验证码是否过期的一个方法isExpried。
代码如下
@RestController
public class ValidateCodeController {
private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = createImageCode(request);
sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
private ImageCode createImageCode(HttpServletRequest request) {
int width = 67;
int height = 23;
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 < 4; 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, 60);
}
/**
* 生成随机背景条纹
*
* @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);
}
}
二、在认证流程中加入图形验证码的校验
在SpringSecurity并没有提供图像验证码的过滤器,但是我们可以在过滤器链中加入我们自己写的图形过滤器。就是在UsernamePasswordAuthenticationFilter过滤器之前加一个自己写的过滤器。在自己写的过滤器里面去执行校验的逻辑,如果验证通过则将请求通过,如果验证失败就抛出异常。
代码:
package com.tinner.security.core.validate.code;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
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.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;
public class ValidateCodeFilter extends OncePerRequestFilter {
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
if (StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI()) && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(),"post")){
try {
validate(new ServletWebRequest(httpServletRequest));
}catch (ValidateCodeException e){
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
return ;
}
}
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
/**
* 校验逻辑
* @param
*/
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 (codeInSession == 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 class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg) {
super(msg);
}
private static final long serialVersionUID = 1422465195260228715L;
}
我来继承了AuthenticationException ,这个异常是安全框架默认提供的一个异常。
然后是重写页面index.html
登录
标准登录页面
表单登录
三、配置SpringSecurity的config
package com.tinner.security.browser;
import com.tinner.security.core.properties.SecurityProperties;
import com.tinner.security.core.validate.code.ValidateCodeController;
import com.tinner.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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import java.io.File;
/**
* WebSecurityConfigurerAdapter是springSecurity提供的一个适配器类
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private AuthenticationSuccessHandler tinnerAuthentivationSuccessHandler;
@Autowired
private AuthenticationFailureHandler tinnerAuthentivationFailureHandler;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setAuthenticationFailureHandler(tinnerAuthentivationFailureHandler);
// super.configure(http);
//实现的效果:让它去表单登录,而不是alert框
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/authentication/require")
.loginProcessingUrl("/authentication/form")
.successHandler(tinnerAuthentivationSuccessHandler)
.failureHandler(tinnerAuthentivationFailureHandler)
// http.httpBasic()
.and()
.authorizeRequests()//对请求进行授权
.antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll()
.anyRequest()//任何请求
.authenticated()
.and().csrf().disable();//都需要身份认证
}
}
这样,项目就可以运行了,但是在我登录失败之后它会弹出堆栈信息,并不能显示出我的校验的信息。因此我还得重写我之前的TinnerAuthentivationFailureHandler的onAuthenticationFailure方法,之前是将异常全部写入httpServletResponse中,现在只将异常的message写进 去即可
package com.tinner.security.browser.authentication;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tinner.security.browser.support.SimpleResponse;
import com.tinner.security.core.properties.LoginType;
import com.tinner.security.core.properties.SecurityProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component("tinnerAuthentivationFailureHandler")
//public class TinnerAuthentivationFailureHandler implements AuthenticationFailureHandler {
public class TinnerAuthentivationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
LOGGER.info("登录失败");
if (LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())){
httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(e.getMessage())));
}else{
super.onAuthenticationFailure(httpServletRequest,httpServletResponse,e);
}
}
}