Spring Security 註冊流程

Spring Security
Remote
1
09:24 PM · Nov 29 ,2025

1. 概述在本教程中,我們將使用 Spring Security 實現一個基本的註冊流程。我們將在此基礎上構建我們之前文章中探索的關於登錄的概念。

這裏的目標是添加一個完整的註冊流程,允許用户註冊,並驗證和持久化用户數據。

2. 註冊頁面首先,我們將實現一個簡單的註冊頁面,顯示以下字段

姓名(名和姓)郵箱密碼(以及密碼確認字段)以下示例顯示了一個簡單的registration.html頁面:

示例 2.1.


<html>
<body>
<h1 th:text="#{label.form.title}">表單</h1>
<form action="/" th:object="${user}" method="POST" enctype="utf8">
    <div>
        <label th:text="#{label.user.firstName}">名</label>
        <input th:field="*{firstName}"/>
        <p th:each="error: ${#fields.errors('firstName')}" 
          th:text="${error}">驗證錯誤</p>
    </div>
    <div>
        <label th:text="#{label.user.lastName}">姓</label>
        <input th:field="*{lastName}"/>
        <p th:each="error : ${#fields.errors('lastName')}" 
          th:text="${error}">驗證錯誤</p>
    </div>
    <div>
        <label th:text="#{label.user.email}">郵箱</label>
        <input type="email" th:field="*{email}"/>
        <p th:each="error : ${#fields.errors('email')}" 
          th:text="${error}">驗證錯誤</p>
    </div>
    <div>
        <label th:text="#{label.user.password}">密碼</label>
        <input type="password" th:field="*{password}"/>
        <p th:each="error : ${#fields.errors('password')}" 
          th:text="${error}">驗證錯誤</p>
    </div>
    <div>
        <label th:text="#{label.user.confirmPass}">確認</label>
        <input type="password" th:field="*{matchingPassword}"/>
    </div>
    <button type="submit" th:text="#{label.form.submit}">提交</button>
</form>

<a th:href="@{/login.html}" th:text="#{label.form.loginLink}">登錄</a>
</body>
</html>

3.用户 DTO 對象

我們需要一個 數據傳輸對象,用於將所有註冊信息發送到我們的 Spring 後端。 DTO 對象應包含我們稍後在創建和填充我們的 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;
    
    // 標準的 getter 和 setter
}

請注意,我們使用了標準的 javax.validation 標註在 DTO 對象的字段上。 稍後,我們還將 實現我們自己的自定義驗證標註,以驗證電子郵件地址的格式,以及密碼確認(參見 第 5 節)。

4. 註冊控制器

登錄頁面上的 註冊鏈接 將用户帶到 註冊 頁面。該頁面的後端邏輯位於註冊控制器中,並映射到 “/user/registration”

示例 4.1. showRegistration 方法

@GetMapping("/user/registration")
public String showRegistrationForm(WebRequest request, Model model) {
    UserDto userDto = new UserDto();
    model.addAttribute("user", userDto);
    return "registration";
}

當控制器接收到請求 “/user/registration”,它將創建一個新的 UserDto 對象,該對象支持 註冊 表單,將其綁定並返回。

接下來,我們將研究控制器在註冊新帳户時執行的驗證:

  1. 所有必需字段已填寫(沒有空或空字段)。
  2. 電子郵件地址有效(格式正確)。
  3. 密碼確認字段與密碼字段匹配。
  4. 帳户尚不存在。

對於簡單的檢查,我們將使用 DTO 對象上的內置 Bean 驗證註解,例如 等。

然後,為了觸發驗證過程,我們將使用 標註對象在控制器層。

public ModelAndView registerUserAccount(@ModelAttribute("user") @Valid UserDto userDto,
  HttpServletRequest request, Errors errors) {
    //...
}

然後我們將驗證電子郵件地址並確保其格式正確。為此,我們將構建一個 ,以及一個 我們將其稱為

重要的是要注意,我們正在自己構建自定義註解,而不是使用 Hibernate 的 ,因為 Hibernate 認為舊的內部地址格式 是有效的(參見 Stackoverflow 文章),這並不好。

因此,以下是電子郵件驗證註解和自定義驗證器:

@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 {};
}

請注意,我們已將註解定義為 級別,因為這是它在概念上適用的位置。

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();
    }
}

然後我們將 在我們的 實現上:

@ValidEmail
@NotNull
@NotEmpty
private String email;

我們還需要一個自定義註解和驗證器,以確保 字段匹配:

@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 {};
}

請注意, 註解指示這是一個 級別的註解。這是因為我們需要整個 對象來執行驗證。

將調用此註解的自定義驗證器如下所示:

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());
    }
}

然後, 註解應應用於我們的 對象:

@PasswordMatches
public class UserDto {
    //...
}

所有自定義驗證都將與所有標準註解一起評估,在整個驗證過程運行期間。

第四個檢查將是驗證 帳户是否已經在數據庫中存在。

此操作在表單驗證後執行,並且藉助 對象。

@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
}

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
    
    @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());
        }

        // the rest of the registration operation
    }
    private boolean emailExists(String email) {
        return userRepository.findByEmail(email) != null;
    }
}

UserService 依賴 類來檢查給定電子郵件地址是否已存在於數據庫中的。

類的實際實現在持久層中並不相關,但有一種快速的方法是使用 Spring Data 生成存儲庫層。

6. 保持數據持久化與流程完成接下來,我們將實現控制層中的註冊邏輯:

示例 6.1. 控制器中的 RegisterAccount 方法

@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", "一個用户名/郵箱已經存在。");
        return mav;
    }

    return new ModelAndView("successRegister", "user", userDto);
}

代碼中需要注意的事項:

  1. 控制器返回 ModelAndView 對象,這是方便的類,用於將模型數據(user)與視圖關聯。
  2. 如果驗證時間設置了任何錯誤,控制器將重定向到註冊表單。

7. UserService 中的 UserService – 註冊操作

最後,我們將完成 UserService 中註冊操作的實現:

示例 7.1. IUserService 接口

public interface IUserService {
    User registerNewUserAccount(UserDto userDto);
}

示例 7.2. UserService

@Service
@Transactional
public class UserService implements IUserService {
    @Autowired
    private UserRepository repository;
    
    @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. 加載用户詳細信息用於安全登錄

在之前的文章中,登錄使用了硬編碼憑據。我們將通過使用新註冊的用户信息和憑據來改變這一點。 此外,我們還將實現一個自定義UserDetailsService,以從持久層檢查登錄憑據。

8.1. 自定義UserDetailsService

我們將從自定義的用户詳細信息服務實現開始:

@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(), 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. 啓用新的身份驗證提供者

為了在 Spring Security 配置中啓用新的用户服務,我們只需要在UserDetailsService內部添加對用户詳細信息服務的引用,並添加UserDetailsService bean:

示例 8.2. 身份管理器和UserDetailsService

<authentication-manager>
    <authentication-provider user-service-ref="userDetailsService" />
</authentication-manager>
 
<beans:bean id="userDetailsService" class="com.baeldung.security.MyUserDetailsService" />

另一種選擇是使用 Java 配置:

@Autowired
private MyUserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService);
}

9. 結論

在本文中,我們演示了使用 Spring Security 和 Spring MVC 實現的完整且接近 生產級的註冊流程。接下來,我們將討論通過驗證新用户的電子郵件來激活新註冊賬户的過程。

下一條 »
註冊 – 通過電子郵件激活新賬户
« 上一篇
Spring Security 註冊系列
user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.