Spring Security 雙因素認證

Spring Security
Remote
0
09:41 PM · Nov 29 ,2025

1. 概述

在本教程中,我們將使用 Soft Token 和 Spring Security 實現 雙因素身份驗證 功能。

我們將把這項新功能集成到現有的簡單登錄流程中,並使用 Google Authenticator 應用 生成令牌。

簡單來説,雙因素身份驗證是一種遵循“用户知道的東西和用户擁有的東西”這一著名原則的驗證過程。

因此,用户在身份驗證過程中提供額外的“驗證令牌”——基於時間敏感一次性密碼 TOTP 算法生成的單次密碼。

2. Maven 配置

首先,為了在我們的應用中使用 Google Authenticator,我們需要:

  • 生成密鑰
  • 通過 QR 碼向用户提供密鑰
  • 使用該密鑰驗證用户輸入的令牌。

我們將使用一個簡單的服務器端 來生成/驗證一次性密碼,通過在我們的 pom.xml 中添加以下依賴項:

<dependency>
    <groupId>org.jboss.aerogear</groupId>
    <artifactId>aerogear-otp-java</artifactId>
    <version>1.0.0</version>
</dependency>

3. 用户實體接下來,我們將修改我們的用户實體以包含額外信息,如下所示:

@Entity
public class User {
    ...
    private boolean isUsing2FA;
    private String secret;

    public User() {
        super();
        this.secret = Base32.random();
        ...
    }
}

請注意:

我們為每個用户保存一個隨機秘密代碼,稍後用於生成驗證碼 我們的兩步驗證是可選的

4. 額外登錄參數

首先,我們需要調整安全配置以接受額外的參數——驗證令牌。我們可以通過使用自定義 AuthenticationDetailsSource 來實現這一點。

以下是我們的 CustomWebAuthenticationDetailsSource

@Component
public class CustomWebAuthenticationDetailsSource implements
  AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new CustomWebAuthenticationDetails(context);
    }
}

以及 CustomWebAuthenticationDetails

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

    private String verificationCode;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        verificationCode = request.getParameter("code");
    }

    public String getVerificationCode() {
        return verificationCode;
    }
}

以及我們的安全配置:

@Configuration
@EnableWebSecurity
public class LssSecurityConfig {

    @Autowired
    private CustomWebAuthenticationDetailsSource authenticationDetailsSource;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.formLogin()
            .authenticationDetailsSource(authenticationDetailsSource)
            ...
    }
}

最後,我們在登錄表單中添加了額外的參數:

<labelth:text="#{label.form.login2fa}">
    Google Authenticator Verification Code
</label>
<input type='text' name='code'/>

注意:我們需要在安全配置中設置我們的自定義 AuthenticationDetailsSource

5. 自定義身份驗證提供者接下來,我們需要一個自定義的AuthenticationProvider來處理額外的參數驗證:

public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    private UserRepository userRepository;

    @Override
    public Authentication authenticate(Authentication auth)
      throws AuthenticationException {
        String verificationCode 
          = ((CustomWebAuthenticationDetails) auth.getDetails())
            .getVerificationCode();
        User user = userRepository.findByEmail(auth.getName());
        if ((user == null)) {
            throw new BadCredentialsException("Invalid username or password");
        }
        if (user.isUsing2FA()) {
            Totp totp = new Totp(user.getSecret());
            if (!isValidLong(verificationCode) || !totp.verify(verificationCode)) {
                throw new BadCredentialsException("Invalid verfication code");
            }
        }
        
        Authentication result = super.authenticate(auth);
        return new UsernamePasswordAuthenticationToken(
          user, result.getCredentials(), result.getAuthorities());
    }

    private boolean isValidLong(String code) {
        try {
            Long.parseLong(code);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

注意——在驗證一次性密碼驗證碼後,我們只是將身份驗證委託給下游。

這是我們的身份驗證提供者 Bean

@Bean
public DaoAuthenticationProvider authProvider() {
    CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(encoder());
    return authProvider;
}

6. 註冊流程

現在,為了讓用户能夠使用該應用程序生成令牌,他們需要在註冊時正確設置各項內容。

因此,我們需要對註冊流程進行一些簡單的修改——允許選擇使用雙重驗證的用户掃描所需的QR-code

首先,我們在註冊表單中添加以下輸入框:

使用雙重驗證 <input type="checkbox" name="using2FA" value="true"/>

然後,在我們的RegistrationController中,我們根據用户的選擇進行重定向,確認註冊後:

@GetMapping("/registrationConfirm")
public String confirmRegistration(@RequestParam("token") String token, ...) {
    String result = userService.validateVerificationToken(token);
    if(result.equals("valid")) {
        User user = userService.getUser(token);
        if (user.isUsing2FA()) {
            model.addAttribute("qr", userService.generateQRUrl(user));
            return "redirect:/qrcode.html?lang=" + locale.getLanguage();
        }
        
        model.addAttribute(
          "message", messages.getMessage("message.accountVerified", null, locale));
        return "redirect:/login?lang=" + locale.getLanguage();
    }
    ...
}

下面是我們的generateQRUrl()方法:

public static String QR_PREFIX = 
  "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=";

@Override
public String generateQRUrl(User user) {
    return QR_PREFIX + URLEncoder.encode(String.format(
      "otpauth://totp/%s:%s?secret=%s&issuer=%s", 
      APP_NAME, user.getEmail(), user.getSecret(), APP_NAME),
      "UTF-8");
}

下面是我們的qrcode.html頁面:

<html>
<body>
<div id="qr">
    <p>
        使用 Google Authenticator 應用程序在您的手機上掃描此條形碼,以便稍後使用
        登錄
    </p>
    <img th:src="${param.qr[0]}"/>
</div>
<a href="/login" class="btn btn-primary">前往登錄頁面</a>
</body>
</html>

請注意:

  • generateQRUrl()方法用於生成 QR-code URL
  • 此 QR-code 將由用户的手機通過 Google Authenticator 應用程序掃描
  • 該應用程序將生成一個有效的 30 秒的 6 位驗證碼
  • 此驗證碼將在我們自定義的 AuthenticationProvider 中進行驗證

7. 啓用兩步驗證

接下來,我們將確保用户可以隨時更改登錄偏好,如下所示:

@PostMapping("/user/update/2fa")
public GenericResponse modifyUser2FA(@RequestParam("use2FA") boolean use2FA)
  throws UnsupportedEncodingException {
    User user = userService.updateUser2FA(use2FA);
    if (use2FA) {
        return new GenericResponse(userService.generateQRUrl(user));
    }
    return null;
}

以下是 updateUser2FA() 的代碼:

@Override
public User updateUser2FA(boolean use2FA) {
    Authentication curAuth = SecurityContextHolder.getContext().getAuthentication();
    User currentUser = (User) curAuth.getPrincipal();
    currentUser.setUsing2FA(use2FA);
    currentUser = repository.save(currentUser);
    
    Authentication auth = new UsernamePasswordAuthenticationToken(
      currentUser, currentUser.getPassword(), curAuth.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(auth);
    return currentUser;
}

以下是前端代碼:

<div th:if="${#authentication.principal.using2FA}">
    您正在使用兩步驗證
    <a href="#" onclick="disable2FA()">禁用 2FA</a> 
</div>
<div th:if="${! #authentication.principal.using2FA}">
    您沒有使用兩步驗證
    <a href="#" onclick="enable2FA()">啓用 2FA</a> 
</div>
<br/>
<div id="qr" style="display:none;">
    <p>使用您手機上的 Google Authenticator 應用程序掃描此條形碼</p>
</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript">
function enable2FA(){
    set2FA(true);
}
function disable2FA(){
    set2FA(false);
}
function set2FA(use2FA){
    $.post( "/user/update/2fa", { use2FA: use2FA } , function( data ) {
        if(use2FA){
        	$("#qr").append('<img src="'+data.message+'" />').show();
        }else{
            window.location.reload();
        }
    });
}
</script>

8. 結論

在本快速教程中,我們演示瞭如何使用 Soft Token 與 Spring Security 實現雙因素身份驗證。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.