iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
佛心分享-SideProject30

吃出一個SideProject!系列 第 13

Day 13:Auth Service-實作登入功能 (1)

  • 分享至 

  • xImage
  •  

昨天我們以循序圖描繪了整個登入流程中各組件溝通的狀況,並簡單介紹了 JWT 的相關概念,這樣的資訊應該足夠做為我們今天實作登入功能的基底。

這兩天我想會以說明實作程式碼,跟釐清一些觀念為主,功能測試則放到後天進行。
為什麼是後天?因為今天寫完發現篇幅實在太長了 XD,特別是 JWT Token 建立的部分。
第一次實作,不希望匆匆帶過這個過程,因此我決定將 JWT 實作內容留到明天。

讓我們先從很久之前就介紹過的 UserDetailsService 開始吧!

UserDetailsServiceImpl:載入使用者資訊的方法

第三天介紹 Spring Security 時,我們提到 UserDetailsService 這個 interface 會定義如何驗證使用者身分。實作這個介面等於是在告訴系統,當接收到使用者資訊時,要去哪裡找出驗證使用者身分。

現在讓我們在 serviceImpl package 中建立檔案 UserDetailsServiceImpl類別來實作 UserDetailsService這個介面:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("User Not Found with email: " + email));
    }
}

在此類別中,我們覆寫 loadUserByUsername 方法,告訴系統應該到哪個資料表查詢使用者資訊。若找不到使用者,拋出 UsernameNotFoundException;若找到使用者,則回傳 UserDetails 物件。

SecurityConfig:設定驗證方式與驗證人員

還記得當時我們將 UserDetailsService 比喻作保全人員,但實作完這個環節,我會說 UserDetailsService 看起來更像一本「手冊」,用來告訴保全人員如何驗證使用者身分。

真正去使用這本手冊的「保全人員」,會比較像 Spring 預設元件 ** DaoAuthenticationProvider **

AuthenticationManagerDaoAuthenticationProvider 的關聯是什麼?

  • AuthenticationManager 是一個介面,其最主要的預設實作是 ProviderManager
  • ProviderManager 內部維護了一個 AuthenticationProvider 的列表,它可以管理多種不同的認證方式。
  • 在我們的案例中,當 Spring Boot 偵測到容器中存在 UserDetailsServicePasswordEncoder 時,它會自動配置一個 DaoAuthenticationProvider
  • 這個 DaoAuthenticationProvider 會被註冊到 ProviderManager 維護的 AuthenticationProvider 列表中。

註冊 AuthenticationManager

了解來龍去脈後,我們只需將 AuthenticationManager 註冊到 Spring 容器中,就能讓其他元件(如 Controller)注入並使用它來進行驗證。因此,我們要在 SecurityConfig 中,新增獲取 AuthenticationManager 的方法:

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

DTOs:登入的請求與回應

定義好驗證的方式,也配置好驗證的人員後,我們可以進一步來定義登入的 API 端點請求與回應的內容。

LoginRequest

在 dto package 下建立以 record 宣告的 LoginRequest類別,包含 emailpassword 欄位,並加上 @NotBlank 等驗證。

public record LoginRequest(
        @NotBlank @Email
        String email,

        @NotBlank
        String password
) {}

LoginResponse

在 dto package 下建立 以 record 宣告的 LoginResponse 類別,包含 jwtTokenidemail 等成功登入後需要回傳給前端的資訊。

public record LoginResponse(
        String token,
        Long id,
        String email,
        List<String> roles
) {}

AuthController:登入的 API 端點

跳過 JwtUtils,先來撰寫登入的/login 端點,實作細節如下:

...
 @PostMapping("/login")
  public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

      // 1. 使用 AuthenticationManager 進行認證
      Authentication authentication = authenticationManager.authenticate(
              new UsernamePasswordAuthenticationToken(loginRequest.email(), loginRequest.password()));

      // 2. 將認證成功的資訊存入 SecurityContext
      SecurityContextHolder.getContext().setAuthentication(authentication);

      // 3. 產生 JWT Token
      String jwt = jwtUtils.generateJwtToken(authentication);

      // 4. 從 Authentication 物件中取得 UserDetails 
      UserEntity userDetails = (UserEntity) authentication.getPrincipal();
      List<String> roles = userDetails.getAuthorities().stream()
              .map(item -> item.getAuthority())
              .toList();

      // 5. 回傳 JWT 和使用者資訊
      return ResponseEntity.ok(new LoginResponse(jwt,
              userDetails.getId(),
              userDetails.getUsername(),
              roles));
    }
...

雖然跳過 JwtUtils,先來撰寫登入的/login 端點,可能導致目前服務還無法正常運行,但我們可以想像此處使用到 JwtUtils 的情況,就是傳入 Authentication 物件,並取得工具回傳的 JwtToken。

整體流程就像 Day 12 的循序圖:呼叫 AuthenticationManager 進行認證,認證成功後使用 JwtUtils 產生 Token,最後將 Token 與使用者資訊打包回傳。

SecurityContext 在這次登入請求存入使用者資訊其實沒有特別的作用,省略也不影響功能,但在未來有其他請求需要驗證使用者權限資訊時,就會需要將驗證結果存入 context 中。


上一篇
Day 12:Auth Service-登入流程 與 JWT Token
系列文
吃出一個SideProject!13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言