iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
佛心分享-SideProject30

吃出一個SideProject!系列 第 6

Day 6:Auth Service - 實作註冊功能(業務邏輯與API端點)

  • 分享至 

  • xImage
  •  

昨天完成 Entity 的實作,比較像是在對內部資料結構進行定義。

今天要從 Dto 開始,定義「外部」傳進來的資料長什麼樣子。

再得知外部會傳入的資料樣貌後,緊接著就可以處理業務邏輯(Service),因為業務邏輯中需要注入 passwordEncoder 來對密碼加密,中間會穿插 Config 的設定。

各項組件都具備後,再回頭定義註冊的API端點。

Dto:定義接收參數型態

在 dto 資料夾下以 record 關鍵字建立 UserRegistrationRequest 這個類別,實作內容如下:

public record UserRegistrationRequest(

        @NotBlank(message = "Email can not be blank")
        @Email(message = "Invalid email format")
        String email,

        @NotBlank(message = "Password can not be blank")
        @Size(min = 8, max = 20, message = "Password must be between 8 and 20 characters")
        String password
) {}
  • record

    是 Java 16 推出的新類別,用來建立一個不可變的 (immutable) 資料傳輸物件。使用上僅需使用 record 關鍵字並在參數中定義需要的屬性,編譯器會自動為我們產生下列內容:

    • private final 欄位:會為宣告中的每個元件(傳入的參數),建立一個對應的 private final 屬性。
    • 標準建構子 (Canonical Constructor)
    • 公開的存取器方法 (Accessor Methods)
      • 為每個欄位產生一個公開的的存取器方法。
      • 注意其產生的名稱與欄位完全相同,不同於一般習慣的 getter 命名方式。如:public String email() ,後續的 service 中使用到這個 getter 的方式就會是 registrationRequest.email()
    • 覆寫常見的方法 :equals()hashCode()toString()
  • Validation Annotations

    引入的套件 spring-boot-starter-validation 包含了規範與實作,因此只須加上 Annotation ,spring boot 即會自動幫我們進行欄位檢核。

    • @NotBlank:確保傳入的字串不是空值或只有空白。
    • @Email:驗證字串是否符合 Email 的格式。
    • @Size:限制字串的長度必須在指定的範圍內。

Config:註冊 passwordEncoder

因為在密碼傳入後,需要先加密才能存入資料庫,因此需要先定義密碼的加密方式。

根據前幾天對 Spring Secutiy 的介紹,我們會在 Config 中定義呼叫 passwordEncoder 方法要回傳給我們哪一種演算法的 Encoder,並且將其註冊於 spring 容器中,以便後續使用。

在 config 資料夾下新增 SecurityConfig類別,實作細節如下:

...
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用 BCrypt 演算法來加密密碼
        return new BCryptPasswordEncoder();
    }
...
}

@Configuration:用來宣告這個類別是一個用於設定的類別。

@EnableWebSecurity

  • 用於啟用 Spring Security Web 安全性功能。
  • 會觸發 Spring Security 引入一系列預設的 Web 安全性組態,例如:建立 Filter Chain。如果沒有這個註解,即便定義了 SecurityFilterChain@Bean,Spring Security 的安全機制也不會被啟用,所有相關的安全設定都將無效。

Service:實現業務邏輯

Service 層在此階段的職責是實現「註冊」功能。

我們首先會先建立一個Service的資料夾,再在其中建立一個Impl的資料夾

service資料夾中存放介面類型檔案;impl則存放各介面實作的內容,前者負責定義該服務中有什麼功能,並由後者實際完成那些功能,讓程式依賴介面,而非實作,降低耦合,也提升後續維護的彈性。

首先,先在 AuthService 介面中加入註冊用戶的方法,這個方法會接收剛剛 Dto 定義好的, UserRegistrationRequest 型態的參數,並回傳 UserEntity 型態的物件。

...

public interface AuthService {

    UserEntity registerUser(UserRegistrationRequest registrationRequest);

}

接著建立AuthServiceImpl 。由於以 email 作為註冊時的帳號,因此每次收到註冊請求的第一件事就是先檢查這次請求中的 Email是否存在資料庫中。實作細節如下:

...

@Service
public class AuthServiceImpl  implements AuthService {

    @Autowired
    private AuthRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public UserEntity registerUser(UserRegistrationRequest registrationRequest){
        if (userRepository.findByEmail(registrationRequest.email()).isPresent()) {
            throw new IllegalStateException("該信箱已被註冊");
        }

        UserEntity user = new UserEntity();
        user.setEmail(registrationRequest.email());
        user.setPassword(passwordEncoder.encode(registrationRequest.password()));

        return userRepository.save(user);
    }
}

  • save()
    • 此方法會自動檢查目標資料表中是否已有相同 Primary Key 的資料,若沒有會insert 一筆新的資料;若有則會以 update 的方式更新該筆資料內容。無論是新增還是更新,都會回傳執行後物件資訊。
    • 上面的範例因為是新用戶註冊,並不會由使用者提供Primary Key(Id),因此會以insert的方式在資料庫加入這筆資料,並且回傳已帶有 Id 的物件資訊。
  • @Service 衍生自@Component,用意也是將此類別註冊spring容器中,但比使用@Compoennt語意更清晰,提升程式可讀性。
  • @Autowired :用於注入 (Inject) 需要的依賴(如AuthRepositorypasswordEncoder)。

Controller:定義 API 端點

在 controller 資料夾下新增 AuthController 類別,並新增 registerUser 方法,用以定義 API 端點對應的內容。其實作細節如下:

...

@RestController
@RequestMapping("/users")
public class AuthController {

    @Autowired
    private AuthService authService;

    @PostMapping
    public ResponseEntity<?> registerUser(@Valid @RequestBody UserRegistrationRequest registrationRequest) {
        try {
            // 這裡呼叫 service,service 內部會處理 UserEntity 的建立
            UserEntity user = authService.registerUser(registrationRequest);
            return ResponseEntity.status(HttpStatus.CREATED).build();
        } catch (IllegalStateException e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }
}

  • @RestController:宣告這是一個 RESTful 風格的 Controller。
  • @RequestMapping("/users"):定義此 Controller 底下所有 API 的路徑前綴為 /users。
  • @Valid:這個註解對應到剛剛在 UserRegistrationRequest DTO 中定義的驗證規則。如果驗證失敗,Spring 會自動回傳 400 Bad Request 的錯誤。
  • @RequestBody:告訴 Spring 要從 HTTP 請求的 body 中讀取 JSON 資料,並轉換成 UserRegistrationRequest 物件。
  • ResponseEntity:用以建構包含 HTTP 狀態碼的回應。

Config: 控制 API 訪問權限

上述功能都完成後,我們回到 Config 中完成 SecurityFilterChain 的設定。

SecurityFilterChain 是 Spring Security 框架中的核心元件,以鏈(Chain)的方式,將各項過濾器(Filter)串起。

    ...
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/users/**").permitAll() 
                        .anyRequest().authenticated()
                );

        return http.build();
    }
  • HttpSecurity
    Spring Security 框架中,用於設定 Web 層面安全性 的物件。透過提供組合的 Filter 來定義安全規則,並最終呼叫 .build() 來產生設定完成的 SecurityFilterChain
    其實就是在告訴 Spring Security,針對 http 請求,我們想要什麼樣的安全規則。
  • CSRF Filter:用來檢查是否有 CSRF 攻擊。為了後續進行API測試,這個階段先以 disable 將此檢查關閉。
  • .authorizeHttpRequests(...):用來宣告要配置 Http 請求的授權規則。所有 URL 的存取權限都會在此方法內進行定義。
  • requestMatchers("/users/**").permitAll():在此階段針對 /users 路徑進行設定,並允許所有使用者對其的存取 (permitAll),確保註冊 API 能被公開訪問。
  • return http.build();:將在 HttpSecurity 內的所有設定,包裝成一個不可變的 SecurityFilterChain 物件。這個物件會被 Spring 容器註冊為 Bean,並實際套用為應用程式的安全規則。

今天因為希望盡量將相關的內容在同一章節內說明,還將很多自己在第一次開發時會遇到的疑問寫了下來,如果有誤,還望各位讀者大神不吝賜教。

至此,我們初步完成了註冊功能,明天開始我們來試著撰寫 dockerfile,並透過 docker-compose 啟動本專案與 posrgreSQL 的服務,最後簡單透過 API Tester 測試 API 是否正常回應。


上一篇
Day5:Auth Service - 實作註冊功能(資料層)
下一篇
Day 7:撰寫 Dockerfile
系列文
吃出一個SideProject!8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言