个人博客系统后端 - 注册登录功能实现指南

发布于:2025-04-13 ⋅ 阅读:(97) ⋅ 点赞:(0)

一、功能概述

个人博客系统的注册登录功能包括:

  1. 用户注册:新用户可以通过提供用户名、密码、邮箱等信息创建账号
  2. 用户登录:已注册用户可以通过用户名和密码进行身份验证,获取JWT令牌
  3. 身份验证:使用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;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
        updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    protected 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;

    @Autowired
    public AuthServiceImpl(UserRepository userRepository,
                          PasswordEncoder passwordEncoder,
                          AuthenticationManager authenticationManager,
                          JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.authenticationManager = authenticationManager;
        this.jwtUtil = jwtUtil;
    }

    @Override
    @Transactional
    public 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);
    }

    @Override
    public 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);

        // 获取用户ID
        User 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;

    @Autowired
    public 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 {
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public 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. 测试用户注册

  1. 创建POST请求

    • URL: http://localhost:8080/auth/register
    • 请求头: Content-Type: application/json
    • 请求体:
    {
      "username": "testuser",
      "password": "Password123",
      "email": "testuser@example.com",
      "nickname": "测试用户"
    }
    
  2. 发送请求并验证响应

    • 成功响应(201 Created):
    {
      "code": 201,
      "message": "注册成功"
    }
    
    • 失败响应(400 Bad Request):
    {
      "code": 400,
      "message": "用户名 testuser 已被注册"
    }
    

2. 测试用户登录

  1. 创建POST请求

    • URL: http://localhost:8080/auth/login
    • 请求头: Content-Type: application/json
    • 请求体:
    {
      "username": "testuser",
      "password": "Password123"
    }
    
  2. 发送请求并验证响应

    • 成功响应(200 OK):
    {
      "code": 200,
      "message": "登录成功",
      "data": {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "userId": 1,
        "username": "testuser",
        "expiresIn": 604800
      }
    }
    
    • 失败响应(401 Unauthorized):
    {
      "code": 401,
      "message": "用户名或密码错误"
    }
    

3. 使用JWT令牌访问受保护的API

  1. 创建请求(例如获取用户信息):

    • URL: http://localhost:8080/users/1
    • 请求头: Authorization: Bearer {token}(使用登录时获取的token)
  2. 发送请求并验证响应

五、常见问题及解决方案

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路径。

解决方案

  1. 方案一:使用正确的URL:http://localhost:8080/auth/registerhttp://localhost:8080/auth/login

  2. 方案二:在application.properties中添加上下文路径配置:

    server.servlet.context-path=/api/v1
    

    这样就可以使用README中描述的URL:http://localhost:8080/api/v1/auth/registerhttp://localhost:8080/api/v1/auth/login

3. 数据库连接问题

问题描述:注册接口返回成功,但数据库中没有保存数据。

可能原因

  1. 数据库名称配置错误
  2. 事务回滚(可能由未捕获的异常引起)
  3. 数据库连接问题

解决方案

  1. 检查application.properties中的数据库配置是否正确:

    spring.datasource.url=jdbc:mysql://localhost:3306/weblog?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    spring.datasource.username=root
    spring.datasource.password=123456
    
  2. 确保数据库存在并且可以连接

  3. 检查日志中是否有事务回滚的错误信息

4. 请求验证失败

问题描述:注册或登录请求返回400错误,但没有明确的错误信息。

可能原因:请求体中缺少必填字段或格式不正确。

解决方案

  1. 确保请求体中包含所有必填字段
  2. 确保字段格式正确(例如,邮箱格式、密码长度等)
  3. 检查控制台日志,查看详细的验证错误信息

六、最佳实践

  1. 密码安全

    • 始终使用BCrypt等安全的密码哈希算法
    • 不要在响应中返回密码,即使是加密后的密码
    • 设置密码复杂度要求(长度、特殊字符等)
  2. JWT安全

    • 使用强密钥(至少256位)
    • 设置合理的过期时间
    • 考虑实现令牌刷新机制
    • 在生产环境中使用HTTPS
  3. 异常处理

    • 为不同类型的异常提供明确的错误消息
    • 不要在生产环境中暴露敏感的技术细节
    • 使用统一的响应格式
  4. 日志记录

    • 记录关键操作(注册、登录、登出)
    • 记录异常和错误
    • 不要记录敏感信息(密码、令牌等)

网站公告

今日签到

点亮在社区的每一天
去签到