前言
登录注册是一个网站最基本的功能,但它其实可以涉及到比较多方面,如用户注册时的密码校验,账户邮件激活,或者用户登录时的权限认证等。这次我们就来逐步实现一个登录注册功能。具体会用到 Spring Security来管理应用的认证授权,对象映射框架 JPA,同时为了方便演示,使用了基于内存的 H2 数据库。
首先来实现一个基本的注册功能。
项目架构
项目结构图如下:
![]()
pom 依赖如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> </parent> <groupId>top.yekongle</groupId> <artifactId>springboot-registraion-sample</artifactId> <version>0.0.1-SNAPSHOT</version> <name>springboot-registraion-sample</name> <description>Registraion project for Spring Boot</description>
<properties> <java.version>1.8</java.version> <passay.version>1.5.0</passay.version> <guava.version>29.0-jre</guava.version> </properties>
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.passay</groupId> <artifactId>passay</artifactId> <version>${passay.version}</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>${guava.version}</version> </dependency>
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
</project>
|
代码编写
项目使用 Thymeleaf 作为模板引擎,全局配置文件如下:
application.properties
1 2 3 4 5 6 7 8 9 10
|
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html spring.thymeleaf.mode=HTML
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.cache=false
|
UserDTO.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package top.yekongle.registration.dto;
import javax.validation.constraints.Email; import javax.validation.constraints.NotEmpty;
import lombok.Data; import top.yekongle.registration.validation.PasswordMatches; import top.yekongle.registration.validation.ValidPassword;
@Data @PasswordMatches public class UserDTO { @Email @NotEmpty private String email; @NotEmpty @ValidPassword private String password;
private String matchingPassword; }
|
自定义 PasswordMatches 注解
PasswordMatches.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package top.yekongle.registration.validation;
import java.lang.annotation.Documented;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME;
import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.Retention;
@Documented @Retention(RUNTIME) @Constraint(validatedBy = PasswordMatchesValidator.class) @Target({TYPE, ANNOTATION_TYPE}) public @interface PasswordMatches { String message() default "密码不一致"; Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { }; }
|
@PasswordMatches 绑定的校验类
PasswordMatchesValidator.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package top.yekongle.registration.validation;
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext;
import top.yekongle.registration.dto.UserDTO;
public class PasswordMatchesValidator implements ConstraintValidator<PasswordMatches, Object> {
@Override public boolean isValid(Object value, ConstraintValidatorContext context) { UserDTO user = (UserDTO) value; return user.getPassword().equals(user.getMatchingPassword()); } }
|
自定义 ValidPassword 注解
ValidPassword.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| package top.yekongle.registration.validation;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target;
import javax.validation.Constraint; import javax.validation.Payload;
@Documented @Retention(RUNTIME) @Constraint(validatedBy = PasswordConstraintValidator.class) @Target({TYPE, FIELD, ANNOTATION_TYPE}) public @interface ValidPassword { String message() default "密码无效"; Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { }; }
|
@ValidPassword 绑定的校验类, 根据自定义规则校验密码,使用了 passay 密码库, 并根据规则码自定义错误消息
PasswordConstraintValidator.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| package top.yekongle.registration.validation;
import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.Arrays; import java.util.Properties;
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext;
import org.passay.CharacterRule; import org.passay.EnglishCharacterData; import org.passay.LengthRule; import org.passay.MessageResolver; import org.passay.PasswordData; import org.passay.PasswordValidator; import org.passay.PropertiesMessageResolver; import org.passay.RuleResult; import org.passay.WhitespaceRule;
import com.google.common.base.Joiner;
import lombok.extern.slf4j.Slf4j;
@Slf4j public class PasswordConstraintValidator implements ConstraintValidator<ValidPassword, String> {
@Override public boolean isValid(String password, ConstraintValidatorContext context) {
URL resource = this.getClass().getClassLoader().getResource("passay-messages.properties"); Properties props = new Properties(); try { InputStreamReader isr = new InputStreamReader(new FileInputStream(resource.getPath()), "UTF-8"); props.load(isr); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } MessageResolver resolver = new PropertiesMessageResolver(props); PasswordValidator validator = new PasswordValidator(resolver, Arrays.asList( new LengthRule(6, 18), new WhitespaceRule(), new CharacterRule(EnglishCharacterData.UpperCase, 1), new CharacterRule(EnglishCharacterData.Digit, 1), new CharacterRule(EnglishCharacterData.Special, 1) )); RuleResult result = validator.validate(new PasswordData(password)); log.info("Result:" + validator.getMessages(result)); if(result.isValid()) { return true; } context.disableDefaultConstraintViolation(); context.buildConstraintViolationWithTemplate(Joiner.on(",").join(validator.getMessages(result))).addConstraintViolation(); return false; } }
|
在 Resources 下创建该 properties,用于自定义 passay 的错误消息, key: 错误码 value: 错误信息
passay-messages.properties
1 2 3 4 5 6
| TOO_SHORT=密码长度不能少于%1$s位 TOO_LONG=密码长度不能超过%2$s位 INSUFFICIENT_DIGIT=至少要有%1$s位数字 ILLEGAL_WHITESPACE=不能有空格 INSUFFICIENT_SPECIAL=至少要有%1$s个特殊字符 INSUFFICIENT_UPPERCASE=至少要有%1$s个大写字母
|
创建实体对象类
User.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package top.yekongle.registration.entity;
import java.util.List;
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Transient;
import lombok.Data; import lombok.NoArgsConstructor;
@Entity @Data @NoArgsConstructor public class User { @Id @GeneratedValue private Long id; private String email; private String password; @Transient private List<String> roles; }
|
用户数据操作接口
UserRepository.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package top.yekongle.registration.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import top.yekongle.registration.entity.User;
public interface UserRepository extends JpaRepository<User, Long> { User findByEmail(String email); }
|
业务处理接口
UserService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package top.yekongle.registration.service;
import top.yekongle.registration.dto.UserDTO; import top.yekongle.registration.entity.User; import top.yekongle.registration.exception.UserAlreadyExistException;
public interface UserService { User registerNewUserAccount(UserDTO userDTO) throws UserAlreadyExistException; }
|
用户业务处理实现类
UserServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| package top.yekongle.registration.service.impl;
import java.util.Arrays;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service;
import top.yekongle.registration.dto.UserDTO; import top.yekongle.registration.entity.User; import top.yekongle.registration.exception.UserAlreadyExistException; import top.yekongle.registration.repository.UserRepository; import top.yekongle.registration.service.UserService;
@Service public class UserServiceImpl implements UserService {
@Autowired private UserRepository userRepository; @Transactional @Override public User registerNewUserAccount(UserDTO userDTO) throws UserAlreadyExistException { if (emailExists(userDTO.getEmail())) { throw new UserAlreadyExistException("该邮箱已被注册:" + userDTO.getEmail()); } User user = new User(); user.setEmail(userDTO.getEmail()); user.setRoles(Arrays.asList("ROLE_USER")); return userRepository.save(user); }
private boolean emailExists(String email) { return userRepository.findByEmail(email) != null; } }
|
注册时如果该用户已经注册则抛出一个自定义异常:
UserAlreadyExistException.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package top.yekongle.registration.exception;
public class UserAlreadyExistException extends RuntimeException {
private static final long serialVersionUID = 1L; public UserAlreadyExistException(String message) { super(message); } }
|
注册请求处理
RegistrationController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| package top.yekongle.registration.controller;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody;
import lombok.extern.slf4j.Slf4j; import top.yekongle.registration.dto.UserDTO; import top.yekongle.registration.entity.User; import top.yekongle.registration.service.UserService; import top.yekongle.registration.util.GenericResponse;
@Slf4j @RequestMapping("/user") @Controller public class RegistrationController { @Autowired UserService userService;
@GetMapping("/registration") public String registration(Model model) { return "registration"; }
@PostMapping("/registration") @ResponseBody public GenericResponse registerUserAccount(@Valid UserDTO userDTO) { User registered = userService.registerNewUserAccount(userDTO);
return new GenericResponse("success"); } }
|
自定义结果返回
GenericResponse.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package top.yekongle.registration.util;
import java.util.List; import java.util.stream.Collectors;
import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError;
public class GenericResponse { private String message; private String error;
public GenericResponse(final String message) { super(); this.message = message; }
public GenericResponse(final String message, final String error) { super(); this.message = message; this.error = error; }
public GenericResponse(List<ObjectError> allErrors, String error) { this.error = error; String temp = allErrors.stream().map(e -> { if (e instanceof FieldError) { return "{\"field\":\"" + ((FieldError) e).getField() + "\",\"defaultMessage\":\"" + e.getDefaultMessage() + "\"}"; } else { return "{\"object\":\"" + e.getObjectName() + "\",\"defaultMessage\":\"" + e.getDefaultMessage() + "\"}"; } }).collect(Collectors.joining(",")); this.message = "[" + temp + "]"; }
public String getMessage() { return message; }
public void setMessage(final String message) { this.message = message; }
public String getError() { return error; }
public void setError(final String error) { this.error = error; } }
|
自定义一个全局异常处理:
RestExceptionHandler.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| package top.yekongle.registration.exception;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.MessageSource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import top.yekongle.registration.util.GenericResponse;
@ControllerAdvice public class RestExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity<Object> handleBindException(final BindException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { logger.error("400 Status Code", ex); final BindingResult result = ex.getBindingResult(); final GenericResponse bodyOfResponse = new GenericResponse(result.getAllErrors(), "Invalid" + result.getObjectName()); return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } @Override protected ResponseEntity<Object> handleMethodArgumentNotValid(final MethodArgumentNotValidException ex, final HttpHeaders headers, final HttpStatus status, final WebRequest request) { logger.error("400 Status Code", ex); final BindingResult result = ex.getBindingResult(); final GenericResponse bodyOfResponse = new GenericResponse(result.getAllErrors(), "Invalid" + result.getObjectName()); return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.BAD_REQUEST, request); } @ExceptionHandler(UserAlreadyExistException.class) public ResponseEntity<Object> handleUserAlreadyExist(final UserAlreadyExistException ex, final WebRequest request) { logger.error("409 Status Code", ex); final GenericResponse bodyOfResponse = new GenericResponse(ex.getMessage(), "UserAlreadyExist"); return handleExceptionInternal(ex, bodyOfResponse, new HttpHeaders(), HttpStatus.CONFLICT, request); }
}
|
前端注册页面
registration.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| <html xmlns:th="http://www.thymeleaf.org"> <head> <meta content="text/html;charset=UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/>
<style type="text/css"> .middle { float: none; display: inline-block; vertical-align: middle; } </style> </head> <body>
<div class="container"> <h2>注册</h2> <br/> <form action="/" method="POST"> <div class="row"> <div class="form-group col-md-6 vertical-middle-sm"> <label for="email">邮箱</label> <input type="email" class="form-control" id="email" name="email" aria-describedby="emailHelp"> <small id="emailHelp" class="form-text text-muted">我们绝不会与其他任何人共享您的电子邮件</small> </div> <span id="emailError" class="alert alert-danger middle col-xs-4" style="display:none;margin-top:10px;"></span> </div> <div class="row"> <div class="form-group col-md-6"> <label for="password">密码</label> <input type="password" class="form-control" id="password" name="password"> </div> <span id="passwordError" class="alert alert-danger middle col-xs-4" style="display:none;margin-top:10px;"></span> </div> <div class="row"> <div class="form-group col-md-6"> <label for="matchingPassword">确认密码</label> <input type="password" class="form-control" id="matchingPassword" name="matchingPassword"> </div> <span id="globalError" class="alert alert-danger middle col-xs-4" style="display:none;margin-top:10px;"></span> </div> <button type="submit" class="btn btn-primary">提交</button> </form> </div>
<script th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script> <script th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script> <script th:inline="javascript"> var serverContext = [[@{/}]];
$(document).ready(function () { $('form').submit(function(event) { register(event); });
function register(event) { event.preventDefault(); $(".alert").html("").hide(); var formData= $('form').serialize(); $.post(serverContext + "user/registration", formData ,function(data){ if(data.message == "success"){ window.location.href = serverContext + "successRegister.html"; } }) .fail(function(data) { if(data.responseJSON.error == "UserAlreadyExist"){ $("#emailError").show().html(data.responseJSON.message); } else{ var errors = $.parseJSON(data.responseJSON.message); $.each( errors, function( index,item ){ if (item.field) { $("#"+item.field+"Error").show().append(item.defaultMessage+"<br/>"); } else { $("#globalError").show().append(item.defaultMessage+"<br/>"); } }); } }); } }); </script>
</body> </html>
|
注册成功展示页面
successRegister.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <html xmlns:th="http://www.thymeleaf.org"> <head> <meta content="text/html;charset=UTF-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet"/> </head> <body>
<div class="container"> <div class="alert alert-success" role="alert"> <p>注册成功!</p> </div> <a th:href="@{/login}" >立即登录</a> </div> <script th:src="@{/js/jquery-3.5.1.min.js}" type="text/javascript"></script> <script th:src="@{/js/bootstrap.min.js}" type="text/javascript"></script>
</body> </html>
|
运行演示
启动项目
访问 http://localhost:8080/user/registration
![]()
提交空白信息
![]()
输入密码 123456
![]()
密码框输入: A123456!
确认密码框输入: 123456
![]()
正确输入账号密码提交
![]()
项目已上传至 Github: https://github.com/yekongle/springboot-code-samples/tree/master/springboot-registraion-sample , 希望对小伙伴们有帮助哦。
参考链接: