昨天完成 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 是否正常回應。