spring-security-oauth2(四) 圖片驗證碼
阿新 • • 發佈:2018-12-11
圖片驗證碼
- 圖片驗證碼生成介面
- 認證流程加入圖片驗證碼校驗
- 圖片驗證碼重構
1.圖片驗證碼生成介面
- 呼叫com.google.code.kaptcha.Producer生成圖片驗證碼
- 將隨機數存到session快取中
- 將生成的圖片寫到響應流中
圖片驗證碼封裝類 ImageCaptchaVo
package com.rui.tiger.auth.core.captcha; import lombok.Data; import java.awt.image.BufferedImage; import java.time.LocalDateTime; /** * 圖片驗證碼資訊物件 * @author CaiRui * @Date 2018/12/9 18:03 */ @Data public class ImageCaptchaVo { /** * 圖片驗證碼 */ private BufferedImage image; /** * 驗證碼 */ private String code; /** * 失效時間 這個通常放在快取中或維護在資料庫中 */ private LocalDateTime expireTime; public ImageCaptchaVo(BufferedImage image, String code, int expireAfterSeconds){ this.image = image; this.code = code; //多少秒後 this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds); } public ImageCaptchaVo(BufferedImage image, String code, LocalDateTime expireTime){ this.image = image; this.code = code; this.expireTime = expireTime; } /** * 是否失效 * @return */ public boolean isExpried() { return LocalDateTime.now().isAfter(expireTime); } }
圖片驗證碼服務類 CaptchaController 注意這個路徑/captcha/image要在BrowserSecurityConfig配置中放行
package com.rui.tiger.auth.core.captcha; 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.annotation.GetMapping; 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.io.IOException; /** * 驗證碼控制器 * @author CaiRui * @date 2018-12-10 12:13 */ @RestController public class CaptchaController { public static final String IMAGE_CAPTCHA_SESSION_KEY="image_captcha_session_key"; private static final String FORMAT_NAME="JPEG"; @Autowired private CaptchaGenerate captchaGenerate; //spring session 工具類 private SessionStrategy sessionStrategy=new HttpSessionSessionStrategy(); /** * 獲取圖片驗證碼 * @param request * @param response * @throws IOException */ @GetMapping("/captcha/image") public void createKaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException { //1.介面生成驗證碼 ImageCaptchaVo imageCaptcha= (ImageCaptchaVo) captchaGenerate.generate(); //2.儲存到session中 sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CAPTCHA_SESSION_KEY, imageCaptcha); //3.寫到響應流中 response.setHeader("Cache-Control", "no-store, no-cache");// 沒有快取 response.setContentType("image/jpeg"); ImageIO.write(imageCaptcha.getImage(),FORMAT_NAME,response.getOutputStream()); } }
CaptchaGenerate 介面及其實現類
package com.rui.tiger.auth.core.captcha;
/**
* 驗證碼生成介面
*
* @author CaiRui
* @date 2018-12-10 12:03
*/
public interface CaptchaGenerate {
/**
* 生成圖片驗證碼
*
* @return
*/
ImageCaptchaVo generate();
}
package com.rui.tiger.auth.core.captcha; import com.google.code.kaptcha.Producer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.awt.image.BufferedImage; /** * 圖片驗證碼生成介面 * * @author CaiRui * @date 2018-12-10 12:07 */ @Service("imageCaptchaGenerate") public class ImageCaptchaGenerate implements CaptchaGenerate { @Autowired private Producer producer;//config bean中配置 @Override public ImageCaptchaVo generate() { String code = producer.createText(); BufferedImage bufferedImage = producer.createImage(code); return new ImageCaptchaVo(bufferedImage, code, 60 * 5);//5分鐘過期 } }
Producer配置類 KaptchaGenerateConfig
package com.rui.tiger.auth.core.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/**
* 驗證碼生成配置類
* @author CaiRui
* @date 2018-12-10 12:09
*/
@Configuration
public class KaptchaGenerateConfig {
//TODO 配置項放在配置檔案中
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "5");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2.認證流程加入圖片驗證碼校驗
通過上篇的原始碼分析spring-security原理,其實就是過濾器鏈上的各個過濾器協同工作,思路如下:
- 編寫我們的自定義圖片驗證碼過濾器
- 將它放在UsernamePasswordAuthenticationFilter表單過濾器之前
驗證碼過濾器
package com.rui.tiger.auth.core.captcha;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang.StringUtils;
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;
/**
* 圖片驗證碼過濾器
* OncePerRequestFilter 過濾器只會呼叫一次
*
* @author CaiRui
* @date 2018-12-10 12:23
*/
public class CaptchaFilter extends OncePerRequestFilter {
//一般在配置類中進行注入
@Setter
@Getter
private AuthenticationFailureHandler failureHandler;
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//表單登入的post請求
if (StringUtils.equals("/authentication/form", request.getRequestURI())
&& StringUtils.equalsIgnoreCase("post", request.getMethod())) {
try {
validate(request);
} catch (CaptchaException captchaException) {
//失敗呼叫我們的自定義失敗處理器
failureHandler.onAuthenticationFailure(request, response, captchaException);
//後續流程終止
return;
}
}
filterChain.doFilter(request, response);
}
/**
* 圖片驗證碼校驗
*
* @param request
*/
private void validate(HttpServletRequest request) throws ServletRequestBindingException {
// 拿到之前儲存的imageCode資訊
ServletWebRequest swr = new ServletWebRequest(request);
ImageCaptchaVo imageCodeInSession = (ImageCaptchaVo) sessionStrategy.getAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new CaptchaException("驗證碼的值不能為空");
}
if (imageCodeInSession == null) {
throw new CaptchaException("驗證碼不存在");
}
if (imageCodeInSession.isExpried()) {
sessionStrategy.removeAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
throw new CaptchaException("驗證碼已過期");
}
if (!StringUtils.equals(imageCodeInSession.getCode(), codeInRequest)) {
throw new CaptchaException("驗證碼不匹配");
}
//驗證通過 移除快取
sessionStrategy.removeAttribute(swr, CaptchaController.IMAGE_CAPTCHA_SESSION_KEY);
}
}
自定義驗證碼異常
package com.rui.tiger.auth.core.captcha;
import org.springframework.security.core.AuthenticationException;
/**
* 自定義驗證碼異常
* @author CaiRui
* @date 2018-12-10 12:43
*/
public class CaptchaException extends AuthenticationException {
public CaptchaException(String msg, Throwable t) {
super(msg, t);
}
public CaptchaException(String msg) {
super(msg);
}
}
將過濾器加入到瀏覽器許可權配置中
package com.rui.tiger.auth.browser.config;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationFailureHandler;
import com.rui.tiger.auth.core.authentication.TigerAuthenticationSuccessHandler;
import com.rui.tiger.auth.core.captcha.CaptchaFilter;
import com.rui.tiger.auth.core.properties.SecurityProperties;
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.UsernamePasswordAuthenticationFilter;
/**
* 瀏覽器security配置類
*
* @author CaiRui
* @date 2018-12-4 8:41
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SecurityProperties securityProperties;
@Autowired
private TigerAuthenticationFailureHandler tigerAuthenticationFailureHandler;
@Autowired
private TigerAuthenticationSuccessHandler tigerAuthenticationSuccessHandler;
/**
* 密碼加密解密
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//加入圖片驗證碼過濾器
CaptchaFilter captchaFilter=new CaptchaFilter();
captchaFilter.setFailureHandler(tigerAuthenticationFailureHandler);
//圖片驗證碼放在認證之前
http.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class)
.formLogin()
.loginPage( "/authentication/require")//自定義登入請求
.loginProcessingUrl("/authentication/form")//自定義登入表單請求
.successHandler(tigerAuthenticationSuccessHandler)
.failureHandler(tigerAuthenticationFailureHandler)
.and()
.authorizeRequests()
.antMatchers(securityProperties.getBrowser().getLoginPage(),
"/authentication/require","/captcha/image")//此路徑放行 否則會陷入死迴圈
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()//跨域關閉
;
}
}
前段標準登入介面加入驗證碼改造
<html>
<head>
<meta charset="UTF-8">
<title>登入</title>
</head>
<body>
<h2>標準登入頁面</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>圖形驗證碼:</td>
<td>
<input type="text" name="imageCode">
<img src="/captcha/image">
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登入</button></td>
</tr>
</table>
</form>
</body>
</html>
ok 我們專案重新啟動下 來看下自定義的驗證碼過濾器是否可用,直接瀏覽器輸入我們的登入地址http://localhost:8070/tiger-login.html
可以看到圖片驗證碼已經成功顯示出來了,我們來看看驗證邏輯是否可用,試下不輸入驗證碼
這是因為我們自定義的失敗處理器,列印了全部的錯誤堆疊資訊我們來調整下,調整後如下
package com.rui.tiger.auth.core.authentication;
import com.alibaba.fastjson.JSON;
import com.rui.tiger.auth.core.model.enums.LoginTypeEnum;
import com.rui.tiger.auth.core.properties.SecurityProperties;
import com.rui.tiger.auth.core.support.SimpleResponse;
import lombok.extern.slf4j.Slf4j;
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;
/**
* 認證失敗處理器
* @author CaiRui
* @date 2018-12-6 12:40
*/
@Component("tigerAuthenticationFailureHandler")
@Slf4j
public class TigerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private SecurityProperties securityProperties;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.info("登入失敗");
if (LoginTypeEnum.JSON.equals(securityProperties.getBrowser().getLoginType())) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(new SimpleResponse(exception.getMessage())));
} else {
// 如果使用者配置為跳轉,則跳到Spring Boot預設的錯誤頁面
super.onAuthenticationFailure(request, response, exception);
}
}
}
這次我們試下一個錯誤的驗證碼登入看下
ok 我們的自定義驗證碼生效,其它的情況可以自行除錯 下面我們將對驗證碼進行重構
3.圖片驗證碼重構
- 驗證碼生成的基本引數可以配置
- 驗證碼攔截的介面可以配置
- 驗證碼的生成邏輯可以配置