SpringSecurity6.4中一次性令牌登錄(One-Time Token Login)實(shí)現(xiàn)
本系列Spring Boot 版本 3.4.0
本系列Spring Security 版本 6.4.2
Spring Security為一次性令牌(One-Time Token,OTT)認(rèn)證提供了支持,通過oneTimeTokenLogin()DSL(Domain Specific Language,領(lǐng)域特定語言,它使用了一種特定的語法和約定來簡(jiǎn)化配置和管理Spring應(yīng)用程序的過程。)即可使用該功能。在進(jìn)行實(shí)現(xiàn)之前,需要明確OTT功能在框架中的范圍。
一、理解一次性令牌與一次性密碼的區(qū)別
人們常常會(huì)混淆一次性令牌(OTT)和一次性密碼(OTP),但在 Spring Security 中,這兩個(gè)概念在幾個(gè)關(guān)鍵方面有所不同。為了清晰起見,我們將假設(shè) OTP 指的是基于時(shí)間的一次性密碼(TOTP)或基于 HMAC 的一次性密碼(HOTP)。
1.區(qū)別一:設(shè)置要求
- OTT:無需初始設(shè)置。用戶無需提前進(jìn)行任何配置。
- OTP:通常需要設(shè)置,例如使用外部工具生成和共享一個(gè)密鑰來生成一次性密碼。
2.區(qū)別二:令牌交付/發(fā)送
- OTT:通常需要實(shí)現(xiàn)一個(gè)自定義的
OneTimeTokenGenerationSuccessHandler,負(fù)責(zé)將令牌交付給最終用戶。 - OTP:令牌通常由外部工具生成,因此無需通過應(yīng)用程序發(fā)送給用戶。
3.區(qū)別三:令牌生成
- OTT:通過
OneTimeTokenService.generate(GenerateOneTimeTokenRequest)方法在服務(wù)器端生成令牌。 - OTP:令牌不一定是在服務(wù)器端生成的,通常是由客戶端使用共享密鑰創(chuàng)建的。
總結(jié)來說,一次性令牌(OTT)提供了一種無需額外賬戶設(shè)置即可驗(yàn)證用戶的方法,與通常涉及更復(fù)雜設(shè)置過程并依賴外部工具生成令牌的一次性密碼(OTP)不同。
二、一次性令牌登錄的流程
一次性令牌登錄主要分為兩個(gè)步驟:
- 用戶提交用戶標(biāo)識(shí)符(通常為用戶名)請(qǐng)求令牌,令牌會(huì)以魔法鏈接(
Magic Link)的形式,通過電子郵件、短信等方式發(fā)送給用戶。 - 用戶將令牌提交到一次性令牌登錄端點(diǎn),也就是點(diǎn)擊魔法鏈接。若令牌有效,則用戶成功登錄。
- 整體流程
開始→用戶訪問一次性令牌登錄頁面
→點(diǎn)擊發(fā)送令牌按鈕
→發(fā)送POST /ott/generate 請(qǐng)求
→GenerateOneTimeTokenFilter 監(jiān)聽POST /ott/generate 請(qǐng)求,并通過OneTimeTokenService.generate()方法在服務(wù)器端生成令牌
→令牌生成成功后,調(diào)用 自定義的OneTimeTokenGenerationSuccessHandler接口實(shí)現(xiàn)類的handle()方法
→handle()方法負(fù)責(zé)通過Email或短信將包含令牌的魔法鏈接(Magic Link)發(fā)送給用戶
→用戶接收到郵件/短信后,點(diǎn)擊魔法鏈接
→服務(wù)器端接收GET /login/ott請(qǐng)求
→由 DefaultOneTimeTokenSubmitPageGeneratingFilter 生成一次性令牌提交頁面,并返回給用戶
→用戶點(diǎn)擊提交頁面中的登錄按鈕
→發(fā)送POST /login/ott 請(qǐng)求,服務(wù)端需要自定義一個(gè)HTTP接口,請(qǐng)求路徑為POST /login/ott→服務(wù)器端進(jìn)入身份認(rèn)證流程(FilterChainProxy.doFilter→AuthenticationFilter.doFilterInternal)
→AuthenticationFilter.attemptAuthentication方法中通過調(diào)用this.authenticationConverter.convert(request)構(gòu)造OneTimeTokenAuthenticationToken實(shí)例,其中this.authenticationConverter是OneTimeTokenAuthenticationConverter的實(shí)例
→AuthenticationFilter.doFilterInternal方法中通過調(diào)用AuthenticationManager.authenticate(OneTimeTokenAuthenticationToken)進(jìn)行認(rèn)證
→調(diào)用ProviderManager.authenticate(OneTimeTokenAuthenticationToken)→交給OneTimeTokenAuthenticationProvider認(rèn)證
→OneTimeTokenAuthenticationProvider.authenticate()方法調(diào)用OneTimeTokenService.consume(OneTimeTokenAuthenticationToken)方法消費(fèi)token,對(duì)token進(jìn)行驗(yàn)證
→驗(yàn)證通過
→調(diào)用userDetailsService.loadUserByUsername獲取用戶信息和權(quán)限信息
→認(rèn)證完成
→DispatcherServlet接著處理POST /login/ott 請(qǐng)求
→調(diào)用服務(wù)端自定義的對(duì)應(yīng)Controller方法
→該方法處理登錄成功后的一些邏輯,比如重定向到指定頁面,記錄日志等。
→結(jié)束
三、一次性令牌登錄的配置
1.OTT與默認(rèn)生成的登錄頁面的集成
oneTimeTokenLogin()DSL可與formLogin()結(jié)合使用,這會(huì)在默認(rèn)生成的登錄頁面中添加一個(gè)一次性令牌請(qǐng)求表單,同時(shí)設(shè)置DefaultOneTimeTokenSubmitPageGeneratingFilter來生成默認(rèn)的一次性令牌提交頁面。
2.發(fā)送令牌給用戶
Spring Security無法確定令牌的交付方式,因此需提供自定義的OneTimeTokenGenerationSuccessHandler。
最常用的發(fā)送策略之一是通過電子郵件、短信等發(fā)送一個(gè)魔法鏈接。
在下面的示例中,我們將創(chuàng)建一個(gè)魔法鏈接并將其發(fā)送到用戶的電子郵件。
- 一次性令牌登錄配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
}
- 自定義
OneTimeTokenGenerationSuccessHandler
@Component // ①
public class MagicLinkOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final MailSender mailSender;
private final OneTimeTokenGenerationSuccessHandler redirectHandler = new RedirectOneTimeTokenGenerationSuccessHandler("/ott/sent");
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott")
.queryParam("token", oneTimeToken.getTokenValue()); // ②
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); // ③
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); // ④
this.redirectHandler.handle(request, response, oneTimeToken); // ⑤
}
private String getUserEmail() {
// ...
}
}
- 發(fā)送令牌給用戶成功后要跳轉(zhuǎn)到的頁面
@Controller
public class PageController {
@GetMapping("/ott/sent")
String ottSent() {
return "my-template";
}
}
① 將 MagicLinkOneTimeTokenGenerationSuccessHandler 配置為 Spring Bean
② 創(chuàng)建一個(gè)帶有 token 作為查詢參數(shù)的登錄處理 URL
③ 根據(jù)用戶名檢索用戶電子郵件
④ 使用 JavaMailSender API 向用戶發(fā)送帶有魔法鏈接的電子郵件
⑤ 使用 RedirectOneTimeTokenGenerationSuccessHandler 進(jìn)行重定向到您想要的 URL
郵件內(nèi)容將類似于:
Use the following link to sign in into the application: http://localhost:8080/login/ott?token=a830c444-29d8-4d98-9b46-6aba7b22fe5b
默認(rèn)提交頁面會(huì)檢測(cè)URL中的token查詢參數(shù),并將它的值自動(dòng)填充到表單字段中。
3.配置一次性令牌提交頁面
默認(rèn)的一次性令牌提交頁面由 DefaultOneTimeTokenSubmitPageGeneratingFilter 生成,并監(jiān)聽 GET /login/ott 。URL 也可以這樣更改:
- 配置默認(rèn)提交頁面 URL
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.defaultSubmitPageUrl("/ott/submit")
);
return http.build();
}
}
@Component
public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
}
TIPS:2025.1.14官方文檔示例是使用submitPageUrl(String)DSL 方法進(jìn)行更改。我使用 Spring Security6.4.2 測(cè)試時(shí),只有defaultSubmitPageUrl(String)DSL 方法。因此示例修改為defaultSubmitPageUrl(String)DSL 方法。
4.自定義一次性令牌提交頁面
如果您想使用自己的一次性令牌提交頁面,您可以禁用默認(rèn)頁面,然后提供自己的端點(diǎn)。
- 禁用默認(rèn)提交頁面
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
// 禁用默認(rèn)提交頁面
.showDefaultSubmitPage(false)
// 自定義提交頁面URL
.defaultSubmitPageUrl("/ott/submit")
);
return http.build();
}
}
@Controller
public class MyController {
// 自定義默認(rèn)提交頁面入口
@GetMapping("/ott/submit")
public String ottSubmitPage() {
return "my-ott-submit";
}
}
@Component
public class MagicLinkGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
// ...
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/ott/submit")
.queryParam("token", oneTimeToken.getTokenValue()); // ②
String magicLink = builder.toUriString();
String email = getUserEmail(oneTimeToken.getUsername()); // ③
this.mailSender.send(email, "Your Spring Security One Time Token", "Use the following link to sign in into the application: " + magicLink); // ④
this.redirectHandler.handle(request, response, oneTimeToken); // ⑤
}
// ...
}
5.更改一次性令牌生成 URL
默認(rèn)情況下, GenerateOneTimeTokenFilter 監(jiān)聽 POST /ott/generate 請(qǐng)求。該 URL 可以通過使用 tokenGeneratingUrl(String) DSL 方法進(jìn)行更改:
// Java示例
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http
// ...
.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenGeneratingUrl("/ott/my-generate-url")
);
return http.build();
}
}
TIPS:2025.1.14官方文檔示例是使用generateTokenUrl(String)DSL 方法進(jìn)行更改。我使用 Spring Security6.4.2 測(cè)試時(shí),只有tokenGeneratingUrl(String)DSL 方法。因此示例修改為tokenGeneratingUrl(String)DSL 方法。
6.自定義如何生成和消費(fèi)令牌
定義生成和使用一次性令牌常見操作的接口是OneTimeTokenService。Spring Security默認(rèn)使用InMemoryOneTimeTokenService,生產(chǎn)環(huán)境可考慮使用JdbcOneTimeTokenService。
一些自定義 OneTimeTokenService 最常見的原因包括但不限于:
- 更改一次性令牌過期時(shí)間
- 存儲(chǔ)更多生成令牌請(qǐng)求的信息
- 更改令牌值的創(chuàng)建方式
- 在使用一次性令牌時(shí)進(jìn)行額外驗(yàn)證
有兩種自定義方式:
- 一是將自定義的
OneTimeTokenService作為Bean提供,會(huì)被oneTimeTokenLogin()DSL自動(dòng)識(shí)別:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
return new MyCustomOneTimeTokenService();
}
}
- 二是將
OneTimeTokenService實(shí)例傳遞給DSL,當(dāng)存在多個(gè)SecurityFilterChain且每個(gè)需要不同的OneTimeTokenService時(shí),這種方式很有用:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.formLogin(Customizer.withDefaults())
.oneTimeTokenLogin((ott) -> ott
.tokenService(new MyCustomOneTimeTokenService())
);
return http.build();
}
}
TIPS:2025.1.14官方文檔示例是使用oneTimeTokenService(OneTimeTokenService)DSL 方法進(jìn)行更改。我使用 Spring Security6.4.2 測(cè)試時(shí),只有tokenService(OneTimeTokenService)DSL 方法。因此示例修改為tokenService(OneTimeTokenService)DSL 方法。
7.完整配置示例
@Configuration
@EnableWebSecurity
public class SecurityConfig {
/**
* 配置Security過濾鏈,用于處理一次性令牌登錄
*
* @param http 用于配置Web安全的HttpSecurity對(duì)象
* @param userDetailsService 自定義的用戶信息查詢服務(wù),實(shí)現(xiàn)了UserDetailsService接口
* @param mailSender 郵件服務(wù)
* @return 配置好的SecurityFilterChain對(duì)象
*/
@Bean
@Order(1)
public SecurityFilterChain ottLoginSecurityFilterChain(HttpSecurity http, final OneTimeTokenService oneTimeTokenService, final UserDetailsServiceImpl userDetailsService, final JavaMailSender mailSender) throws Exception {
http.formLogin(Customizer.withDefaults())
// 配置請(qǐng)求授權(quán),所有請(qǐng)求都需要認(rèn)證
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated())
// 禁用CSRF保護(hù),適用于本例的簡(jiǎn)單演示場(chǎng)景,在生產(chǎn)環(huán)境中需謹(jǐn)慎考慮。若是前后端分離情況,需要禁用
.csrf(CsrfConfigurer::disable)
// 配置一次性令牌登錄
.oneTimeTokenLogin(ott->ott
// (可選)設(shè)置一次性令牌生成的URL,默認(rèn)值:/ott/generate
.tokenGeneratingUrl("/ott/generate")
// (可選)設(shè)置一次性令牌生成和驗(yàn)證的服務(wù),實(shí)現(xiàn)OneTimeTokenService接口,默認(rèn)值:InMemoryOneTimeTokenService實(shí)例
.tokenService(oneTimeTokenService)
// (必做)配置一次性令牌生成成功后的處理,默認(rèn)值:RedirectOneTimeTokenGenerationSuccessHandler實(shí)例
.tokenGenerationSuccessHandler(new CustomOneTimeTokenGenerationSuccessHandler(mailSender))
// (可選)設(shè)置是否顯示默認(rèn)的登錄提交頁面,默認(rèn)值:true
.showDefaultSubmitPage(true)
// (可選)設(shè)置默認(rèn)登錄提交頁面的URL,默認(rèn)值:/login/ott
.defaultSubmitPageUrl("/login/ott")
// (可選)配置認(rèn)證轉(zhuǎn)換器,用于將HTTP請(qǐng)求中的token轉(zhuǎn)換為OneTimeTokenAuthenticationToken認(rèn)證對(duì)象,默認(rèn)值:OneTimeTokenAuthenticationConverter實(shí)例
.authenticationConverter(new OneTimeTokenAuthenticationConverter())
// (可選)配置認(rèn)證提供者,用于驗(yàn)證OneTimeTokenAuthenticationToken認(rèn)證對(duì)象,默認(rèn)值:OneTimeTokenAuthenticationProvider實(shí)例
.authenticationProvider(new OneTimeTokenAuthenticationProvider(oneTimeTokenService, userDetailsService))
// (可選)配置認(rèn)證成功后的處理
.authenticationSuccessHandler(((request, response, authentication) -> {
System.out.println("認(rèn)證成功");
}))
// (可選)配置認(rèn)證失敗后的處理
.authenticationFailureHandler((request, response, exception) -> {
System.out.println("認(rèn)證失敗:"+exception.getMessage());
new DefaultRedirectStrategy().sendRedirect(request, response, "/login");
})
// (可選)設(shè)置一次性令牌登錄成功后的URL
// (必做)添加一個(gè) /login/ott 類型的HTTP接口
.loginProcessingUrl("/login/ott")
);
// 構(gòu)建并返回配置好的Security過濾鏈
return http.build();
}
@Bean
public OneTimeTokenService oneTimeTokenService() {
// 創(chuàng)建自定義的OneTimeTokenService實(shí)例
// 為了測(cè)試方便,直接使用內(nèi)置的InMemoryOneTimeTokenService
return new InMemoryOneTimeTokenService();
}
static class CustomOneTimeTokenGenerationSuccessHandler implements OneTimeTokenGenerationSuccessHandler {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
private final JavaMailSender mailSender;
public CustomOneTimeTokenGenerationSuccessHandler(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, OneTimeToken oneTimeToken) throws IOException, ServletException {
// 發(fā)送郵件或短信給用戶
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(request.getContextPath())
.replaceQuery(null)
.fragment(null)
.path("/login/ott") // 設(shè)置默認(rèn)登錄提交頁面的URL
.queryParam("token", oneTimeToken.getTokenValue());
String magicLink = builder.toUriString();
// 發(fā)件人:從數(shù)據(jù)庫(kù)或配置文件中讀取,這里只是測(cè)試,直接寫死
String from = "test@test.com";
String email = getUserEmail(oneTimeToken.getUsername());
SimpleMailMessage msg = new SimpleMailMessage();
msg.setFrom(from);
msg.setTo(email);
msg.setSubject("Your Spring Security One Time Token");
msg.setText("Use the following link to sign in into the application: " + magicLink);
this.mailSender.send(msg);
// 為了測(cè)試方便,重定向到token提交頁
String redirectUrl = "/login/ott?token=" + oneTimeToken.getTokenValue();
this.redirectStrategy.sendRedirect(request, response, redirectUrl);
}
private String getUserEmail(String username) {
// TODO 獲取用戶郵箱地址
return "test@test.com";
}
}
}
@RestController
public class OTTLoginController {
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@PostMapping("/login/ott")
public void login(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("認(rèn)證成功后跳轉(zhuǎn)到主頁");
this.redirectStrategy.sendRedirect(request,response,"/");
}
}
通過以上配置,可以根據(jù)具體需求靈活使用Spring Security的一次性令牌登錄功能。
引用
到此這篇關(guān)于SpringSecurity6.4中一次性令牌登錄(One-Time Token Login)實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)SpringSecurity 一次性令牌登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot查詢PGSQL分表后的數(shù)據(jù)的代碼示例
數(shù)據(jù)庫(kù)用的pgsql,在表數(shù)據(jù)超過100w條的時(shí)候執(zhí)行定時(shí)任務(wù)進(jìn)行了分表,分表后表名命名為原的表名后面拼接時(shí)間,但是我在java業(yè)務(wù)代碼中,我想查詢之前的那條數(shù)據(jù)就查不到了,本文給大家介紹了SpringBoot中如何查詢PGSQL分表后的數(shù)據(jù),需要的朋友可以參考下2024-05-05
springmvc后臺(tái)基于@ModelAttribute獲取表單提交的數(shù)據(jù)
這篇文章主要介紹了springmvc后臺(tái)基于@ModelAttribute獲取表單提交的數(shù)據(jù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-10-10
Java執(zhí)行shell命令的實(shí)現(xiàn)
本文主要介紹了Java執(zhí)行shell命令的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
Java創(chuàng)建多線程的幾種方式實(shí)現(xiàn)
這篇文章主要介紹了Java創(chuàng)建多線程的幾種方式實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10
SpringBoot @ComponentScan掃描的局限性方式
文章總結(jié):SpringBoot的@ComponentScan注解在掃描組件時(shí)存在局限性,只能掃描指定的包及其子包,無法掃描@SpringBootApplication注解自動(dòng)配置的組件,使用@SpringBootApplication注解可以解決這一問題,它集成了@Configuration、@EnableAutoConfiguration2025-01-01
深入理解Java設(shè)計(jì)模式之職責(zé)鏈模式
這篇文章主要介紹了JAVA設(shè)計(jì)模式之職責(zé)鏈模式的的相關(guān)資料,文中示例代碼非常詳細(xì),供大家參考和學(xué)習(xí),感興趣的朋友可以了解2021-11-11
Java使用easyExcel導(dǎo)出excel數(shù)據(jù)案例
這篇文章主要介紹了Java使用easyExcel導(dǎo)出excel數(shù)據(jù)案例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10

