經過一個時序圖來表述以下圖,圖中細化了一下各個控制器和過濾器之間的功能職責,還不是很正規,只爲了更好表達上述的流程描述,因此讀者們將就一下:前端
時序圖 plantUML 代碼java
plantUML 的使用教程請移步至以前發佈過的文章:PlantUML 語法之時序圖git
@startuml
hide footbox
skinparam sequenceMessageAlign center
skinparam sequenceArrowFontSize 11
skinparam noteFontSize 11
skinparam monochrome true
skinparam lifelinestrategy solid
autonumber "<b>[000]"
participant browser as ui
participant imageCodeController as ic
participant UserController as uc
database sessionStorage as session
participant CodeAuthenticationFilter as cf
participant "UsernamePasswordAuthenticationFilter" as uf
ui -> ic: 登陸請求
ic -> ic: 生成驗證碼
ic -> session: 保存驗證碼信息
ic --> ui: 驗證碼圖片
...
autonumber "<b>[000]"
ui -> cf: 賬號登陸
cf -> session: 獲取驗證碼
session --> cf: 驗證碼
cf -> cf: 校驗請求驗證碼合法性
cf -> uf: 用戶認證的後續操做
uf --> uc: 返回認證結果
uc --> ui: 用戶登陸成功
@enduml
複製代碼
將上述的邏輯進行任務拆分:隨機驗證碼和圖片生成,生成驗證碼請求Controller
,session存儲器就臨時使用spring-social-web
包中的SessionStrategy
來存儲,驗證碼過濾器,配置過濾器到spring scuerity
github
過濾器中的用戶密碼驗證過濾器以前。web
採用小步快走的開發模式,前端控制器和生成驗證碼的代碼都寫在一塊兒,後期再進行代碼重構, 這裏主要引用了spring-social-web
依賴:spring
<dependency>
<groupId>org.springframework.social</groupId>
<artifactId>spring-social-web</artifactId>
<version>1.1.4.RELEASE</version>
</dependency>
複製代碼
這個包裏面有個很小巧的session
管理工具:SessionStrategy
chrome
考慮到這個 session 在驗證碼過濾器中還得使用,因此自定義了一個配置,直接注入到了spring中:apache
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
@Configuration
public class AppConfig {
@Bean("sessionStrategy")
public SessionStrategy registBean() {
SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
return sessionStrategy;
}
}
複製代碼
這樣,在 Controller 層直接經過@Autowired
引用便可。安全
圖片驗證碼對象須要至少三個屬性:圖片,驗證碼,過時時間。springboot
後期可能還有其餘的驗證形式,可是其中公共的部分:驗證碼和過時時間是能夠抽象出來,這裏爲了演示不作重構。
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ImageCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
public ImageCode(BufferedImage image, String code, int expireIn) {
this.code = code;
this.image = image;
this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
}
public boolean isExpried() {
return LocalDateTime.now().isAfter(getExpireTime());
}
}
複製代碼
在圖片生成代碼中,圖片的尺寸,驗證碼的隨機隨機數長度和過時時間,都設計在了靜態常量類中,固然也能夠作成配置文件。驗證碼的 session 的惟一標識也作成了公共的,以便在驗證碼過濾器中進行校驗時使用:
public class MyConstants {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
// 圖片寬度
public static final int WIDTH = 90;
// 圖片高度
public static final int HEIGHT = 20;
// 驗證碼的位數
public static final int RANDOM_SIZE = 4;
// 驗證碼過時秒數
public static final int EXPIRE_SECOND = 30;
}
複製代碼
生成驗證碼請求Controller
源碼:
import static org.woodwhale.king.commons.MyConstants.EXPIRE_SECOND;
import static org.woodwhale.king.commons.MyConstants.HEIGHT;
import static org.woodwhale.king.commons.MyConstants.RANDOM_SIZE;
import static org.woodwhale.king.commons.MyConstants.SESSION_KEY;
import static org.woodwhale.king.commons.MyConstants.WIDTH;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import org.woodwhale.king.model.ImageCode;
@RestController
public class ValidateCodeController {
@Autowired
private SessionStrategy sessionStrategy;
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
ImageCode imageCode = generate(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
/** * 生成圖形驗證碼 * @param request * @return */
private ImageCode generate(ServletWebRequest request) {
int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width", WIDTH);
int height = ServletRequestUtils.getIntParameter(request.getRequest(), "height", HEIGHT);
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 = "";
int length = ServletRequestUtils.getIntParameter(request.getRequest(), "length", RANDOM_SIZE);
for (int i = 0; i < length; 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, EXPIRE_SECOND);
}
/** * 生成隨機背景條紋 * * @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);
}
}
複製代碼
爲了提高代碼的可擴展性,隨機驗證碼的生成方法generate()
的方法最好是抽成接口,後期可能還有短信驗證碼,三方登陸的驗證碼,這裏僅作演示。上述驗證碼圖片效果:
SpringSecurity
是經過過濾器鏈來進行校驗的,咱們想要驗證圖形驗證碼,因此能夠在認證流程以前,也就是UsernamePasswordAuthenticationFilter
以前進行校驗。
那麼自定義的驗證碼過濾器也須要實現j2EE
的過濾器接口,同時驗證方法validate()
只作了內部方法抽象,後期能夠作成可擴展的抽象接口,這個void
方法可能會拋出異常,這裏的異常設計成了spring security
框架的AuthenticationException
高級抽象異常的子類,爲了就是保證和安全認證的異常同步,後期使用同一個失敗處理器抓取AuthenticationException
類型的異常便可:
import org.springframework.security.core.AuthenticationException;
public class MyException extends AuthenticationException {
private static final long serialVersionUID = 1L;
public MyException(String msg) {
super(msg);
}
}
複製代碼
而上述異常的接收者就是springboot + spring security 學習筆記(一)自定義基本使用及個性化登陸配置裏提到的自定義認證失敗處理器。
import static org.woodwhale.king.commons.MyConstants.SESSION_KEY;
import java.io.IOException;
import java.util.Objects;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.stereotype.Component;
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 org.woodwhale.king.MyException;
import org.woodwhale.king.model.ImageCode;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements Filter {
/** * 驗證碼校驗失敗處理器 */
@Autowired
private AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private SessionStrategy sessionStrategy;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 必須是登陸的post請求才能進行驗證,其餘的直接放行
if(StringUtils.equals("/user/login", request.getRequestURI()) && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
log.info("request : {}", request.getRequestURI());
try {
// 1. 進行驗證碼的校驗
validate(new ServletWebRequest(request));
} catch (AuthenticationException e) {
// 2. 捕獲步驟1中校驗出現異常,交給失敗處理類進行進行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 3. 校驗經過,就放行
filterChain.doFilter(request, response);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// 1. 獲取請求中的驗證碼
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
// 2. 校驗空值狀況
if(StringUtils.isEmpty(codeInRequest)) {
throw new MyException("驗證碼不能爲空");
}
// 3. 獲取服務器session池中的驗證碼
ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, SESSION_KEY);
if(Objects.isNull(codeInSession)) {
throw new MyException("驗證碼不存在");
}
// 4. 校驗服務器session池中的驗證碼是否過時
if(codeInSession.isExpried()) {
sessionStrategy.removeAttribute(request, SESSION_KEY);
throw new MyException("驗證碼過時了");
}
// 5. 請求驗證碼校驗
if(!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new MyException("驗證碼不匹配");
}
// 6. 移除已完成校驗的驗證碼
sessionStrategy.removeAttribute(request, SESSION_KEY);
}
}
複製代碼
細節注意:這個過濾器繼承了OncePerRequestFilter
,目的在於接受 spring 的管理,它能保證咱們的過濾器在一次請求中只被調用一次。
驗證碼的過濾應該在用戶認證過濾以前,因此須要配置在UsernamePasswordAuthenticationFilter
過濾器以前,自定義的ValidateCodeFilter
過濾器因爲配置了@Component("validateCodeFilter")
,因此已經注入到了 spring 中,安全認證配置中直接@Autowired
引用便可。
**注意:**由於在驗證碼Controller 中設置了這個
/code/image
請求路徑,所以要作不需驗證配置,將其加入到.antMatchers()
中。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private ValidateCodeFilter validateCodeFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 將自定義的驗證碼過濾器放置在 UsernamePasswordAuthenticationFilter 以前
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage("/login") // 設置登陸頁面
.loginProcessingUrl("/user/login") // 自定義的登陸接口
.successHandler(myAuthenctiationSuccessHandler)
.failureHandler(myAuthenctiationFailureHandler)
.defaultSuccessUrl("/home").permitAll() // 登陸成功以後,默認跳轉的頁面
.and().authorizeRequests() // 定義哪些URL須要被保護、哪些不須要被保護
.antMatchers("/", "/index", "/user/login", "/code/image").permitAll() // 設置全部人均可以訪問登陸頁面
.anyRequest().authenticated() // 任何請求,登陸後能夠訪問
.and().csrf().disable(); // 關閉csrf防禦
}
}
複製代碼
到此,整個圖片驗證碼的安全認證流程設計就結束了,能夠再回頭看看筆者最開始畫的時序圖,感受仍是很是不專業規範的,這裏輔助說明的草稿,如筆者有設計更好的時序圖,歡迎交流。
我的博客:woodwhale's blog
博客園:木鯨魚的博客