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