昨天完成 Entity 的實作,比較像是在對內部資料結構進行定義。
今天要從 Dto 開始,定義「外部」傳進來的資料長什麼樣子。
再得知外部會傳入的資料樣貌後,緊接著就可以處理業務邏輯(Service),因為業務邏輯中需要注入 passwordEncoder 來對密碼加密,中間會穿插 Config 的設定。
各項組件都具備後,再回頭定義註冊的API端點。
在 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 屬性。public String email() ,後續的 service 中使用到這個 getter 的方式就會是 registrationRequest.email()
equals() 、 hashCode() 與toString()
Validation Annotations:
引入的套件 spring-boot-starter-validation 包含了規範與實作,因此只須加上 Annotation ,spring boot 即會自動幫我們進行欄位檢核。
@NotBlank:確保傳入的字串不是空值或只有空白。@Email:驗證字串是否符合 Email 的格式。@Size:限制字串的長度必須在指定的範圍內。因為在密碼傳入後,需要先加密才能存入資料庫,因此需要先定義密碼的加密方式。
根據前幾天對 Spring Secutiy 的介紹,我們會在 Config 中定義呼叫 passwordEncoder 方法要回傳給我們哪一種演算法的 Encoder,並且將其註冊於 spring 容器中,以便後續使用。
在 config 資料夾下新增 SecurityConfig類別,實作細節如下:
...
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        // 使用 BCrypt 演算法來加密密碼
        return new BCryptPasswordEncoder();
    }
...
}
@Configuration:用來宣告這個類別是一個用於設定的類別。
@EnableWebSecurity:
SecurityFilterChain 的 @Bean,Spring Security 的安全機制也不會被啟用,所有相關的安全設定都將無效。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()
@Service : 衍生自@Component,用意也是將此類別註冊spring容器中,但比使用@Compoennt語意更清晰,提升程式可讀性。@Autowired :用於注入 (Inject) 需要的依賴(如AuthRepository 和 passwordEncoder)。在 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 中完成 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:.build() 來產生設定完成的 SecurityFilterChain。http 請求,我們想要什麼樣的安全規則。.authorizeHttpRequests(...):用來宣告要配置 Http 請求的授權規則。所有 URL 的存取權限都會在此方法內進行定義。requestMatchers("/users/**").permitAll():在此階段針對 /users 路徑進行設定,並允許所有使用者對其的存取 (permitAll),確保註冊 API 能被公開訪問。return http.build();:將在 HttpSecurity 內的所有設定,包裝成一個不可變的 SecurityFilterChain 物件。這個物件會被 Spring 容器註冊為 Bean,並實際套用為應用程式的安全規則。今天因為希望盡量將相關的內容在同一章節內說明,還將很多自己在第一次開發時會遇到的疑問寫了下來,如果有誤,還望各位讀者大神不吝賜教。
至此,我們初步完成了註冊功能,明天開始我們來試著撰寫 dockerfile,並透過 docker-compose 啟動本專案與 posrgreSQL 的服務,最後簡單透過 API Tester 測試 API 是否正常回應。