SpringSceurity(3)—圖形驗證碼功能實現
- 2020 年 6 月 7 日
- 筆記
- 【框架】-- SpringSecurity
SpringSceurity(3)—圖形驗證碼功能實現
有關springSceurity之前有寫過兩篇文章:
1、SpringSecurity(1)—認證+授權程式碼實現
這篇我們來講圖形驗證碼功能實現。
一、思路
我整理下springSceurity整合圖形驗證碼的大致思路:
1、首先對於驗證碼本身而言,應該有三部分組成 1、存放驗證碼的背景圖片 2、驗證碼 3、驗證碼的有效時間。
2、對於springSceurity而言,驗證碼的執行校驗順序肯定是在UsernamePasswordAuthenticationFilter之前的,因為如果驗證碼都不對,那麼
根本都不需要驗證帳號密碼。所以我們需要自定義一個驗證碼過濾器,並且配置在UsernamePasswordAuthenticationFilter之前執行。
3、對於獲取驗證碼的介面,肯定是不需要進行認證限制的。
4、對於獲取驗證碼的介面的時候,需要把該驗證碼資訊+當前瀏覽器的SessonId綁定在一起存在Seesion中,為了後面校驗的時候通過SessonId
去取這個驗證碼資訊。
5、登陸請求介面,除了帶上用戶名和密碼之外,還需要帶上驗證碼資訊。在進入驗證碼過濾器的時候,首先通過SessonId獲取存在Sesson中的
驗證碼資訊,拿到驗證碼資訊之後首先還要校驗該驗證碼是否在有效期內。之後再和當前登陸介面帶來的驗證碼進行對比,如果一致,那麼當前
驗證碼這一關就過了,就開始驗證下一步帳號和密碼是否正確了。
整個流程大致就是這樣。下面現在是具體程式碼,然後進行測試。
二、程式碼展示
這裡只展示一些核心程式碼,具體完整項目會放到github上。
1、ImageCodeProperties
這個是一個bean實體,是一個圖形驗證碼的默認配置。
@Data
public class ImageCodeProperties {
/**
* 驗證碼寬度
*/
private int width = 67;
/**
* 驗證碼高度
*/
private int height = 23;
/**
* 驗證碼長度
*/
private int length = 4;
/**
* 驗證碼過期時間
*/
private int expireIn = 60;
/**
* 需要驗證碼的請求url字元串,用英文逗號隔開
*/
private String url = "/login";
}
2、ImageCode
這個是圖片驗證碼的完整資訊,也會將這個完整資訊存放於Sesson中。
圖片驗證碼資訊 由三部分組成 :
1.圖片資訊(長、寬、背景色等等)。2.code就是真正的驗證碼,用來驗證用。3.該驗證碼的有效時間。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireIn) {
this.image = image;
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
/**
* 校驗是否過期
*/
public boolean isExpired() {
return LocalDateTime.now().isAfter(expireTime);
}
}
3、ValidateCodeGeneratorService
獲取驗證碼的介面
public interface ValidateCodeGeneratorService {
/**
* 生成圖片驗證碼
*
* @param request 請求
* @return ImageCode實例對象
*/
ImageCode generate(ServletWebRequest request);
}
4、ImageCodeGeneratorServiceImpl
獲取圖片驗證碼的介面的實現類
@Data
public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {
private static final String IMAGE_WIDTH_NAME = "width";
private static final String IMAGE_HEIGHT_NAME = "height";
private static final Integer MAX_COLOR_VALUE = 255;
private ImageCodeProperties imageCodeProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
//設置圖片的寬度和高度
int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, imageCodeProperties.getWidth());
int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME, imageCodeProperties.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);
}
// 生成數字驗證碼
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < imageCodeProperties.getLength(); 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(), imageCodeProperties.getExpireIn());
}
/**
* 生成隨機背景條紋
*
* @param fc 前景色
* @param bc 背景色
* @return RGB顏色
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > MAX_COLOR_VALUE) {
fc = MAX_COLOR_VALUE;
}
if (bc > MAX_COLOR_VALUE) {
bc = MAX_COLOR_VALUE;
}
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);
}
}
5、ValidateCodeController
獲取驗證碼的請求介面。
@RestController
public class ValidateCodeController {
/**
* 前綴
*/
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private static final String FORMAT_NAME = "JPEG";
@Autowired
private ValidateCodeGeneratorService imageCodeGenerator;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 第一步:根據請求生成一個圖形驗證碼對象
ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
// 第二步:將圖形驗證碼對象存到session中,第一個參數可以從傳入的請求中獲取session
sessionStrategy.setAttribute(new ServletRequestAttributes(request), SESSION_KEY, imageCode);
// 第三步:將生成的圖片寫到介面的響應中
ImageIO.write(imageCode.getImage(), FORMAT_NAME, response.getOutputStream());
}
}
到這裡,我們可用請求獲取圖片驗證碼的資訊了。接下來我們就要登陸請求部分的程式碼。
6、ValidateCodeFilter
自定義過濾器,這裡面才是核心的程式碼,首先繼承OncePerRequestFilter(直接繼承Filter也是可用的),實現InitializingBean是為了初始化一些初始數據。
這裡走的邏輯就是把存在session中的圖片驗證碼和當前請求的驗證碼進行比較,如果相同則放行,否則直接拋出異常。
@Data
@Slf4j
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private static final String SUBMIT_FORM_DATA_PATH = "/login";
/**
* 失敗處理器
*/
@Autowired
private AuthenctiationFailHandler authenctiationFailHandler;
/**
* 驗證碼屬性類
*/
@Autowired
private ImageCodeProperties imageCodeProperties;
/**
* 存放需要走驗證碼請求url
*/
private Set<String> urls = new HashSet<>();
/**
* 處理session工具類
*/
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
/**
* 正則配置工具
*/
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 在初始化bean的時候都會執行該方法
*/
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils.split(imageCodeProperties.getUrl(), ",");
// 登錄的鏈接是必須要進行驗證碼驗證的
urls.addAll(Arrays.asList(configUrls));
}
/**
* 攔截請求進來的方法。
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
for (String url : urls) {
// 如果實際訪問的URL可以與用戶在imageCodeProperties中url配置的相同,那麼就進行驗證碼校驗
log.info("request.getRequestURI = {}",request.getRequestURI());
if (antPathMatcher.match(url, request.getRequestURI())) {
action = true;
}
}
//說明需要校驗
if (action) {
try {
validate(new ServletWebRequest(request));
} catch (ValidateCodeException e) {
authenctiationFailHandler.onAuthenticationFailure(request, response, e);
return;
}
}
//進入下一個過濾器
filterChain.doFilter(request, response);
}
/**
* 驗證碼校驗邏輯
*
*/
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 從session中獲取圖片驗證碼
ImageCode imageCodeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
// 從請求中獲取用戶填寫的驗證碼
String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
if (StringUtils.isBlank(imageCodeInRequest)) {
throw new ValidateCodeException("驗證碼不能為空");
}
if (null == imageCodeInSession) {
throw new ValidateCodeException("驗證碼不存在");
}
if (imageCodeInSession.isExpired()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("驗證碼已過期");
}
log.info("session中獲取的驗證碼={},sessionId ={}",imageCodeInSession.getCode(),request.getSessionId());
log.info("登陸操作傳來的驗證碼={}",imageCodeInRequest);
if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, imageCodeInSession.getCode())) {
throw new ValidateCodeException("驗證碼不匹配");
}
// 驗證成功,刪除session中的驗證碼
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
}
7、WebSecurityConfig
SpringSecurity的Java 配置類也需要做一點點改動。那就是需要設置ValidateCodeFilter要在UsernamePasswordAuthenticationFilter之前進行攔截過濾。
到這裡整個圖形驗證碼的功能就開發完成了,具體程式碼放在github上下面進行測試。
三、測試
說明下我這裡懶的寫前端相關程式碼了,所以直接用posman用請求來獲取驗證碼,獲取驗證碼之後再進行登陸操作。
1、獲取驗證碼
這裡驗證碼code為:1848
2、登陸成功
輸入的驗證碼也是1848,顯示登陸成功。
3、登陸失敗
因為配置的時候圖片驗證碼有效期為60秒,所以在我們獲取驗證碼後,過60秒再去登陸,就能發現,驗證碼已過期。
整個驗證碼功能大致就是這樣。
Github地址
: spring-boot-security-study-03
別人罵我胖,我會生氣,因為我心裡承認了我胖。別人說我矮,我就會覺得好笑,因為我心裡知道我不可能矮。這就是我們為什麼會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(19)