?? 上一篇文章中我們對SpringSecurity認(rèn)證流程源碼進(jìn)行了講解。本章我們講解一下SpringSecurity的圖片驗(yàn)證碼。
?? 實(shí)現(xiàn)圖形驗(yàn)證碼功能要有兩個(gè)步驟:
?? 1.開發(fā)生成圖形驗(yàn)證碼接口
?? 2.在認(rèn)證流程中加入圖形驗(yàn)證碼校驗(yàn)
1.開發(fā)生成圖形驗(yàn)證碼接口
1.1驗(yàn)證碼信息封裝類
驗(yàn)證碼要包含圖片,code,還有超時(shí)時(shí)間3個(gè)要素,考慮到我們的browser模塊和APP模塊都會用到驗(yàn)證碼信息,所以我們把這塊代碼放入到core模塊中
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 ImageCode(BufferedImage image, String code, LocalDateTime expireTime) {
this.image = image;
this.code = code;
this.expireTime = expireTime;
}
public boolean isExpired() {
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;
}
}
1.2驗(yàn)證碼生成器接口
public interface ValidateCodeGenerator {
ImageCode generate(ServletWebRequest request);
}
就一個(gè)驗(yàn)證碼生成方法,這里為什么要定義接口呢,因?yàn)楸菊鹿?jié)中我們只講解圖形驗(yàn)證碼,后面我們可能還有短信驗(yàn)證碼,所以這里必須要以接口的形式提供。
1.3驗(yàn)證碼生成器實(shí)現(xiàn)類
public class ImageCodeGenerator implements ValidateCodeGenerator {
@Autowired
private SecurityProperties securityProperties;
@Override
public ImageCode generate(ServletWebRequest request) {
int width = ServletRequestUtils.getIntParameter(request.getRequest(), "width",
securityProperties.getCode().getImage().getLength());
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());
}
/**
* 生成隨機(jī)背景條紋
*
* @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;
}
}
這里面包含了驗(yàn)證碼的生成邏輯,另外我們看到還引入了SecurityProperties這個(gè)類,這個(gè)類主要是包含了對圖形驗(yàn)證碼的一些配置。我們可以在1.4中看看SecurityProperties的一些信息
1.4SecurityProperties
@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
private BrowserProperties browser = new BrowserProperties();
private ValidateCodeProperties code = new ValidateCodeProperties();
public BrowserProperties getBrowser() {
return browser;
}
public void setBrowser(BrowserProperties browser) {
this.browser = browser;
}
public ValidateCodeProperties getCode() {
return code;
}
public void setCode(ValidateCodeProperties code) {
this.code = code;
}
}
BrowserProperties是上一章節(jié)中講解的配置了,這里我們還引入了ValidateCodeProperties是驗(yàn)證碼的配置信息,但是剛才我們也提到了驗(yàn)證碼有短信驗(yàn)證碼,圖形驗(yàn)證碼之分,所以ValidateCodeProperties里面還會封裝一層信息,見1.5
1.5ValidateCodeProperties
public class ValidateCodeProperties {
/**
* 圖片驗(yàn)證碼配置
*/
private ImageCodeProperties image = new ImageCodeProperties();
public ImageCodeProperties getImage() {
return image;
}
public void setImage(ImageCodeProperties image) {
this.image = image;
}
}
1.6ImageCodeProperties
public class ImageCodeProperties {
/**
* 圖片寬
*/
private int width = 67;
/**
* 圖片高
*/
private int height = 23;
private int length = 4;
private int expireIn = 60;
private String url;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public int getExpireIn() {
return expireIn;
}
public void setExpireIn(int expireIn) {
this.expireIn = expireIn;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
??首先這個(gè)配置項(xiàng)包含了驗(yàn)證碼圖片的寬度、高度,驗(yàn)證碼位數(shù),驗(yàn)證碼的超時(shí)時(shí)間,為什么有這么一個(gè)配置項(xiàng),主要是為了讓demo用戶可以對這些選項(xiàng)做到靈活配置。那么它和1.1中的ImageCode又有什么區(qū)別呢?區(qū)別在于ImageCodeGenerator會根據(jù)ImageCodeProperties配置來生成ImageCode信息??!
1.7ValidateBeanConfig
@Configuration
public class ValidateBeanConfig {
@Autowired
private SecurityProperties securityProperties;
@Bean
@ConditionalOnMissingBean(name = "imageCodeGenertor")
public ValidateCodeGenerator imageCodeGenerator() {
ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
codeGenerator.setSecurityProperties(securityProperties);
return codeGenerator;
}
}
??這個(gè)類主要是驗(yàn)證碼生成的配置類,為什么要做這么一想配置,是因?yàn)橛袝r(shí)候我們希望讓用戶在demo里面建立自己的驗(yàn)證碼生成器。只要它把生成器的名字命名為imageCodeGenertor。這么做的好處是什么呢?當(dāng)我們想擁有新的驗(yàn)證碼生成器的時(shí)候可以不用改舊的代碼,而是讓用戶自己重寫即可,可以做到代碼的無污染和改動,一個(gè)好的架構(gòu)就是這樣形成滴~~
1.8ValidateCodeController
我們的html頁面在初始化的時(shí)候必須訪問Controller來生成圖形驗(yàn)證碼,此時(shí)我們需要提供一個(gè)Controller來完成這段邏輯,Controller會用到ValidateCodeGenerator來生成驗(yàn)證碼,代碼如下:
@RestController
public class ValidateCodeController {
public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
/**
* 創(chuàng)建驗(yàn)證碼,根據(jù)驗(yàn)證碼類型不同,調(diào)用不同的 {@link ValidateCodeProcessor}接口實(shí)現(xiàn)
*
* @param request
* @param response
* @param type
* @throws Exception
*/
@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
}
}
為什么我們的驗(yàn)證碼的輸入?yún)?shù)是一個(gè)request請求呢?這是因?yàn)槲覀兿M脩舫四軌蛟谒麄兊膽?yīng)用中配置驗(yàn)證碼的參數(shù)外,我們還希望在請求的時(shí)候也能修改這些參數(shù),我們的思路如下圖所示:

好了驗(yàn)證碼的生成接口我們就介紹完成了,那么我們?nèi)绾斡玫剿兀?/p>
2.在認(rèn)證流程中加入圖形驗(yàn)證碼校驗(yàn)
2.1使用過濾器來完成對圖形驗(yàn)證碼的校驗(yàn)--ValidateCodeFilter
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AuthenticationFailureHandler authenticationFailureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
private Set<String> urls = new HashSet<>();
private SecurityProperties securityProperties;
private AntPathMatcher pathMatcher = new AntPathMatcher();
@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);
}
@Override
public void afterPropertiesSet() throws ServletException {
super.afterPropertiesSet();
String[] configUrls = StringUtils
.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(), ",");
for (String configUrl : configUrls) {
urls.add(configUrl);
}
urls.add("/authentication/form");
}
private 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("驗(yàn)證碼的值不能為空");
}
if (codeInSession == null) {
throw new ValidateCodeException("驗(yàn)證碼不存在");
}
if (codeInSession.isExpired()) {
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
throw new ValidateCodeException("驗(yàn)證碼已過期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("驗(yàn)證碼不匹配");
}
sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
}
public AuthenticationFailureHandler getAuthenticationFailureHandler() {
return authenticationFailureHandler;
}
public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler;
}
public SessionStrategy getSessionStrategy() {
return sessionStrategy;
}
public void setSessionStrategy(SessionStrategy sessionStrategy) {
this.sessionStrategy = sessionStrategy;
}
public Set<String> getUrls() {
return urls;
}
public void setUrls(Set<String> urls) {
this.urls = urls;
}
public SecurityProperties getSecurityProperties() {
return securityProperties;
}
public void setSecurityProperties(SecurityProperties securityProperties) {
this.securityProperties = securityProperties;
}
}
這個(gè)過濾器繼承了OncePerRequestFilter,這個(gè)Filter里面有這些成員變量:
- authenticationFailureHandler
主要用于圖形驗(yàn)證碼校驗(yàn)失敗后的處理邏輯 - sessionStrategy
驗(yàn)證碼驗(yàn)證完畢后,我們必須將它從session中remove掉 - Set<String> urls
定義哪些訪問的url必須走驗(yàn)證碼邏輯。我們這里設(shè)置了對/user和/authentication/form的請求必須帶驗(yàn)證碼。其余的請求不需要提供驗(yàn)證碼。
Filter的驗(yàn)證邏輯有判斷前臺傳遞的驗(yàn)證碼的值是否為空、Session中是否有驗(yàn)證碼、驗(yàn)證碼是否過期、驗(yàn)證碼是否匹配。如果發(fā)生異常則會拋出ValidateCodeException
2.2ValidateCodeException
public class ValidateCodeException extends AuthenticationException {
/**
*
*/
private static final long serialVersionUID = -7285211528095468156L;
public ValidateCodeException(String msg) {
super(msg);
}
}
這個(gè)異常繼承了AuthenticationException,這樣的話可以被失敗處理器處理
2.3BrowserSecurityConfig

這個(gè)配置里面將驗(yàn)證碼的Filter設(shè)置在了用戶名密碼FIlter的前面,另外也排除了/code/image路徑的攔截
2.4signIn.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<h2>標(biāo)準(zhǔn)登錄頁面</h2>
<h3>表單登錄</h3>
<form action="/authentication/form" method="post">
<table>
<tr>
<td>用戶名:</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密碼:</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>圖形驗(yàn)證碼:</td>
<td><input type="text" name="imageCode"> <img
src="/code/image?width=200"></td>
</tr>
<tr>
<td colspan="2"><button type="submit">登錄</button></td>
</tr>
</table>
</form>
</body>
</html>
2.5驗(yàn)證訪問


輸入完驗(yàn)證碼后,成功實(shí)現(xiàn)了跳轉(zhuǎn)