Skip to content

5.2.2 Tùy chỉnh xác thực người dùng

Trong chương trước, bạn đã chọn sử dụng Spring Data JPA làm tùy chọn lưu trữ cho tất cả dữ liệu taco, nguyên liệu và đơn hàng. Vì vậy, việc lưu trữ dữ liệu người dùng theo cùng cách là hợp lý. Nếu làm vậy, dữ liệu cuối cùng sẽ nằm trong cơ sở dữ liệu quan hệ, vì thế bạn có thể sử dụng xác thực JDBC. Nhưng sẽ còn tốt hơn nếu tận dụng repository của Spring Data JPA để lưu trữ người dùng.

Tuy nhiên, trước tiên hãy tạo đối tượng miền (domain object) và interface repository để đại diện và lưu trữ thông tin người dùng.

ĐỊNH NGHĨA DOMAIN VÀ LỚP PERSISTENCE CỦA NGƯỜI DÙNG

Khi khách hàng Taco Cloud đăng ký với ứng dụng, họ sẽ cần cung cấp nhiều hơn chỉ tên người dùng và mật khẩu. Họ cũng sẽ cung cấp họ tên đầy đủ, địa chỉ và số điện thoại. Thông tin này có thể được sử dụng cho nhiều mục đích khác nhau, bao gồm điền sẵn vào mẫu đơn đặt hàng (chưa kể đến các cơ hội tiếp thị tiềm năng).

Để thu thập tất cả thông tin đó, bạn sẽ tạo một lớp User như sau.

Liệt kê 5.3 Định nghĩa một entity người dùng

java
package tacos;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.
                      SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

@Entity
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
public class User implements UserDetails {

  private static final long serialVersionUID = 1L;

  @Id
  @GeneratedValue(strategy=GenerationType.AUTO)
  private Long id;

  private final String username;
  private final String password;
  private final String fullname;
  private final String street;
  private final String city;
  private final String state;
  private final String zip;
  private final String phoneNumber;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }

}

Điều đầu tiên cần lưu ý về kiểu User này là nó không giống với lớp User mà chúng ta đã sử dụng khi tạo dịch vụ người dùng trong bộ nhớ. Lớp này chứa nhiều thông tin chi tiết hơn về người dùng mà chúng ta cần để xử lý đơn đặt hàng taco, bao gồm địa chỉ và thông tin liên lạc của người dùng.

Bạn cũng có thể nhận thấy rằng lớp User này phức tạp hơn một chút so với bất kỳ entity nào đã được định nghĩa trong chương 3. Ngoài việc định nghĩa một vài thuộc tính, User còn triển khai interface UserDetails từ Spring Security.

Các lớp triển khai UserDetails sẽ cung cấp một số thông tin người dùng thiết yếu cho framework, chẳng hạn như những quyền nào được cấp cho người dùng và liệu tài khoản người dùng có được kích hoạt hay không.

Phương thức getAuthorities() nên trả về một tập hợp các quyền được cấp cho người dùng. Các phương thức is* trả về giá trị boolean để cho biết liệu tài khoản người dùng có được kích hoạt, bị khóa hoặc hết hạn hay không.

Đối với entity User của bạn, phương thức getAuthorities() chỉ đơn giản trả về một tập hợp cho biết rằng tất cả người dùng đều được cấp quyền ROLE_USER. Và, ít nhất là hiện tại, Taco Cloud không cần vô hiệu hóa người dùng, nên tất cả các phương thức is* đều trả về true để biểu thị rằng người dùng đang hoạt động.

Khi entity User đã được định nghĩa, bạn có thể định nghĩa interface repository như sau:

java
package tacos.data;
import org.springframework.data.repository.CrudRepository;
import tacos.User;

public interface UserRepository extends CrudRepository<User, Long> {

  User findByUsername(String username);

}

Ngoài các thao tác CRUD được cung cấp bởi việc mở rộng CrudRepository, UserRepository còn định nghĩa phương thức findByUsername() mà bạn sẽ sử dụng trong dịch vụ chi tiết người dùng để tra cứu User theo tên người dùng.

Như bạn đã học trong chương 3, Spring Data JPA sẽ tự động tạo implementation cho interface này trong thời gian chạy. Vì vậy, bạn đã sẵn sàng để viết một dịch vụ chi tiết người dùng tùy chỉnh sử dụng repository này.

TẠO DỊCH VỤ CHI TIẾT NGƯỜI DÙNG

Như bạn nhớ, interface UserDetailsService chỉ định nghĩa một phương thức duy nhất là loadUserByUsername(). Điều đó có nghĩa là nó là một functional interface và có thể được triển khai bằng một lambda thay vì một lớp triển khai đầy đủ. Bởi vì tất cả những gì chúng ta thực sự cần là dịch vụ UserDetailsService tùy chỉnh ủy quyền cho UserRepository, nên nó có thể được khai báo đơn giản như một bean bằng phương thức cấu hình sau.

Liệt kê 5.4 Định nghĩa một bean dịch vụ chi tiết người dùng tùy chỉnh

java
@Bean
public UserDetailsService userDetailsService(UserRepository userRepo) {
  return username -> {
    User user = userRepo.findByUsername(username);
    if (user != null) return user;

    throw new UsernameNotFoundException("User '" + username + "' not found");
  };
}

Phương thức userDetailsService() nhận một UserRepository làm tham số. Để tạo bean, nó trả về một lambda nhận một tham số là tên người dùng và dùng nó để gọi findByUsername() trên UserRepository được cung cấp.

Phương thức loadByUsername() có một quy tắc đơn giản: nó không bao giờ được trả về null. Vì vậy, nếu gọi findByUsername() trả về null, lambda sẽ ném ra một UsernameNotFoundException (được định nghĩa bởi Spring Security). Ngược lại, đối tượng User tìm thấy sẽ được trả về.

Giờ đây bạn đã có một dịch vụ chi tiết người dùng tùy chỉnh đọc thông tin người dùng thông qua một repository JPA, bạn chỉ cần một cách để thêm người dùng vào cơ sở dữ liệu ngay từ đầu. Bạn cần tạo một trang đăng ký để khách hàng Taco Cloud có thể đăng ký với ứng dụng.

ĐĂNG KÝ NGƯỜI DÙNG

Mặc dù Spring Security xử lý nhiều khía cạnh của bảo mật, nhưng nó không thực sự tham gia trực tiếp vào quá trình đăng ký người dùng, vì vậy bạn sẽ dựa vào một chút Spring MVC để xử lý tác vụ đó. Lớp RegistrationController trong đoạn mã sau sẽ trình bày và xử lý các biểu mẫu đăng ký.

Liệt kê 5.5 Bộ điều khiển đăng ký người dùng

java
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import tacos.data.UserRepository;
@Controller
@RequestMapping("/register")
public class RegistrationController {

  private UserRepository userRepo;
  private PasswordEncoder passwordEncoder;

  public RegistrationController(
    UserRepository userRepo, PasswordEncoder passwordEncoder) {
      this.userRepo = userRepo;
      this.passwordEncoder = passwordEncoder;
  }

  @GetMapping
  public String registerForm() {
    return "registration";
  }

  @PostMapping
  public String processRegistration(RegistrationForm form) {
    userRepo.save(form.toUser(passwordEncoder));
    return "redirect:/login";
  }
}

Giống như bất kỳ bộ điều khiển Spring MVC điển hình nào, RegistrationController được chú thích với @Controller để chỉ định nó là một bộ điều khiển và đánh dấu nó cho quét thành phần. Nó cũng được chú thích với @RequestMapping sao cho nó sẽ xử lý các yêu cầu có đường dẫn là /register.

Cụ thể hơn, một yêu cầu GET đến /register sẽ được xử lý bởi phương thức registerForm(), phương thức này chỉ đơn giản trả về tên view logic là registration. Danh sách sau đây hiển thị một mẫu Thymeleaf định nghĩa view registration.

Liệt kê 5.6 View biểu mẫu đăng ký Thymeleaf

html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">

  <head>
    <title>Taco Cloud</title>
  </head>

  <body>
    <h1>Register</h1>

    <img th:src="@{/images/TacoCloud.png}"/>
    
    <form method="POST" th:action="@{/register}" id="registerForm">
      <label for="username">Username: </label>
      <input type="text" name="username"/><br/>
      <label for="password">Password: </label>
      <input type="password" name="password"/><br/>
      <label for="confirm">Confirm password: </label>
      <input type="password" name="confirm"/><br/>
      <label for="fullname">Full name: </label>
      <input type="text" name="fullname"/><br/>
      <label for="street">Street: </label>
      <input type="text" name="street"/><br/>
      <label for="city">City: </label>
      <input type="text" name="city"/><br/>
      <label for="state">State: </label>
      <input type="text" name="state"/><br/>
      <label for="zip">Zip: </label>
      <input type="text" name="zip"/><br/>
      <label for="phone">Phone: </label>
      <input type="text" name="phone"/><br/>
      <input type="submit" value="Register"/>
    </form>
  </body>
</html>

Khi biểu mẫu được gửi đi, phương thức processRegistration() sẽ xử lý yêu cầu POST qua HTTPS. Các trường trong biểu mẫu sẽ được Spring MVC gắn kết vào một đối tượng RegistrationForm và truyền vào phương thức processRegistration() để xử lý. RegistrationForm được định nghĩa trong lớp sau:

java
package tacos.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.Data;
import tacos.User;

@Data
public class RegistrationForm {

  private String username;
  private String password;
  private String fullname;
  private String street;
  private String city;
  private String state;
  private String zip;
  private String phone;

  public User toUser(PasswordEncoder passwordEncoder) {
    return new User(
      username, passwordEncoder.encode(password),
      fullname, street, city, state, zip, phone);
  }

}

Phần lớn, RegistrationForm chỉ là một lớp Lombok cơ bản với một vài thuộc tính. Nhưng phương thức toUser() sử dụng các thuộc tính đó để tạo ra một đối tượng User mới, đó là đối tượng mà processRegistration() sẽ lưu, sử dụng UserRepository được tiêm vào.

Bạn chắc chắn đã để ý rằng RegistrationController được tiêm vào một PasswordEncoder. Đây chính là bean PasswordEncoder mà bạn đã khai báo trước đó. Khi xử lý việc gửi biểu mẫu, RegistrationController truyền nó vào phương thức toUser(), phương thức này sử dụng nó để mã hóa mật khẩu trước khi lưu vào cơ sở dữ liệu. Theo cách này, mật khẩu được gửi sẽ được ghi ở dạng mã hóa, và dịch vụ chi tiết người dùng sẽ có thể xác thực đối chiếu với mật khẩu đã mã hóa đó.

Giờ đây, ứng dụng Taco Cloud đã hỗ trợ đầy đủ chức năng đăng ký và xác thực người dùng. Nhưng nếu bạn khởi động nó ở thời điểm này, bạn sẽ thấy rằng bạn thậm chí không thể truy cập vào trang đăng ký mà không bị yêu cầu đăng nhập. Đó là vì, theo mặc định, tất cả các yêu cầu đều yêu cầu xác thực. Hãy xem cách các yêu cầu web được chặn và bảo mật để bạn có thể sửa tình huống "con gà và quả trứng" kỳ lạ này.

Released under the MIT License.