Bài viết gốc xem ở đây.
1. Tổng quát
Như tiêu đề bài viết, chúng ta sẽ triển khai phần đăng ký cơ bản sử dụng Spring Security. Phần này được xây dựng dựa trên các khái niệm đã được đề cập ở bài viết trước, bài viết chúng ta đã bàn về việc đăng nhập.
Mục tiêu của phần này là thêm vào một quy trình đăng ký đầy đủ cho phép người dùng đăng ký, xác thực và lưu trữ thông tin người dùng.
Trang đăng ký
Đầu tiên, hãy triển khai một trang đăng ký đơn giản hiển thị các trường sau:
- tên (họ và tên)
- mật khẩu (và trường xác nhận lại mật khẩu)
Ví dụ sau sẽ giới thiệu một trang registration.html đơn giản:
Ví dụ 2.1.
<html>
<body>
<h1 th:text="#{label.form.title}">form</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
<div>
<label th:text="#{label.user.firstName}">first</label>
<input th:field="*{firstName}"/>
<p th:each="error: ${#fields.errors('firstName')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.lastName}">last</label>
<input th:field="*{lastName}"/>
<p th:each="error : ${#fields.errors('lastName')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.email}">email</label>
<input type="email" th:field="*{email}"/>
<p th:each="error : ${#fields.errors('email')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.password}">password</label>
<input type="password" th:field="*{password}"/>
<p th:each="error : ${#fields.errors('password')}"
th:text="${error}">Validation error</p>
</div>
<div>
<label th:text="#{label.user.confirmPass}">confirm</label>
<input type="password" th:field="*{matchingPassword}"/>
</div>
<button type="submit" th:text="#{label.form.submit}">submit</button>
</form>
<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">login</a>
</body>
</html>
3. Đối tượng User DTO
Chúng ta cần một Data Transfer Object để gửi tất cả thông tin đăng ký tới backend Spring của chúng ta. Đối tượng DTO nên có tất cả thông tin chúng ta yêu cầu người dùng điền vào, nó tương tự với đối tượng User:
public class UserDto {
@NotNull
@NotEmpty
private String firstName;
@NotNull
@NotEmpty
private String lastName;
@NotNull
@NotEmpty
private String password;
private String matchingPassword;
@NotNull
@NotEmpty
private String email;
// standard getters and setters
}
Chú ý chúng ta sử dụng annotation javax.validation tiêu chuẩn trong các trường của đối tượng DTO. Sau đó, chúng ta sẽ triển khai các annotation xác thực tùy chỉnh riêng của mình để xác thực định dạng của địa chỉ email cũng như xác nhận mật khẩu. (xem thêm ở Phần 5)
4. Controller phần đăng ký
Một đường dẫn đăng ký trên trang đăng nhập sẽ dẫn người dùng đến trang đăng ký. Đây là phần backend cho trang này được điều hướng và mapped với “/user/registration”.
Ví dụ 4.1 - Phương thức showRegistration
@GetMapping("/user/registration")
public String showRegistrationForm(WebRequest request, Model model) {
UserDto userDto = new UserDto();
model.addAttribute("user", userDto);
return "registration";
}
Khi controller nhận được yêu cầu “/user/registration”, nó tạo một đối tượng UserDto mới có nhiệm vụ tiếp nhận thông tin từ form đăng ký, liên kết dữ liệu và trả nó về.
5. Xác thực thông tin đăng ký
Tiếp theo, hãy xem các điều cần xác thực khi controller xử lý một đăng ký tài khoản mới:
- Tất cả các trường phải được điền đầy đủ (Không được để trường trống)
- Địa chỉ email phải hợp lệ (đúng chuẩn)
- Mật khẩu ở trường xác nhận lại phải trùng với mật khẩu vừa nhập bên trên
- Tài khoản này phải không tồn tại trước đó
5.1. Tích hợp phần xác thực
Đối với các kiểm tra đơn giản, chúng ta sẽ sử dụng annotation xác thực out of the box bean trong đối tượng DTO - các annotation như @NotNull, @NotEmpty, etc.
Để kích hoạt quá trình xác thực, chúng ta sẽ chú thích đối tượng trong lớp controller với annotation @Valid:
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
...
}
5.2. Tuỳ biến xác thực email
Tiếp theo, chúng ta sẽ xác thực địa chỉ email và chắc chắn rằng nó đúng chuẩn. Chúng ta sẽ xây dựng một trình xác thực tuỳ chỉnh cho nó, cũng như annotation tuỳ chỉnh, chúng ta sẽ gọi nó là @ValidEmail.
Chú thích nhanh ở đây - chúng ta làm annotation tuỳ chỉnh của chúng ta thay cho @Email của Hibernate bởi vì Hibernate coi định dạng mạng nội bộ kiểu cũ là hợp lệ: myaddress@myserver là hợp lệ (xem thêm bài viết ở Stackoverflow), điều này không ổn.
Đây là annotation xác thực email và trình xác thực tuỳ chỉnh:
Ví dụ 5.2.1. - Annotation tuỳ chỉnh cho trình xác thực email
@Target({TYPE, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = EmailValidator.class)
@Documented
public @interface ValidEmail {
String message() default "Invalid email";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Chú ý rằng chúng ta đã định nghĩa annotation ở cấp FIELD - vì đó là nơi nó được áp dụng về mặt khái niệm.
Ví dụ 5.2.2. - EmailValidator tuỳ chỉnh:
public class EmailValidator
implements ConstraintValidator<ValidEmail, String> {
private Pattern pattern;
private Matcher matcher;
private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-+]+
(.[_A-Za-z0-9-]+)*@" + "[A-Za-z0-9-]+(.[A-Za-z0-9]+)*
(.[A-Za-z]{2,})$";
@Override
public void initialize(ValidEmail constraintAnnotation) {
}
@Override
public boolean isValid(String email, ConstraintValidatorContext context){
return (validateEmail(email));
}
private boolean validateEmail(String email) {
pattern = Pattern.compile(EMAIL_PATTERN);
matcher = pattern.matcher(email);
return matcher.matches();
}
}
Bây giờ sẽ sử dụng annotation mới trong UserDto của chúng ta đã triển khai:
@ValidEmail
@NotNull
@NotEmpty
private String email;
5.3. Sử dụng xác thực tuỳ chỉnh để xác nhận mật khẩu
Chúng ta cần một annotation tuỳ chỉnh và trình xác thực để chắc chắn rằng người dùng đã nhập mật khẩu ở cả hai trường giống nhau:
Ví dụ 5.3.1. - Tuỳ chỉnh annotation cho trình xác nhận mật khẩu
@Target({TYPE,ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = PasswordMatchesValidator.class)
@Documented
public @interface PasswordMatches {
String message() default "Passwords don't match";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Chú ý rằng annotation @Target cho biết rằng đây là một annotation cấp TYPE. Điều này bởi vì chúng ta cần toàn bộ đối tượng UserDto để xử lý xác thực.
Trình xác thực tuỳ chỉnh sẽ được gọi bởi annotation như bên dưới đây:
Ví dụ 5.3.2. Trình xác thực tuỳ chỉnh PasswordMatchesValidator
public class PasswordMatchesValidator
implements ConstraintValidator<PasswordMatches, Object> {
@Override
public void initialize(PasswordMatches constraintAnnotation) {
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context){
UserDto user = (UserDto) obj;
return user.getPassword().equals(user.getMatchingPassword());
}
}
Bây giờ, annotation @PasswordMathces sẽ được áp dụng vào đối tượng UserDto:
@PasswordMatches
public class UserDto {
...
}
Tất cả trình xác thực tuỳ biến sẽ được chạy song song với các annotation tiêu chuẩn trong quá trình xác thực chạy.
5.4. Kiểm tra xem tài khoản đã tồn tại chưa
Điều thứ tư chúng ta cần triển khai đó là kiểm tra xem tài khoản đã tồn tại trong cơ sở dữ liệu chưa.
Điều này sẽ được thực hiện sau khi biểu mẫu đã được xác thực và nó sẽ được thực hiện với việc triển khai UserService.
Ví dụ 5.4.1. - Controller của phương thức createUserAccount gọi đối tượng UserService
@PostMapping("/user/registration")
public ModelAndView registerUserAccount
(@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserAlreadyExistException uaeEx) {
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
// rest of the implementation
}
Ví dụ 5.4.2. - UserService kiểm tra email trùng lặp
@Service
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Transactional
@Override
public User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException {
if (emailExist(userDto.getEmail())) {
throw new UserAlreadyExistException(
"There is an account with that email address: "
+ userDto.getEmail());
}
...
// the rest of the registration operation
}
private boolean emailExist(String email) {
return userRepository.findByEmail(email) != null;
}
}
UserService dựa vào class UserRepository để kiểm tra xem tài khoản đã tồn tại trong cơ sở dữ liệu chưa.
Bây giờ, việc triển khai UserRepository trong lớp persistence không thuộc trong bài viết này. Một cách nhanh chóng, tất nhiên là sử dụng Spring Data để sinh ra lớp repository.
6. Lưu dữ liệu và hoàn thành biểu mẫu
Cuối cùng, chúng ta triển khai logic đăng ký trong lớp controller của chúng ta.
Ví dụ 6.1.1. - Phương thức RegisterAccount trong Controller
@PostMapping("/user/registration")
public ModelAndView registerUserAccount(
@ModelAttribute("user") @Valid UserDto userDto,
HttpServletRequest request, Errors errors) {
try {
User registered = userService.registerNewUserAccount(userDto);
} catch (UserAlreadyExistException uaeEx) {
mav.addObject("message", "An account for that username/email already exists.");
return mav;
}
return new ModelAndView("successRegister", "user", userDto);
}
Những điều cần lưu ý trong đoạn code trên:
- Controller đang trả về một đối tượng ModelAndView là class thuận tiện để gửi model data (user) được liên kết với view.
- Controller sẽ điều hướng tới form đăng ký nếu có bất kỳ lỗi nào trong quá trình xác thực.
7. UserService - Quy trình đăng ký
Cuối cùng cùng triển khai quy trình đăng ký trong UserService:
Ví dụ 7.1. Interface IUserService
public interface IUserService {
User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException;
}
Ví dụ 7.2. - Class UserService
@Service
public class UserService implements IUserService {
@Autowired
private UserRepository repository;
@Transactional
@Override
public User registerNewUserAccount(UserDto userDto)
throws UserAlreadyExistException {
if (emailExists(userDto.getEmail())) {
throw new UserAlreadyExistException(
"There is an account with that email address:
+ userDto.getEmail());
}
User user = new User();
user.setFirstName(userDto.getFirstName());
user.setLastName(userDto.getLastName());
user.setPassword(userDto.getPassword());
user.setEmail(userDto.getEmail());
user.setRoles(Arrays.asList("ROLE_USER"));
return repository.save(user);
}
private boolean emailExists(String email) {
return userRepository.findByEmail(email) != null;
}
}
8. Tải chi tiết User cho đăng nhập bảo mật
Trong bài viết trước của chúng ta, đăng nhập đã được mã hoá cứng. Hãy thay đổi nó và sử dụng thông tin đăng nhập mới sau đó mã hoá nó. Chúng ta sẽ triển khai UserDetailsService được tuỳ chỉnh để kiểm tra chứng chỉ cho biểu mẫu đăng nhập từ lớp persistence:
8.1. Tuỳ chỉnh UserDetailsService
Cùng bắt đầu triển khai service chi tiết user được tuỳ chỉnh
@Service
@Transactional
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
//
public UserDetails loadUserByUsername(String email)
throws UsernameNotFoundException {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(
"No user found with username: "+ email);
}
boolean enabled = true;
boolean accountNonExpired = true;
boolean credentialsNonExpired = true;
boolean accountNonLocked = true;
return new org.springframework.security.core.userdetails.User
(user.getEmail(),
user.getPassword().toLowerCase(), enabled, accountNonExpired,
credentialsNonExpired, accountNonLocked,
getAuthorities(user.getRoles()));
}
private static List<GrantedAuthority> getAuthorities (List<String> roles) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (String role : roles) {
authorities.add(new SimpleGrantedAuthority(role));
}
return authorities;
}
}
8.2. Nhúng cách xác thực mới
Để nhúng service user mới trong cấu hình Spring Security - chúng ta đơn giản chỉ cần thêm UserDetailsService trong đối tượng authentication-manager và thêm bean UserDetailsService:
Ví dụ 8.2. - Authentication Manager và UserDetailsService
<authentication-manager>
<authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>
<beans:bean id="userDetailsService"
class="com.baeldung.security.MyUserDetailsService"/>
Hoặc cấu hình Java:
@Autowired
private MyUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.userDetailsService(userDetailsService);
}
9. Tổng kết
Và cuối cùng chúng ta đã gần như hoàn thành triển khai quy trình đăng ký với Spring Security và Spring MVC. Tiếp theo, chúng ta sẽ thảo luận về quá trình kích hoạt tài khoản mới bằng email của user.
Hướng dẫn REST Spring Security có thể xem ở GitHub này - đây là project ở Eclipse vì vậy nó rất dễ dàng import và chạy nó.
Bình luận