5.5 Nhận diện người dùng của bạn
Thường thì chỉ biết rằng người dùng đã đăng nhập và có những quyền gì là chưa đủ. Thông thường, bạn cũng cần biết họ là ai để có thể tùy biến trải nghiệm cho họ.
Ví dụ, trong OrderController, khi bạn khởi tạo đối tượng TacoOrder được liên kết với form đặt hàng, sẽ rất tiện lợi nếu bạn có thể điền sẵn thông tin tên và địa chỉ của người dùng vào TacoOrder, để họ không phải nhập lại cho mỗi đơn hàng. Còn quan trọng hơn, khi bạn lưu đơn hàng, bạn nên liên kết thực thể TacoOrder với User đã tạo đơn hàng đó.
Để thiết lập mối liên hệ giữa thực thể TacoOrder và User, bạn cần thêm thuộc tính sau vào lớp TacoOrder:
@Data
@Entity
@Table(name="Taco_Order")
public class TacoOrder implements Serializable {
...
@ManyToOne
private User user;
...
}Chú thích @ManyToOne trên thuộc tính này cho biết một đơn hàng thuộc về một người dùng, và ngược lại, một người dùng có thể có nhiều đơn hàng. (Vì bạn đang sử dụng Lombok, bạn không cần khai báo các phương thức getter/setter cho thuộc tính này.)
Trong OrderController, phương thức processOrder() chịu trách nhiệm lưu đơn hàng. Nó cần được chỉnh sửa để xác định người dùng đã xác thực là ai và gọi setUser() trên đối tượng TacoOrder để liên kết đơn hàng với người dùng đó.
Chúng ta có một số cách để xác định người dùng là ai. Dưới đây là một vài cách phổ biến nhất:
- Tiêm một đối tượng
java.security.Principalvào phương thức controller. - Tiêm một đối tượng
org.springframework.security.core.Authenticationvào phương thức controller. - Sử dụng
org.springframework.security.core.context.SecurityContextHolderđể truy cập vào ngữ cảnh bảo mật. - Tiêm tham số phương thức được chú thích với
@AuthenticationPrincipal. (@AuthenticationPrincipalđến từ packageorg.springframework.security.core.annotationcủa Spring Security.)
Ví dụ, bạn có thể sửa processOrder() để chấp nhận một đối tượng java.security.Principal làm tham số. Sau đó, bạn có thể sử dụng tên principal để tìm người dùng từ UserRepository như sau:
@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
SessionStatus sessionStatus,
Principal principal) {
...
User user = userRepository.findByUsername(
principal.getName());
order.setUser(user);
...
}Cách này hoạt động tốt, nhưng nó khiến cho mã vốn không liên quan đến bảo mật bị lộn xộn bởi mã bảo mật. Bạn có thể tinh giản mã bảo mật bằng cách sửa processOrder() để nhận một đối tượng Authentication làm tham số thay vì Principal, như sau:
@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
SessionStatus sessionStatus,
Authentication authentication) {
...
User user = (User) authentication.getPrincipal();
order.setUser(user);
...
}Với Authentication trong tay, bạn có thể gọi getPrincipal() để lấy đối tượng principal, trong trường hợp này là một User. Lưu ý rằng getPrincipal() trả về một java.util.Object, vì vậy bạn cần ép kiểu sang User.
Tuy nhiên, có lẽ cách rõ ràng và gọn gàng nhất là đơn giản chấp nhận một đối tượng User trong processOrder() và chú thích nó bằng @AuthenticationPrincipal để nó chính là principal của xác thực, như sau:
@PostMapping
public String processOrder(@Valid TacoOrder order, Errors errors,
SessionStatus sessionStatus,
@AuthenticationPrincipal User user) {
if (errors.hasErrors()) {
return "orderForm";
}
order.setUser(user);
orderRepo.save(order);
sessionStatus.setComplete();
return "redirect:/";
}Điều hay ho về @AuthenticationPrincipal là bạn không cần ép kiểu (như với Authentication), và nó giới hạn mã bảo mật trong chính chú thích đó. Khi bạn nhận được đối tượng User trong processOrder(), nó đã sẵn sàng để sử dụng và gán cho TacoOrder.
Có một cách khác để xác định người dùng đã xác thực là ai, mặc dù nó hơi rối vì dính nhiều mã bảo mật. Bạn có thể lấy đối tượng Authentication từ security context và sau đó lấy principal như sau:
Authentication authentication =
SecurityContextHolder.getContext().getAuthentication();
User user = (User) authentication.getPrincipal();Mặc dù đoạn mã này dày đặc mã bảo mật, nhưng nó có một lợi thế so với các cách khác đã mô tả: nó có thể được sử dụng ở bất kỳ đâu trong ứng dụng, không chỉ trong các phương thức xử lý của controller. Điều này làm cho nó phù hợp để sử dụng ở các tầng thấp hơn của mã nguồn.
