表單驗證碼登錄
表單登錄驗證碼驗證喷好,一般在用戶名翔横、密碼提交登錄前,添加過濾器梗搅,先驗證驗證碼的有效性(開發(fā)中一般用的這種)禾唁,然后再提交用戶名、密碼些膨。文章下面還會使用另一種方法:驗證碼和用戶名蟀俊、密碼一起同時提交登錄。
在Spring Security
中订雾,兩種實現(xiàn)方式為:
- 使用自定義過濾器(
Filter
)肢预,在提交用戶名、密碼前洼哎,先驗證驗證碼的有效性 - 驗證碼和用戶名烫映、密碼一起在
Spring Security
中進(jìn)行驗證
一、驗證碼生成
新建一個包validateCode
放置所有驗證碼相關(guān)的類噩峦。
1.1锭沟、驗證碼實體對象
@Data
public class ValidateCode {
private BufferedImage image;
private String code;
private LocalDateTime expireTime;
/**
* @param expirtSecond 設(shè)置過期時間,單位秒
*/
public ValidateCode(BufferedImage image, String code, int expirtSecond){
this.image = image;
this.code = code;
// expireSecond秒后的時間
this.expireTime = LocalDateTime.now().plusSeconds(expirtSecond);
}
/**
* 驗證碼是否過期
*/
public boolean isExpired(){
return LocalDateTime.now().isAfter(expireTime);
}
}
1.2识补、生成驗證碼:
@Service
public class ValidateCodeCreateService {
public ValidateCode createImageCode() {
// 寬度
// 從請求參數(shù)中獲取數(shù)據(jù)族淮,否則,讀取配置文件配置值
int width = 80;
// 高度
int height = 30;
// 認(rèn)證碼長度
int charLength = 4;
// 過期時間(秒)
int expireTime = 60;
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
// 獲取圖形上下文
Graphics g = image.getGraphics();
// 生成隨機類
Random random = new Random();
// 設(shè)定背景色
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
// 設(shè)定字體
g.setFont(new Font("Times New Roman", Font.PLAIN, 18));
// 隨機產(chǎn)生155條干擾線,使圖象中的認(rèn)證碼不易被其它程序探測到
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);
}
// 取隨機產(chǎn)生的認(rèn)證碼
String sRand = "";
for (int i = 0; i < charLength; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand += rand;
// 將認(rèn)證碼顯示到圖象中
g.setColor(new Color(20 + random.nextInt(110), 20 + random
.nextInt(110), 20 + random.nextInt(110)));
// 調(diào)用函數(shù)出來的顏色相同祝辣,可能是因為種子太接近贴妻,所以只能直接生成
g.drawString(rand, 13 * i + 6, 16);
}
// 圖象生效
g.dispose();
return new ValidateCode(image, sRand, expireTime);
}
/**
* 給定范圍獲得隨機顏色
*/
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);
}
}
驗證碼圖片生成接口
@RestController
public class ValidateCodeController {
@Autowired
private ValidateCodeCreateService validateCodeCreateService;
@GetMapping("/get-validate-code")
public void getImageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 創(chuàng)建驗證碼
ValidateCode validateCode = validateCodeCreateService.createImageCode();
// 將驗證碼放到session中(也可放在Redis中,可設(shè)置過期時間)
request.getSession().setAttribute("validate-code", validateCode);
// 返回驗證碼給前端
ImageIO.write(validateCode.getImage(), "JPEG", response.getOutputStream());
}
}
二蝙斜、登錄頁面配置
修改resources/templates
下登錄頁面名惩,添加驗證碼選項:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
</head>
<body>
<form th:action="@{/my-login}" method="post">
<div><label> 用戶名 : <input type="text" name="username"/> </label></div>
<div><label> 密碼: <input type="password" name="password"/> </label></div>
<div>驗證碼:
<input type="text" class="form-control" name="validateCode" required="required" placeholder="驗證碼">
<img src="get-validate-code" title="看不清,請點我" onclick="refresh(this)" />
</div>
<button type="submit" class="btn">登錄</button>
</form>
<script>
function refresh(obj) { obj.src = "get-validate-code"; }
</script>
</body>
</html>
WebSecurityConfig
配置:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// ...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 獲取驗證碼允許匿名訪問
.antMatchers("/get-validate-code").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user-login").permitAll()
.loginProcessingUrl("/my-login")
// ...
}
}
正常項目已經(jīng)配置好孕荠,啟動項目娩鹉,訪問localhost:8080/hello
跳轉(zhuǎn)到自定義的登錄頁面:
隨便輸入內(nèi)容提交,登錄失敗稚伍,返回:
輸入正確的用戶名弯予、密碼,驗證碼隨意輸入登錄个曙,登錄成功熙涤,返回:
可以看到,這里Spring Security
默認(rèn)只驗證用戶名困檩、密碼,沒有驗證驗證碼是否正確那槽。所以下面開始實現(xiàn)登錄驗證碼驗證悼沿,有以下兩種種實現(xiàn)方式:
- 使用自定義過濾器(
Filter
),在校驗用戶名骚灸、密碼前判斷驗證碼合法性糟趾,驗證通過后,通過用戶名和密碼登錄 - 驗證碼和用戶名甚牲、密碼一起提交到后臺登錄
三义郑、過濾器驗證
原理:在 Spring Security
處理登錄請求前,先驗證驗證碼丈钙,如果正確非驮,放行去登錄;如果不正確雏赦,返回失敗處理劫笙。
2.1、驗證碼過濾器
自定義一個過濾器星岗,OncePerRequestFilter
(該Filter
保證每次請求只過濾一次):
public class ValidateCodeFilter extends OncePerRequestFilter {
// URL正則匹配
private static final PathMatcher pathMatcher = new AntPathMatcher();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 只有登錄請求‘/authentication/form’,并且為'post'請求時填大,才校驗
if ("POST".equals(request.getMethod())
&& pathMatcher.match("/anthentication/form", request.getServletPath())) {
try {
codeValidate(request);
} catch (ValidateCodeException e) {
// 驗證碼不通過,跳到錯誤處理器處理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append(
new ObjectMapper().createObjectNode()
.put("status", "500")
.put("msg", e.getMessage())
.toString());
// 異常后俏橘,不執(zhí)行后面
return;
}
}
doFilter(request, response, filterChain);
}
private void codeValidate(HttpServletRequest request) throws JsonProcessingException {
// 獲取到傳入的驗證碼
String codeInRequest = request.getParameter("validateCode");
ValidateCode codeInSession = (ValidateCode) request.getSession(false).getAttribute("validate-code");
// 校驗驗證碼是否正確
if (StringUtils.isEmpty(codeInRequest)) {
throw new ValidateCodeException("驗證碼的值不能為空");
}
if (codeInSession == null) {
throw new ValidateCodeException("驗證碼不存在");
}
if (codeInSession.isExpired()) {
throw new ValidateCodeException("驗證碼已過期");
}
if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
throw new ValidateCodeException("驗證碼不匹配");
}
// 校驗正確后允华,移除session中驗證碼
request.getSession(false).removeAttribute("validate-code");
}
}
class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String message) {
super(message);
}
}
2.2、配置過濾器
Spring Security
對于用戶名/密碼登錄驗證是通過 UsernamePasswordAuthenticationFilter
處理的,只要在它之前執(zhí)行驗證碼過濾器即可:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 驗證碼過濾器在用戶名靴寂、密碼校驗前
.addFilterBefore(new ValidateCodeFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/get-validate-code").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user-login").permitAll()
.loginProcessingUrl("/my-login")
}
}
2.4磷蜀、運行程序
啟動項目,訪問localhost:8080/login
到登錄頁榨汤,隨機輸入內(nèi)容登錄:
點擊登錄后蠕搜,后臺驗證驗證碼錯誤,顯示如下:
輸入正確的驗證碼收壕,而用戶名妓灌、密碼錯誤:
全部正確時,返回用戶信息:
四蜜宪、和用戶名虫埂、密碼同時驗證
上面使用過濾器實現(xiàn)了驗證碼功能,該過濾器是先驗證驗證碼圃验,驗證成功就讓 Spring Security
驗證用戶名和密碼掉伏。
如果用戶登錄是需要多個登錄字段,不單單是用戶名和密碼澳窑,這時候可以考慮自定義 Spring Security
的驗證邏輯斧散。
3.1、WebAuthenticationDetails
Spring security
默認(rèn)只會處理用戶名和密碼信息摊聋,如果我們需要增加驗證碼字段驗證鸡捐,則需要拿到驗證碼。而WebAuthenticationDetails
類提供了獲取用戶登錄時攜帶的額外信息的功能麻裁,可以通過該類拿到驗證碼箍镜。所以我們需要自定義類繼承該類拿到驗證碼:
public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {
@Getter // 設(shè)置getter方法,以便拿到驗證碼
private final String validateCode;
public CustomWebAuthenticationDetails(HttpServletRequest request) {
super(request);
// 拿頁面?zhèn)鱽淼尿炞C碼
validateCode = request.getParameter("validateCode");
}
}
3.2煎源、AuthenticationDetailSource
把自定義CustomWebAuthenticationDetails
色迂,放入 AuthenticationDetailsSource
中來替換原本的 WebAuthenticationDetails
,因此還得實現(xiàn)自定義 CustomAuthenticationDetailsSource
手销,設(shè)置為我們自定義的 CustomWebAuthenticationDetails
:
@Component("authenticationDetailsSource")
public class CustomAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest httpRequest) {
return new CustomWebAuthenticationDetails(httpRequest);
}
}
3.3歇僧、Spring Security配置
將 CustomAuthenticationDetailsSource
注入Spring Security
中,替換掉默認(rèn)的 AuthenticationDetailsSource
锋拖。
修改 WebSecurityConfig馏慨,將其注入,然后在config()中使用 authenticationDetailsSource(authenticationDetailsSource)方法來指定它姑隅。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 省略其他
@Autowired
private AuthenticationDetailsSource authenticationDetailsSource;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/get-validate-code").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user-login").permitAll()
.loginProcessingUrl("/my-login")
.authenticationDetailsSource(authenticationDetailsSource);
http.csrf().disable();
}
}
3.4写隶、AuthenticationProvider
通過自定義CustomWebAuthenticationDetails
和CustomAuthenticationDetailsSource
將驗證碼和用戶名、密碼一起加入了Spring Security
中讲仰,但默認(rèn)的認(rèn)證中還不會對驗證碼進(jìn)行校驗慕趴,需要重寫UserDetailsAuthenticationProvider
進(jìn)行校驗。
@Component
public class CustomAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 獲取登錄提交的用戶名和密碼
String inputPassword = (String) authentication.getCredentials();
// 獲取登錄提交的驗證碼
CustomWebAuthenticationDetails details = (CustomWebAuthenticationDetails) authentication.getDetails();
String validateCode = details.getValidateCode();
// 驗證碼校驗
checkValidateCode(validateCode);
// 驗證用戶名
if (!passwordEncoder.matches(inputPassword, userDetails.getPassword())) {
throw new BadCredentialsException("密碼錯誤");
}
}
@Override
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) throws AuthenticationException {
return userDetailsService.loadUserByUsername(username);
}
private void checkValidateCode(String validateCode) {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
ValidateCode codeInSession = (ValidateCode) request.getSession(false).getAttribute("validate-code");
if (StringUtils.isEmpty(validateCode)) {
throw new ValidateCodeException("驗證碼的值不能為空");
}
if (codeInSession == null) {
throw new ValidateCodeException("驗證碼不存在");
}
if (codeInSession.isExpired()) {
// 移除session中驗證碼
request.getSession(false).removeAttribute("validate-code");
throw new ValidateCodeException("驗證碼已過期");
}
if (!StringUtils.equals(codeInSession.getCode(), validateCode)) {
throw new ValidateCodeException("驗證碼不匹配");
}
// 移除session中驗證碼
request.getSession(false).removeAttribute("validate-code");
}
}
class ValidateCodeException extends AuthenticationException {
ValidateCodeException(String message) {
super(message);
}
}
在 WebSecurityConfig
中將其注入,并在 configure(AuthenticationManagerBuilder auth)
方法中通過 auth.authenticationProvider()
指定使用
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomAuthenticationProvider authenticationProvider;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// auth.userDetailsService(userDetailsService);
auth.authenticationProvider(authenticationProvider);
}
}
啟動程序測試即可冕房。