本文在前文 Spring Security 入門(一):認證和原理分析 的基礎上介紹圖形驗證碼和手機短信驗證碼登錄的實現。
圖形驗證碼
在用戶登錄時,一般通過表單的方式進行登錄都會要求用戶輸入驗證碼,Spring Security
默認沒有實現圖形驗證碼的功能,所以需要我們自己實現。
實現流程分析

前文中實現的用戶名、密碼登錄是在UsernamePasswordAuthenticationFilter
過濾器進行認證的,而圖形驗證碼一般是在用戶名、密碼認證之前進行驗證的,所以需要在UsernamePasswordAuthenticationFilter
過濾器之前添加一個自定義過濾器 ImageCodeValidateFilter,用來校驗用戶輸入的圖形驗證碼是否正確。自定義過濾器繼承 OncePerRequestFilter 類,該類是 Spring 提供的在一次請求中只會調用一次的 filter。

自定義的過濾器 ImageCodeValidateFilter 首先會判斷請求是否為 POST 方式的登錄表單提交請求,如果是就將其攔截進行圖形驗證碼校驗。如果驗證錯誤,會拋出自定義異常類對象 ValidateCodeException,該異常類需要繼承 AuthenticationException 類。在自定義過濾器中,我們需要手動捕獲自定義異常類對象,並將捕獲到自定義異常類對象交給自定義失敗處理器進行處理。

kpatcha 使用
Kaptcha 是谷歌提供的生成圖形驗證碼的工具,參考地址為:https://github.com/penggle/kaptcha,依賴如下:
<!-- 圖形驗證碼工具 kaptcha -->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
☕ 創建 KaptchaConfig 配置類
package com.example.config;
import com.google.code.kaptcha.Constants;
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;
/**
* 圖形驗證碼的配置類
*/
@Configuration
public class KaptchaConfig {
@Bean
public DefaultKaptcha captchaProducer() {
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
Properties properties = new Properties();
// 是否有邊框
properties.setProperty(Constants.KAPTCHA_BORDER, "yes");
// 邊框顏色
properties.setProperty(Constants.KAPTCHA_BORDER_COLOR, "192,192,192");
// 驗證碼圖片的寬和高
properties.setProperty(Constants.KAPTCHA_IMAGE_WIDTH, "110");
properties.setProperty(Constants.KAPTCHA_IMAGE_HEIGHT, "40");
// 驗證碼顏色
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_COLOR, "0,0,0");
// 驗證碼字體大小
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_FONT_SIZE, "32");
// 驗證碼生成幾個字符
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_LENGTH, "4");
// 驗證碼隨機字符庫
properties.setProperty(Constants.KAPTCHA_TEXTPRODUCER_CHAR_STRING, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYAZ");
// 驗證碼圖片默認是有線條干擾的,我們設置成沒有干擾
properties.setProperty(Constants.KAPTCHA_NOISE_IMPL, "com.google.code.kaptcha.impl.NoNoise");
Config config = new Config(properties);
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
kaptcha 配置的參數說明(定義在 Constants 常量類中):
kaptcha.border
:是否有圖片邊框,合法值:yes,no;默認值為 yes。kaptcha.border.color
:邊框顏色,合法值:rgb 或者 white,black,blue;默認值為 black。kaptcha.border.thickness
邊框厚度,合法值:>0;默認為 1。kaptcha.image.width
:圖片寬,默認值為 200px。kaptcha.image.height
:圖片高,默認值為 50px。kaptcha.producer.impl
:圖片實現類,默認值為com.google.code.kaptcha.impl.DefaultKaptcha
。kaptcha.textproducer.impl
:文本實現類,默認值為com.google.code.kaptcha.text.impl.DefaultTextCreator
。kaptcha.textproducer.char.string
:文本集合,驗證碼值從此集合中獲取,默認值為abcde2345678gfynmnpwx
。kaptcha.textproducer.char.length
:驗證碼長度,默認值為 5。kaptcha.textproducer.font.names
:字體,默認值為 Arial, Courier。kaptcha.textproducer.font.size
:字體大小,默認值為 40px。kaptcha.textproducer.font.color
:字體顏色,合法值:rgb 或者 white,black,blue;默認值為black。kaptcha.textproducer.char.space
:文字間隔,默認值為 2px。kaptcha.noise.impl
:干擾實現類,com.google.code.kaptcha.impl.NoNoise
為沒有干擾。默認值為com.google.code.kaptcha.impl.DefaultNoise
。kaptcha.noise.color
:干擾線顏色,合法值:rgb 或者 white,black,blue;默認值為 black。kaptcha.obscurificator.impl
:圖片樣式,合法值:水紋com.google.code.kaptcha.impl.WaterRipple
,魚眼com.google.code.kaptcha.impl.FishEyeGimpy
, 陰影com.google.code.kaptcha.impl.ShadowGimpy
;默認值為com.google.code.kaptcha.impl.WaterRipple
。kaptcha.background.impl
:背景實現類,默認值為com.google.code.kaptcha.impl.DefaultBackground
。kaptcha.background.clear.from
:背景顏色漸變,開始顏色,默認值為light grey
。kaptcha.background.clear.to
:背景顏色漸變, 結束顏色,默認值為 white。kaptcha.word.impl
:文字渲染器 實現類,默認值為com.google.code.kaptcha.text.impl.DefaultWordRenderer
。kaptcha.session.key
:session key,默認值為KAPTCHA_SESSION_KEY
。kaptcha.session.date
:session date,默認值為KAPTCHA_SESSION_DATE
。
☕ 創建驗證碼的實體類 CheckCode
package com.example.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
public class CheckCode implements Serializable {
private String code; // 驗證碼字符
private LocalDateTime expireTime; // 過期時間
/**
* @param code 驗證碼字符
* @param expireTime 過期時間,單位秒
*/
public CheckCode(String code, int expireTime) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
}
public CheckCode(String code) {
// 默認驗證碼 60 秒后過期
this(code, 60);
}
// 是否過期
public boolean isExpried() {
return this.expireTime.isBefore(LocalDateTime.now());
}
public String getCode() {
return this.code;
}
}
☕ 在 LoginController 中添加獲取圖形驗證碼的 Controller 方法
package com.example.constans;
public class Constants {
// Session 中存儲圖形驗證碼的屬性名
public static final String KAPTCHA_SESSION_KEY = "KAPTCHA_SESSION_KEY";
}
@Controller
public class LoginController {
@Autowired
private DefaultKaptcha defaultKaptcha;
//...
@GetMapping("/code/image")
public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 創建驗證碼文本
String capText = defaultKaptcha.createText();
// 創建驗證碼圖片
BufferedImage image = defaultKaptcha.createImage(capText);
// 將驗證碼文本放進 Session 中
CheckCode code = new CheckCode(capText);
request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, code);
// 將驗證碼圖片返回,禁止驗證碼圖片緩存
response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
ImageIO.write(image, "jpg", response.getOutputStream());
}
}
☕ 在 login.html 中添加驗證碼功能
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄</title>
</head>
<body>
<h3>表單登錄</h3>
<form method="post" th:action="@{/login/form}">
<input type="text" name="name" placeholder="用戶名"><br>
<input type="password" name="pwd" placeholder="密碼"><br>
<input name="imageCode" type="text" placeholder="驗證碼"><br>
<img th:onclick="this.src='/code/image?'+Math.random()" th:src="@{/code/image}" alt="驗證碼"/><br>
<div th:if="${param.error}">
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用戶名或密碼錯誤</span>
</div>
<button type="submit">登錄</button>
</form>
</body>
</html>
☕ 更改安全配置類 SpringSecurityConfig,設置訪問/code/image
不需要任何權限
@EnableWebSecurity // 開啟 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
/**
* 定制基於 HTTP 請求的用戶訪問控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 開啟基於 HTTP 請求訪問控制
http.authorizeRequests()
// 以下訪問不需要任何權限,任何人都可以訪問
.antMatchers("/login/page", "/code/image").permitAll()
// 以下訪問需要 ROLE_ADMIN 權限
.antMatchers("/admin/**").hasRole("ADMIN")
// 以下訪問需要 ROLE_USER 權限
.antMatchers("/user/**").hasAuthority("ROLE_USER")
// 其它任何請求訪問都需要先通過認證
.anyRequest().authenticated();
//...
}
//...
}
☕ 測試
訪問localhost:8080/login/page
,出現圖形驗證的信息

自定義驗證碼過濾器
⭐️ 創建自定義異常類 ValidateCodeException
package com.example.exception;
import org.springframework.security.core.AuthenticationException;
/**
* 自定義驗證碼校驗錯誤的異常類,繼承 AuthenticationException
*/
public class ValidateCodeException extends AuthenticationException {
public ValidateCodeException(String msg, Throwable t) {
super(msg, t);
}
public ValidateCodeException(String msg) {
super(msg);
}
}
⭐ 自定義圖形驗證碼校驗過濾器 ImageCodeValidateFilter
package com.example.config.security;
import com.example.constans.Constants;
import com.example.entity.CheckCode;
import com.example.exception.ValidateCodeException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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 javax.servlet.http.HttpSession;
import java.io.IOException;
@Component
public class ImageCodeValidateFilter extends OncePerRequestFilter {
private String codeParamter = "imageCode"; // 前端輸入的圖形驗證碼參數名
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定義認證失敗處理器
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 非 POST 方式的表單提交請求不校驗圖形驗證碼
if ("/login/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
try {
// 校驗圖形驗證碼合法性
validate(request);
} catch (ValidateCodeException e) {
// 手動捕獲圖形驗證碼校驗過程拋出的異常,將其傳給失敗處理器進行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 放行請求,進入下一個過濾器
filterChain.doFilter(request, response);
}
// 判斷驗證碼的合法性
private void validate(HttpServletRequest request) {
// 獲取用戶傳入的圖形驗證碼值
String requestCode = request.getParameter(this.codeParamter);
if(requestCode == null) {
requestCode = "";
}
requestCode = requestCode.trim();
// 獲取 Session
HttpSession session = request.getSession();
// 獲取存儲在 Session 里的驗證碼值
CheckCode savedCode = (CheckCode) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
if (savedCode != null) {
// 隨手清除驗證碼,無論是失敗,還是成功。客戶端應在登錄失敗時刷新驗證碼
session.removeAttribute(Constants.KAPTCHA_SESSION_KEY);
}
// 校驗出錯,拋出異常
if (StringUtils.isBlank(requestCode)) {
throw new ValidateCodeException("驗證碼的值不能為空");
}
if (savedCode == null) {
throw new ValidateCodeException("驗證碼不存在");
}
if (savedCode.isExpried()) {
throw new ValidateCodeException("驗證碼過期");
}
if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
throw new ValidateCodeException("驗證碼輸入錯誤");
}
}
}
⭐️ 更改安全配置類 SpringSecurityConfig,將自定義過濾器添加過濾器鏈中
@EnableWebSecurity // 開啟 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Autowired
private ImageCodeValidateFilter imageCodeValidateFilter; // 自定義過濾器(圖形驗證碼校驗)
//...
/**
* 定制基於 HTTP 請求的用戶訪問控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 將自定義過濾器(圖形驗證碼校驗)添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
}
//...
}
完整的安全配置類 SpringSecurityConfig 如下:
package com.example.config;
import com.example.config.security.CustomAuthenticationFailureHandler;
import com.example.config.security.CustomAuthenticationSuccessHandler;
import com.example.config.security.ImageCodeValidateFilter;
import com.example.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity // 開啟 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler; // 自定義認證成功處理器
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定義認證失敗處理器
@Autowired
private ImageCodeValidateFilter imageCodeValidateFilter; // 自定義過濾器(圖形驗證碼校驗)
/**
* 密碼編碼器,密碼不能明文存儲
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 使用 BCryptPasswordEncoder 密碼編碼器,該編碼器會將隨機產生的 salt 混入最終生成的密文中
return new BCryptPasswordEncoder();
}
/**
* 定制用戶認證管理器來實現用戶認證
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 采用內存存儲方式,用戶認證信息存儲在內存中
// auth.inMemoryAuthentication()
// .withUser("admin").password(passwordEncoder()
// .encode("123456")).roles("ROLE_ADMIN");
// 不再使用內存方式存儲用戶認證信息,而是動態從數據庫中獲取
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 定制基於 HTTP 請求的用戶訪問控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 啟動 form 表單登錄
http.formLogin()
// 設置登錄頁面的訪問路徑,默認為 /login,GET 請求;該路徑不設限訪問
.loginPage("/login/page")
// 設置登錄表單提交路徑,默認為 loginPage() 設置的路徑,POST 請求
.loginProcessingUrl("/login/form")
// 設置登錄表單中的用戶名參數,默認為 username
.usernameParameter("name")
// 設置登錄表單中的密碼參數,默認為 password
.passwordParameter("pwd")
// 認證成功處理,如果存在原始訪問路徑,則重定向到該路徑;如果沒有,則重定向 /index
//.defaultSuccessUrl("/index")
// 認證失敗處理,重定向到指定地址,默認為 loginPage() + ?error;該路徑不設限訪問
//.failureUrl("/login/page?error");
// 不再使用 defaultSuccessUrl() 和 failureUrl() 方法進行認證成功和失敗處理,
// 使用自定義的認證成功和失敗處理器
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
// 開啟基於 HTTP 請求訪問控制
http.authorizeRequests()
// 以下訪問不需要任何權限,任何人都可以訪問
.antMatchers("/login/page", "/code/image").permitAll()
// 以下訪問需要 ROLE_ADMIN 權限
.antMatchers("/admin/**").hasRole("ADMIN")
// 以下訪問需要 ROLE_USER 權限
.antMatchers("/user/**").hasAuthority("ROLE_USER")
// 其它任何請求訪問都需要先通過認證
.anyRequest().authenticated();
// 關閉 csrf 防護
http.csrf().disable();
// 將自定義過濾器(圖形驗證碼校驗)添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 定制一些全局性的安全配置,例如:不攔截靜態資源的訪問
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 靜態資源的訪問不需要攔截,直接放行
web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
⭐ 測試
訪問localhost:8080/login/page
,等待 60 秒后,輸入正確的用戶名、密碼和驗證碼:

驗證碼過期,重定向到localhost:8080/login/page?error
,顯示錯誤信息:

手機短信驗證碼
一般登錄除了用戶名、密碼登錄,還可以使用手機短信驗證碼登錄,Spring Security
默認沒有實現手機短信驗證碼的功能,所以需要我們自己實現。
實現流程分析

手機短信驗證碼登錄和前面的帶有圖形驗證碼的用戶名、密碼登錄流程類似,紅色標記的部分是需要我們自定義實現的類。
我們首先分析下帶有圖形驗證碼的用戶名、密碼登錄流程:
-
在 ImageCodeValidateFilter 過濾器中校驗用戶輸入的圖形驗證碼是否正確。
-
在 UsernamePasswordAuthenticationFilter 過濾器中將 username 和 password 生成一個用於認證的 Token(UsernamePasswordAuthenticationToken),並將其傳遞給 ProviderManager 接口的實現類 AuthenticationManager。
-
AuthenticationManager 管理器尋找到一個合適的處理器 DaoAuthenticationProvider 來處理 UsernamePasswordAuthenticationToken。
-
DaoAuthenticationProvider 通過 UserDetailsService 接口的實現類 CustomUserDetailsService 從數據庫中獲取指定 username 的相關信息,並校驗用戶輸入的 password。如果校驗成功,那就認證通過,用戶信息類對象 Authentication 標記為已認證。
-
認證通過后,將已認證的用戶信息對象 Authentication 存儲到 SecurityContextHolder 中,最終存儲到 Session 中。
仿照上述流程,我們分析手機短信驗證碼登錄流程:
- 仿照 ImageCodeValidateFilter 過濾器設計 MobileVablidateFilter 過濾器,該過濾器用來校驗用戶輸入手機短信驗證碼。
- 仿照 UsernamePasswordAuthenticationFilter 過濾器設計 MobileAuthenticationFilter 過濾器,該過濾器將用戶輸入的手機號生成一個 Token(MobileAuthenticationToken),並將其傳遞給 ProviderManager 接口的實現類 AuthenticationManager。
- AuthenticationManager 管理器尋找到一個合適的處理器 MobileAuthenticationProvider 來處理 MobileAuthenticationToken,該處理器是仿照 DaoAuthenticationProvider 進行設計的。
- MobileAuthenticationProvider 通過 UserDetailsService 接口的實現類 MobileUserDetailsService 從數據庫中獲取指定手機號對應的用戶信息,此處不需要進行任何校驗,直接將用戶信息類對象 Authentication 標記為已認證。
- 認證通過后,將已認證的用戶信息對象 Authentication 存儲到 SecurityContextHolder 中,最終存儲到 Session 中,此處的操作不需要我們編寫。
最后通過自定義配置類 MobileAuthenticationConfig 組合上述組件,並添加到安全配置類 SpringSecurityConfig 中。
模擬發送短信驗證碼
✏️ 在 UserMapper 接口中添加根據 mobile 查詢用戶的方法
public interface UserMapper {
//...
@Select("select * from user where mobile = #{mobile}")
User selectByMobile(String mobile);
}
✏️ 創建 UserService 類,編寫判斷指定 mobile 是否存在的方法
package com.example.service;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* 判斷指定 mobile 是否存在
*/
public boolean isExistByMobile(String mobile) {
return userMapper.selectByMobile(mobile) != null;
}
}
✏️ 創建 MobileCodeSendService 類,模擬手機短信驗證碼發送服務
package com.example.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class MobileCodeSendService {
/**
* 模擬發送手機短信驗證碼
*/
public void send(String mobile, String code) {
String sendContent = String.format("驗證碼為 %s,請勿泄露!", code);
log.info("向手機號 " + mobile + " 發送短信:" + sendContent);
}
}
✏️ 在 LoginController 中添加手機短信驗證碼相關的 Controller 方法
public class Constants {
//...
// Session 中存儲手機短信驗證碼的屬性名
public static final String MOBILE_SESSION_KEY = "MOBILE_SESSION_KEY";
}
@Controller
public class LoginController {
//...
@Autowired
private MobileCodeSendService mobileCodeSendService; // 模擬手機短信驗證碼發送服務
@Autowired
private UserService userService;
//...
@GetMapping("/mobile/page")
public String mobileLoginPage() { // 跳轉到手機短信驗證碼登錄頁面
return "login-mobile";
}
@GetMapping("/code/mobile")
@ResponseBody
public Object sendMoblieCode(String mobile, HttpServletRequest request) {
// 隨機生成一個 4 位的驗證碼
String code = RandomStringUtils.randomNumeric(4);
// 將手機驗證碼文本存儲在 Session 中,設置過期時間為 10 * 60s
CheckCode mobileCode = new CheckCode(code, 10 * 60);
request.getSession().setAttribute(Constants.MOBILE_SESSION_KEY, mobileCode);
// 判斷該手機號是否注冊
if(!userService.isExistByMobile(mobile)) {
return new ResultData<>(1, "該手機號不存在!");
}
// 模擬發送手機短信驗證碼到指定用戶手機
mobileCodeSendService.send(mobile, code);
return new ResultData<>(0, "發送成功!");
}
}
✏️ 編寫手機短信驗證碼登錄頁面 login-mobile.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
<script src="https://s3.pstatp.com/cdn/expire-1-M/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<form method="post" th:action="@{/mobile/form}">
<input id="mobile" name="mobile" type="text" placeholder="手機號碼"><br>
<div>
<input name="mobileCode" type="text" placeholder="驗證碼">
<button type="button" id="sendCode">獲取驗證碼</button>
</div>
<div th:if="${param.error}">
<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION.message}" style="color:red">用戶名或密碼錯誤</span>
</div>
<button type="submit">登錄</button>
</form>
<script>
// 獲取手機短信驗證碼
$("#sendCode").click(function () {
var mobile = $('#mobile').val().trim();
if(mobile == '') {
alert("手機號不能為空");
return;
}
// /code/mobile?mobile=123123123
var url = "/code/mobile?mobile=" + mobile;
$.get(url, function(data){
alert(data.msg);
});
});
</script>
</body>
</html>
✏️ 更改安全配置類 SpringSecurityConfig,設置訪問/mobile/page
和/code/mobile
不需要任何權限
@EnableWebSecurity // 開啟 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
/**
* 定制基於 HTTP 請求的用戶訪問控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 開啟基於 HTTP 請求訪問控制
http.authorizeRequests()
// 以下訪問不需要任何權限,任何人都可以訪問
.antMatchers("/login/page", "/code/image","/mobile/page", "/code/mobile").permitAll()
// 以下訪問需要 ROLE_ADMIN 權限
.antMatchers("/admin/**").hasRole("ADMIN")
// 以下訪問需要 ROLE_USER 權限
.antMatchers("/user/**").hasAuthority("ROLE_USER")
// 其它任何請求訪問都需要先通過認證
.anyRequest().authenticated();
//...
}
//...
}
✏️ 測試
訪問localhost:8080/mobile/page
,頁面顯示:

手機號碼輸入11111111111
,控制台輸出:
向手機號 11111111111 發送短信:驗證碼為 2561,請勿泄露!
瀏覽器彈出窗口顯示”發送成功“。
自定義認證流程配置
📚 自定義短信驗證碼校驗過濾器 MobileValidateFilter
package com.example.config.security.mobile;
import com.example.config.security.CustomAuthenticationFailureHandler;
import com.example.constans.Constants;
import com.example.entity.CheckCode;
import com.example.exception.ValidateCodeException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
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 javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* 手機短信驗證碼校驗
*/
@Component
public class MobileCodeValidateFilter extends OncePerRequestFilter {
private String codeParamter = "mobileCode"; // 前端輸入的手機短信驗證碼參數名
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定義認證失敗處理器
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 非 POST 方式的手機短信驗證碼提交請求不進行校驗
if("/mobile/form".equals(request.getRequestURI()) && "POST".equals(request.getMethod())) {
try {
// 檢驗手機驗證碼的合法性
validate(request);
} catch (ValidateCodeException e) {
// 將異常交給自定義失敗處理器進行處理
authenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 放行,進入下一個過濾器
filterChain.doFilter(request, response);
}
/**
* 檢驗用戶輸入的手機驗證碼的合法性
*/
private void validate(HttpServletRequest request) {
// 獲取用戶傳入的手機驗證碼值
String requestCode = request.getParameter(this.codeParamter);
if(requestCode == null) {
requestCode = "";
}
requestCode = requestCode.trim();
// 獲取 Session
HttpSession session = request.getSession();
// 獲取 Session 中存儲的手機短信驗證碼
CheckCode savedCode = (CheckCode) session.getAttribute(Constants.MOBILE_SESSION_KEY);
if (savedCode != null) {
// 隨手清除驗證碼,無論是失敗,還是成功。客戶端應在登錄失敗時刷新驗證碼
session.removeAttribute(Constants.MOBILE_SESSION_KEY);
}
// 校驗出錯,拋出異常
if (StringUtils.isBlank(requestCode)) {
throw new ValidateCodeException("驗證碼的值不能為空");
}
if (savedCode == null) {
throw new ValidateCodeException("驗證碼不存在");
}
if (savedCode.isExpried()) {
throw new ValidateCodeException("驗證碼過期");
}
if (!requestCode.equalsIgnoreCase(savedCode.getCode())) {
throw new ValidateCodeException("驗證碼輸入錯誤");
}
}
}
📚 更改自定義失敗處理器 CustomAuthenticationFailureHandler,原先的處理器在認證失敗時,會直接重定向到/login/page?error
顯示認證異常信息。現在我們有兩種登錄方式,應該進行以下處理:
- 帶圖形驗證碼的用戶名、密碼方式登錄方式出現認證異常,重定向到
/login/page?error
。 - 手機短信驗證碼方式登錄出現認證異常,重定向到
/mobile/page?error
。
package com.example.config.security;
import com.example.entity.ResultData;
import com.fasterxml.jackson.databind.ObjectMapper;
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.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 繼承 SimpleUrlAuthenticationFailureHandler 處理器,該類是 failureUrl() 方法使用的認證失敗處理器
*/
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
String xRequestedWith = request.getHeader("x-requested-with");
// 判斷前端的請求是否為 ajax 請求
if ("XMLHttpRequest".equals(xRequestedWith)) {
// 認證成功,響應 JSON 數據
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(objectMapper.writeValueAsString(new ResultData<>(1, "認證失敗!")));
}else {
// 用戶名、密碼方式登錄出現認證異常,需要重定向到 /login/page?error
// 手機短信驗證碼方式登錄出現認證異常,需要重定向到 /mobile/page?error
// 使用 Referer 獲取當前登錄表單提交請求是從哪個登錄頁面(/login/page 或 /mobile/page)鏈接過來的
String refer = request.getHeader("Referer");
String lastUrl = StringUtils.substringBefore(refer, "?");
// 設置默認的重定向路徑
super.setDefaultFailureUrl(lastUrl + "?error");
// 調用父類的 onAuthenticationFailure() 方法
super.onAuthenticationFailure(request, response, e);
}
}
}
📚 自定義短信驗證碼認證過濾器 MobileAuthenticationFilter,仿照 UsernamePasswordAuthenticationFilter 過濾器進行編寫
package com.example.config.security.mobile;
import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 手機短信驗證碼認證過濾器,仿照 UsernamePasswordAuthenticationFilter 過濾器編寫
*/
public class MobileAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private String mobileParamter = "mobile"; // 默認手機號參數名為 mobile
private boolean postOnly = true; // 默認請求方式只能為 POST
protected MobileAuthenticationFilter() {
// 默認登錄表單提交路徑為 /mobile/form,POST 方式請求
super(new AntPathRequestMatcher("/mobile/form", "POST"));
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
//(1) 默認情況下,如果請求方式不是 POST,會拋出異常
if(postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}else {
//(2) 獲取請求攜帶的 mobile
String mobile = request.getParameter(mobileParamter);
if(mobile == null) {
mobile = "";
}
mobile = mobile.trim();
//(3) 使用前端傳入的 mobile 構造 Authentication 對象,標記該對象未認證
// MobileAuthenticationToken 是我們自定義的 Authentication 類,后續介紹
MobileAuthenticationToken authRequest = new MobileAuthenticationToken(mobile);
//(4) 將請求中的一些屬性信息設置到 Authentication 對象中,如:remoteAddress,sessionId
this.setDetails(request, authRequest);
//(5) 調用 ProviderManager 類的 authenticate() 方法進行身份認證
return this.getAuthenticationManager().authenticate(authRequest);
}
}
@Nullable
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParamter);
}
protected void setDetails(HttpServletRequest request, MobileAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
public void setMobileParameter(String mobileParamter) {
Assert.hasText(mobileParamter, "Mobile par ameter must not be empty or null");
this.mobileParamter = mobileParamter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public String getMobileParameter() {
return mobileParamter;
}
}
📚 自定義用戶信息封裝類 MobileAuthenticationToken,仿照 UsernamePasswordAuthenticationToken 類進行編寫
package com.example.config.security.mobile;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class MobileAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 520L;
private final Object principal;
/**
* 認證前,使用該構造器進行封裝信息
*/
public MobileAuthenticationToken(Object principal) {
super((Collection) null); // 用戶權限為 null
this.principal = principal; // 前端傳入的手機號
this.setAuthenticated(false); // 標記為未認證
}
/**
* 認證成功后,使用該構造器封裝用戶信息
*/
public MobileAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities); // 用戶權限集合
this.principal = principal; // 封裝認證用戶信息的 UserDetails 對象,不再是手機號
super.setAuthenticated(true); // 標記認證成功
}
@Override
public Object getCredentials() {
// 由於使用手機短信驗證碼登錄不需要密碼,所以直接返回 null
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException("Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
} else {
super.setAuthenticated(false);
}
}
@Override
public void eraseCredentials() {
// 手機短信驗證碼認證方式不必去除額外的敏感信息,所以直接調用父類方法
super.eraseCredentials();
}
}
📚 自定義短信驗證碼認證的處理器 MobileAuthenticationProvider,仿照 DaoAuthenticationProvider 處理器進行編寫
package com.example.config.security.mobile;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;
public class MobileAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserDetailsChecker authenticationChecks = new MobileAuthenticationProvider.DefaultAuthenticationChecks();
/**
* 處理認證
*/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//(1) 如果入參的 Authentication 類型不是 MobileAuthenticationToken,拋出異常
Assert.isInstanceOf(MobileAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("MobileAuthenticationProvider.onlySupports", "Only MobileAuthenticationToken is supported");
});
// 獲取手機號
String mobile = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
//(2) 根據手機號從數據庫中查詢用戶信息
UserDetails user = this.userDetailsService.loadUserByUsername(mobile);
if (user == null) {
//(3) 未查詢到用戶信息,拋出異常
throw new AuthenticationServiceException("該手機號未注冊");
}
//(4) 檢查賬號是否鎖定、賬號是否可用、賬號是否過期、密碼是否過期
this.authenticationChecks.check(user);
//(5) 查詢到了用戶信息,則認證通過,構建標記認證成功用戶信息類對象 AuthenticationToken
MobileAuthenticationToken result = new MobileAuthenticationToken(user, user.getAuthorities());
// 需要把認證前 Authentication 對象中的 details 信息加入認證后的 Authentication
result.setDetails(authentication.getDetails());
return result;
}
/**
* ProviderManager 管理器通過此方法來判斷是否采用此 AuthenticationProvider 類
* 來處理由 AuthenticationFilter 過濾器傳入的 Authentication 對象
*/
@Override
public boolean supports(Class<?> authentication) {
// isAssignableFrom 返回 true 當且僅當調用者為父類.class,參數為本身或者其子類.class
// ProviderManager 會獲取 MobileAuthenticationFilter 過濾器傳入的 Authentication 類型
// 所以當且僅當 authentication 的類型為 MobileAuthenticationToken 才返回 true
return MobileAuthenticationToken.class.isAssignableFrom(authentication);
}
/**
* 此處傳入自定義的 MobileUserDetailsSevice 對象
*/
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public UserDetailsService getUserDetailsService() {
return userDetailsService;
}
/**
* 檢查賬號是否鎖定、賬號是否可用、賬號是否過期、密碼是否過期
*/
private class DefaultAuthenticationChecks implements UserDetailsChecker {
private DefaultAuthenticationChecks() {
}
@Override
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
throw new LockedException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} else if (!user.isEnabled()) {
throw new DisabledException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
throw new AccountExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
} else if (!user.isCredentialsNonExpired()) {
throw new CredentialsExpiredException(MobileAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
}
}
}
📚 自定義 MobileUserDetailsService 類,MobileAuthenticationProvider 處理器傳入的 UserDetailsService 對象的類型需要我們自定義
package com.example.service;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class MobileUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
//(1) 從數據庫嘗試讀取該用戶
User user = userMapper.selectByMobile(mobile);
// 用戶不存在,拋出異常
if (user == null) {
throw new UsernameNotFoundException("用戶不存在");
}
//(2) 將數據庫形式的 roles 解析為 UserDetails 的權限集合
// AuthorityUtils.commaSeparatedStringToAuthorityList() 是 Spring Security 提供的方法,用於將逗號隔開的權限集字符串切割為可用權限對象列表
user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
//(3) 返回 UserDetails 對象
return user;
}
}
📚 自定義短信驗證碼認證方式配置類 MobileAuthenticationConfig,將上述組件進行管理
package com.example.config.security.mobile;
import com.example.config.security.CustomAuthenticationFailureHandler;
import com.example.config.security.CustomAuthenticationSuccessHandler;
import com.example.service.MobileUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.stereotype.Component;
@Component
public class MobileAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler; // 自定義認證成功處理器
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; // 自定義認證失敗處理器
@Autowired
private MobileCodeValidateFilter mobileCodeValidaterFilter; // 手機短信驗證碼校驗過濾器
@Autowired
private MobileUserDetailsService userDetailsService; // 手機短信驗證方式的 UserDetail
@Override
public void configure(HttpSecurity http) throws Exception {
//(1) 將短信驗證碼認證的自定義過濾器綁定到 HttpSecurity 中
//(1.1) 創建手機短信驗證碼認證過濾器的實例 filer
MobileAuthenticationFilter filter = new MobileAuthenticationFilter();
//(1.2) 設置 filter 使用 AuthenticationManager(ProviderManager 接口實現類) 認證管理器
// 多種登錄方式應該使用同一個認證管理器實例,所以獲取 Spring 容器中已經存在的 AuthenticationManager 實例
AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
filter.setAuthenticationManager(authenticationManager);
//(1.3) 設置 filter 使用自定義成功和失敗處理器
filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
//(1.4) 設置 filter 使用 SessionAuthenticationStrategy 會話管理器
// 多種登錄方式應該使用同一個會話管理器實例,獲取 Spring 容器已經存在的 SessionAuthenticationStrategy 實例
SessionAuthenticationStrategy sessionAuthenticationStrategy = http.getSharedObject(SessionAuthenticationStrategy.class);
filter.setSessionAuthenticationStrategy(sessionAuthenticationStrategy);
//(1.5) 在 UsernamePasswordAuthenticationFilter 過濾器之前添加 MobileCodeValidateFilter 過濾器
// 在 UsernamePasswordAuthenticationFilter 過濾器之后添加 MobileAuthenticationFilter 過濾器
http.addFilterBefore(mobileCodeValidaterFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class);
//(2) 將自定義的 MobileAuthenticationProvider 處理器綁定到 HttpSecurity 中
//(2.1) 創建手機短信驗證碼認證過濾器的 AuthenticationProvider 實例,並指定所使用的 UserDetailsService
MobileAuthenticationProvider provider = new MobileAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
//(2.2) 將該 AuthenticationProvider 實例綁定到 HttpSecurity 中
http.authenticationProvider(provider);
}
}
📚 將上述自定義配置類 MobileAuthenticationConfig 綁定到最終的安全配置類 SpringSecurityConfig 中
@EnableWebSecurity // 開啟 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Autowired
private MobileAuthenticationConfig mobileAuthenticationConfig; // 手機短信驗證碼認證方式的配置類
//...
/**
* 定制基於 HTTP 請求的用戶訪問控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
// 將手機短信驗證碼認證的配置與當前的配置綁定
http.apply(mobileAuthenticationConfig);
}
//...
}
完整的安全配置類如下:
package com.example.config;
import com.example.config.security.CustomAuthenticationFailureHandler;
import com.example.config.security.CustomAuthenticationSuccessHandler;
import com.example.config.security.ImageCodeValidateFilter;
import com.example.config.security.mobile.MobileAuthenticationConfig;
import com.example.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@EnableWebSecurity // 開啟 MVC Security 安全配置
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private CustomAuthenticationSuccessHandler authenticationSuccessHandler; // 自定義認證成功處理器
@Autowired
private CustomAuthenticationFailureHandler authenticationFailureHandler; // 自定義認證失敗處理器
@Autowired
private ImageCodeValidateFilter imageCodeValidateFilter; // 自定義過濾器(圖形驗證碼校驗)
@Autowired
private MobileAuthenticationConfig mobileAuthenticationConfig; // 手機短信驗證碼認證方式的配置類
/**
* 密碼編碼器,密碼不能明文存儲
*/
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 使用 BCryptPasswordEncoder 密碼編碼器,該編碼器會將隨機產生的 salt 混入最終生成的密文中
return new BCryptPasswordEncoder();
}
/**
* 定制用戶認證管理器來實現用戶認證
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 采用內存存儲方式,用戶認證信息存儲在內存中
// auth.inMemoryAuthentication()
// .withUser("admin").password(passwordEncoder()
// .encode("123456")).roles("ROLE_ADMIN");
// 不再使用內存方式存儲用戶認證信息,而是動態從數據庫中獲取
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
/**
* 定制基於 HTTP 請求的用戶訪問控制
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 啟動 form 表單登錄
http.formLogin()
// 設置登錄頁面的訪問路徑,默認為 /login,GET 請求;該路徑不設限訪問
.loginPage("/login/page")
// 設置登錄表單提交路徑,默認為 loginPage() 設置的路徑,POST 請求
.loginProcessingUrl("/login/form")
// 設置登錄表單中的用戶名參數,默認為 username
.usernameParameter("name")
// 設置登錄表單中的密碼參數,默認為 password
.passwordParameter("pwd")
// 認證成功處理,如果存在原始訪問路徑,則重定向到該路徑;如果沒有,則重定向 /index
//.defaultSuccessUrl("/index")
// 認證失敗處理,重定向到指定地址,默認為 loginPage() + ?error;該路徑不設限訪問
//.failureUrl("/login/page?error");
// 不再使用 defaultSuccessUrl() 和 failureUrl() 方法進行認證成功和失敗處理,
// 使用自定義的認證成功和失敗處理器
.successHandler(authenticationSuccessHandler)
.failureHandler(authenticationFailureHandler);
// 開啟基於 HTTP 請求訪問控制
http.authorizeRequests()
// 以下訪問不需要任何權限,任何人都可以訪問
.antMatchers("/login/page", "/code/image","/mobile/page", "/code/mobile").permitAll()
// 以下訪問需要 ROLE_ADMIN 權限
.antMatchers("/admin/**").hasRole("ADMIN")
// 以下訪問需要 ROLE_USER 權限
.antMatchers("/user/**").hasAuthority("ROLE_USER")
// 其它任何請求訪問都需要先通過認證
.anyRequest().authenticated();
// 關閉 csrf 防護
http.csrf().disable();
// 將自定義過濾器(圖形驗證碼校驗)添加到 UsernamePasswordAuthenticationFilter 之前
http.addFilterBefore(imageCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
// 將手機短信驗證碼認證的配置與當前的配置綁定
http.apply(mobileAuthenticationConfig);
}
/**
* 定制一些全局性的安全配置,例如:不攔截靜態資源的訪問
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 靜態資源的訪問不需要攔截,直接放行
web.ignoring().antMatchers("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg");
}
}
📚 測試
訪問localhost:8080/mobile/page
:

手機號碼輸入11111111111
,控制台輸出:
向手機號 11111111111 發送短信:驗證碼為 6979,請勿泄露!
瀏覽器彈出窗口顯示”發送成功“,輸入正確短信驗證碼進行認證之后,瀏覽器重定向到/index
:
