一、功能概述
个人博客系统的注册登录功能包括:
- 用户注册:新用户可以通过提供用户名、密码、邮箱等信息创建账号
- 用户登录:已注册用户可以通过用户名和密码进行身份验证,获取JWT令牌
- 身份验证:使用JWT令牌访问需要认证的API
二、技术栈
- 后端框架:Spring Boot 3.2.5
- 安全框架:Spring Security
- 数据库:MySQL 8.0
- 认证方式:JWT (JSON Web Token)
- API测试工具:Postman
三、实现步骤
1. 数据库设计
用户表(users)设计:
CREATE TABLE users (id BIGINT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL UNIQUE,password VARCHAR(100) NOT NULL,email VARCHAR(100) NOT NULL UNIQUE,nickname VARCHAR(50),role VARCHAR(20) NOT NULL DEFAULT 'USER',status INT NOT NULL DEFAULT 1,created_at DATETIME NOT NULL,updated_at DATETIME NOT NULL
);
2. 实体类设计
User实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "users")
public class User {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;@Column(nullable = false, unique = true, length = 50)private String username;@Column(nullable = false, length = 100)private String password;@Column(nullable = false, unique = true, length = 100)private String email;@Column(length = 50)private String nickname;@Column(nullable = false, length = 20)private String role = "USER"; // 默认角色@Column(nullable = false)private Integer status = 1; // 默认状态(1为激活)@Column(name = "created_at", nullable = false, updatable = false)private LocalDateTime createdAt;@Column(name = "updated_at", nullable = false)private LocalDateTime updatedAt;@PrePersistprotected void onCreate() {createdAt = LocalDateTime.now();updatedAt = LocalDateTime.now();}@PreUpdateprotected void onUpdate() {updatedAt = LocalDateTime.now();}
}
3. DTO设计
注册DTO:
public class RegisterUserDto {@NotBlank(message = "用户名不能为空")@Size(min = 4, max = 50, message = "用户名长度必须在4-50个字符之间")private String username;@NotBlank(message = "密码不能为空")@Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间")private String password;@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")private String email;private String nickname;// getters and setters
}
登录DTO:
public class LoginUserDto {@NotBlank(message = "用户名不能为空")private String username;@NotBlank(message = "密码不能为空")private String password;// getters and setters
}
4. 数据仓库接口
@Repository
public interface UserRepository extends JpaRepository<User, Long> {Optional<User> findByUsername(String username);Optional<User> findByEmail(String email);boolean existsByUsername(String username);boolean existsByEmail(String email);
}
5. 服务层实现
AuthService接口:
public interface AuthService {User registerUser(RegisterUserDto registerUserDto);Map<String, Object> loginUser(LoginUserDto loginUserDto) throws AuthenticationException;
}
AuthServiceImpl实现类:
@Service
public class AuthServiceImpl implements AuthService {private final UserRepository userRepository;private final PasswordEncoder passwordEncoder;private final AuthenticationManager authenticationManager;private final JwtUtil jwtUtil;@Autowiredpublic AuthServiceImpl(UserRepository userRepository,PasswordEncoder passwordEncoder,AuthenticationManager authenticationManager,JwtUtil jwtUtil) {this.userRepository = userRepository;this.passwordEncoder = passwordEncoder;this.authenticationManager = authenticationManager;this.jwtUtil = jwtUtil;}@Override@Transactionalpublic User registerUser(RegisterUserDto registerUserDto) {// 检查用户名是否已存在if (userRepository.existsByUsername(registerUserDto.getUsername())) {throw new UserAlreadyExistsException("用户名 " + registerUserDto.getUsername() + " 已被注册");}// 检查邮箱是否已存在if (registerUserDto.getEmail() != null && !registerUserDto.getEmail().isEmpty() && userRepository.existsByEmail(registerUserDto.getEmail())) {throw new UserAlreadyExistsException("邮箱 " + registerUserDto.getEmail() + " 已被注册");}// 创建新用户实体User newUser = new User();newUser.setUsername(registerUserDto.getUsername());// 加密密码newUser.setPassword(passwordEncoder.encode(registerUserDto.getPassword()));newUser.setEmail(registerUserDto.getEmail());newUser.setNickname(registerUserDto.getNickname());// 使用默认值(role="USER", status=1)// createdAt 和 updatedAt 由 @PrePersist 自动处理// 保存用户到数据库return userRepository.save(newUser);}@Overridepublic Map<String, Object> loginUser(LoginUserDto loginUserDto) throws AuthenticationException {// 创建认证令牌UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUserDto.getUsername(), loginUserDto.getPassword());// 进行认证Authentication authentication = authenticationManager.authenticate(authenticationToken);SecurityContextHolder.getContext().setAuthentication(authentication);// 获取用户详情UserDetails userDetails = (UserDetails) authentication.getPrincipal();// 生成JWT令牌String jwt = jwtUtil.generateToken(userDetails);// 获取用户IDUser user = userRepository.findByUsername(loginUserDto.getUsername()).orElseThrow(() -> new RuntimeException("用户不存在"));// 创建返回结果Map<String, Object> result = new HashMap<>();result.put("token", jwt);result.put("userId", user.getId());result.put("username", user.getUsername());result.put("expiresIn", 604800L); // 默认7天 = 604800秒return result;}
}
6. 控制器实现
@RestController
@RequestMapping("/auth")
public class AuthController {private final AuthService authService;@Autowiredpublic AuthController(AuthService authService) {this.authService = authService;}@PostMapping("/register")public ResponseEntity<?> registerUser(@Valid @RequestBody RegisterUserDto registerUserDto) {User registeredUser = authService.registerUser(registerUserDto);Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.CREATED.value());response.put("message", "注册成功");return ResponseEntity.status(HttpStatus.CREATED).body(response);}@PostMapping("/login")public ResponseEntity<?> loginUser(@Valid @RequestBody LoginUserDto loginUserDto) {try {Map<String, Object> loginResult = authService.loginUser(loginUserDto);Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.OK.value());response.put("message", "登录成功");Map<String, Object> data = new HashMap<>();data.put("token", loginResult.get("token"));data.put("userId", loginResult.get("userId"));data.put("username", loginResult.get("username"));data.put("expiresIn", loginResult.get("expiresIn"));response.put("data", data);return ResponseEntity.ok(response);} catch (BadCredentialsException e) {Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.UNAUTHORIZED.value());response.put("message", "用户名或密码错误");return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);} catch (Exception e) {Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());response.put("message", "服务器内部错误: " + e.getMessage());return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);}}@ExceptionHandler(MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public Map<String, Object> handleValidationExceptions(MethodArgumentNotValidException ex) {Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream().collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage));Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.BAD_REQUEST.value());response.put("message", "请求参数错误");response.put("errors", errors);return response;}@ExceptionHandler(UserAlreadyExistsException.class)@ResponseStatus(HttpStatus.BAD_REQUEST)public Map<String, Object> handleUserAlreadyExistsException(UserAlreadyExistsException ex) {Map<String, Object> response = new HashMap<>();response.put("code", HttpStatus.BAD_REQUEST.value());response.put("message", ex.getMessage());return response;}
}
7. 安全配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {@Autowiredprivate UserDetailsServiceImpl userDetailsService;@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {return authenticationConfiguration.getAuthenticationManager();}@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http.csrf(AbstractHttpConfigurer::disable).exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)).accessDeniedHandler((request, response, accessDeniedException) -> response.setStatus(HttpStatus.FORBIDDEN.value()))).authorizeHttpRequests(authz -> authz.requestMatchers("/auth/**").permitAll().anyRequest().authenticated()).sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));return http.build();}
}
8. JWT工具类
@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;public String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();return createToken(claims, userDetails.getUsername());}private String createToken(Map<String, Object> claims, String subject) {Date now = new Date();Date expiryDate = new Date(now.getTime() + expiration);return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(now).setExpiration(expiryDate).signWith(SignatureAlgorithm.HS512, secret).compact();}// 其他JWT验证方法...
}
四、使用Postman测试注册登录功能
1. 测试用户注册
-
创建POST请求:
- URL:
http://localhost:8080/auth/register
- 请求头:
Content-Type: application/json
- 请求体:
{"username": "testuser","password": "Password123","email": "testuser@example.com","nickname": "测试用户" }
- URL:
-
发送请求并验证响应:
- 成功响应(201 Created):
{"code": 201,"message": "注册成功" }
- 失败响应(400 Bad Request):
{"code": 400,"message": "用户名 testuser 已被注册" }
2. 测试用户登录
-
创建POST请求:
- URL:
http://localhost:8080/auth/login
- 请求头:
Content-Type: application/json
- 请求体:
{"username": "testuser","password": "Password123" }
- URL:
-
发送请求并验证响应:
- 成功响应(200 OK):
{"code": 200,"message": "登录成功","data": {"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","userId": 1,"username": "testuser","expiresIn": 604800} }
- 失败响应(401 Unauthorized):
{"code": 401,"message": "用户名或密码错误" }
3. 使用JWT令牌访问受保护的API
-
创建请求(例如获取用户信息):
- URL:
http://localhost:8080/users/1
- 请求头:
Authorization: Bearer {token}
(使用登录时获取的token)
- URL:
-
发送请求并验证响应
五、常见问题及解决方案
1. Java 9+中缺少javax.xml.bind问题
问题描述:在Java 9及以上版本中使用JJWT 0.9.1库时,可能会遇到以下错误:
java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter
原因:从Java 9开始,Java EE模块(包括javax.xml.bind包)被移除出了JDK核心。
解决方案:在pom.xml中添加JAXB API依赖:
<!-- 添加JAXB API依赖,解决Java 9+中缺少javax.xml.bind问题 -->
<dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version>
</dependency>
<dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-impl</artifactId><version>2.3.1</version>
</dependency>
<dependency><groupId>com.sun.xml.bind</groupId><artifactId>jaxb-core</artifactId><version>2.3.0.1</version>
</dependency>
2. API路径不匹配问题
问题描述:README文档中描述的API路径与实际代码中的路径不匹配。
原因:README中描述的基础路径是http://localhost:8080/api/v1
,但控制器中只配置了/auth
路径。
解决方案:
-
方案一:使用正确的URL:
http://localhost:8080/auth/register
和http://localhost:8080/auth/login
-
方案二:在application.properties中添加上下文路径配置:
server.servlet.context-path=/api/v1
这样就可以使用README中描述的URL:
http://localhost:8080/api/v1/auth/register
和http://localhost:8080/api/v1/auth/login
3. 数据库连接问题
问题描述:注册接口返回成功,但数据库中没有保存数据。
可能原因:
- 数据库名称配置错误
- 事务回滚(可能由未捕获的异常引起)
- 数据库连接问题
解决方案:
-
检查application.properties中的数据库配置是否正确:
spring.datasource.url=jdbc:mysql://localhost:3306/weblog?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=123456
-
确保数据库存在并且可以连接
-
检查日志中是否有事务回滚的错误信息
4. 请求验证失败
问题描述:注册或登录请求返回400错误,但没有明确的错误信息。
可能原因:请求体中缺少必填字段或格式不正确。
解决方案:
- 确保请求体中包含所有必填字段
- 确保字段格式正确(例如,邮箱格式、密码长度等)
- 检查控制台日志,查看详细的验证错误信息
六、最佳实践
-
密码安全:
- 始终使用BCrypt等安全的密码哈希算法
- 不要在响应中返回密码,即使是加密后的密码
- 设置密码复杂度要求(长度、特殊字符等)
-
JWT安全:
- 使用强密钥(至少256位)
- 设置合理的过期时间
- 考虑实现令牌刷新机制
- 在生产环境中使用HTTPS
-
异常处理:
- 为不同类型的异常提供明确的错误消息
- 不要在生产环境中暴露敏感的技术细节
- 使用统一的响应格式
-
日志记录:
- 记录关键操作(注册、登录、登出)
- 记录异常和错误
- 不要记录敏感信息(密码、令牌等)