hongjli
2025-04-15 1acf9a48021d0af1d81fdf3ed8fcf8dffd020f6b
登录,注册,获取用户信息---接口
已修改3个文件
已添加16个文件
900 ■■■■■ 文件已修改
pom.xml 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/WeiwojcApplication.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/config/MybatisPlusConfig.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/config/SecurityConfig.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/controller/UserController.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/exception/GlobalExceptionHandler.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/mapper/UserMapper.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/model/common/Result.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/model/dto/UserLoginDTO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/model/dto/UserRegisterDTO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/model/entity/User.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/security/JwtAuthenticationFilter.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/security/JwtUserDetails.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/service/UserService.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/service/impl/UserServiceImpl.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/utils/JwtUtils.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/utils/PasswordUtils.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/weiwojc/utils/TokenBlacklistManager.java 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -7,7 +7,7 @@
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.4</version>
        <version>3.2.3</version>
        <relativePath/>
    </parent>
@@ -22,20 +22,76 @@
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <mybatis-plus.version>3.5.5</mybatis-plus.version>
        <jjwt.version>0.11.5</jjwt.version>
    </properties>
    <dependencies>
        <!-- Spring Boot Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <!-- MyBatis Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis-spring</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <!-- MySQL Driver -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- JWT -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <!-- Commons -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!-- Development Tools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
@@ -48,6 +104,18 @@
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
src/main/java/com/weiwojc/WeiwojcApplication.java
@@ -2,8 +2,10 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement
public class WeiwojcApplication {
    public static void main(String[] args) {
src/main/java/com/weiwojc/config/MybatisPlusConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.weiwojc.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // æ·»åŠ åˆ†é¡µæ’ä»¶
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}
src/main/java/com/weiwojc/config/SecurityConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.weiwojc.config;
import com.weiwojc.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final JwtAuthenticationFilter jwtAuthenticationFilter;
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement(session -> session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/users/register", "/api/users/login").permitAll()
                .anyRequest().permitAll())  // æš‚时允许所有请求,后续可以根据需要调整
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
src/main/java/com/weiwojc/controller/UserController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package com.weiwojc.controller;
import com.weiwojc.model.common.Result;
import com.weiwojc.model.dto.UserLoginDTO;
import com.weiwojc.model.dto.UserRegisterDTO;
import com.weiwojc.model.entity.User;
import com.weiwojc.service.UserService;
import com.weiwojc.utils.JwtUtils;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;
    private final JwtUtils jwtUtils;
    @PostMapping("/register")
    public Result<User> register(@Valid @RequestBody UserRegisterDTO registerDTO) {
        User user = userService.register(registerDTO);
        return Result.success("注册成功", user);
    }
    @PostMapping("/login")
    public Result<String> login(@Valid @RequestBody UserLoginDTO loginDTO) {
        String token = userService.login(loginDTO);
        return Result.success("登录成功", token);
    }
    @GetMapping("/info")
    public Result<User> getUserInfo(HttpServletRequest request) {
        String token = request.getHeader("token");
        // éªŒè¯token是否存在
        if (token == null || token.isEmpty()) {
            return Result.unauthorized("未登录或token无效");
        }
        Long userId = jwtUtils.getUserIdFromToken(token);
        if (userId == null) {
            return Result.unauthorized("token无效或已过期");
        }
        User user = userService.getUserInfo(userId);
        if (user == null) {
            return Result.error("用户不存在");
        }
        return Result.success(user);
    }
}
src/main/java/com/weiwojc/exception/GlobalExceptionHandler.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package com.weiwojc.exception;
import com.weiwojc.model.common.Result;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(RuntimeException.class)
    public Result<?> handleRuntimeException(RuntimeException e) {
        return Result.badRequest(e.getMessage());
    }
    @ExceptionHandler(BadCredentialsException.class)
    public Result<?> handleBadCredentialsException(BadCredentialsException e) {
        return Result.unauthorized(e.getMessage());
    }
    @ExceptionHandler(LockedException.class)
    public Result<?> handleLockedException(LockedException e) {
        return Result.forbidden(e.getMessage());
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidationException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        List<FieldError> fieldErrors = bindingResult.getFieldErrors();
        String errorMessage = fieldErrors.stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining(", "));
        return Result.badRequest(errorMessage);
    }
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        return Result.error("服务器内部错误:" + e.getMessage());
    }
}
src/main/java/com/weiwojc/mapper/UserMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.weiwojc.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiwojc.model.entity.User;
import org.apache.ibatis.annotations.*;
@Mapper
public interface UserMapper extends BaseMapper<User> {
    @Select("SELECT * FROM user WHERE username = #{username} AND is_deleted = 0")
    User findByUsername(String username);
    @Select("SELECT * FROM user WHERE user_id = #{userId} AND is_deleted = 0")
    User findById(Long userId);
    @Update("UPDATE user SET last_login = #{lastLogin} WHERE user_id = #{userId}")
    int updateLastLogin(@Param("userId") Long userId, @Param("lastLogin") java.time.LocalDateTime lastLogin);
    @Update("UPDATE user SET login_attempts = #{attempts}, locked_until = #{lockedUntil} WHERE user_id = #{userId}")
    int updateLoginAttempts(@Param("userId") Long userId,
                          @Param("attempts") Integer attempts,
                          @Param("lockedUntil") java.time.LocalDateTime lockedUntil);
    @Update("UPDATE user SET login_attempts = 0, locked_until = null WHERE username = #{username}")
    int resetLoginAttempts(String username);
}
src/main/java/com/weiwojc/model/common/Result.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,48 @@
package com.weiwojc.model.common;
import lombok.Data;
@Data
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    private Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null);
    }
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }
    public static <T> Result<T> success(String message, T data) {
        return new Result<>(200, message, data);
    }
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }
    public static <T> Result<T> badRequest(String message) {
        return new Result<>(400, message, null);
    }
    public static <T> Result<T> forbidden(String message) {
        return new Result<>(403, message, null);
    }
    public static <T> Result<T> unauthorized(String message) {
        return new Result<>(401, message, null);
    }
}
src/main/java/com/weiwojc/model/dto/UserLoginDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,13 @@
package com.weiwojc.model.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class UserLoginDTO {
    @NotBlank(message = "账号名不能为空")
    private String accountName;
    @NotBlank(message = "密码不能为空")
    private String password;
}
src/main/java/com/weiwojc/model/dto/UserRegisterDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.weiwojc.model.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Data
public class UserRegisterDTO {
    @NotBlank(message = "用户名不能为空")
    private String nickname;
    @NotBlank(message = "账号名不能为空")
    @Pattern(regexp = "^[a-zA-Z0-9_]{4,16}$", message = "账号名必须是4-16位字母、数字或下划线")
    private String accountName;
    @NotBlank(message = "密码不能为空")
    @Pattern(regexp = "^[a-zA-Z0-9_]{6,16}$", message = "密码必须是6-16位字母、数字或下划线")
    private String password;
}
src/main/java/com/weiwojc/model/entity/User.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package com.weiwojc.model.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("user")
public class User {
    @TableId(value = "user_id", type = IdType.AUTO)
    private Long userId;
    private String uuid;
    private String username;
    private String passwordHash;
    private String passwordSalt;
    private Boolean mfaEnabled;
    private String mfaSecret;
    private String email;
    private Boolean emailVerified;
    private String phone;
    private Boolean phoneVerified;
    private String realName;
    private String nickname;
    private Integer gender;
    private LocalDate birthdate;
    private String avatarUrl;
    private Integer status;
    @TableLogic
    private Boolean isDeleted;
    private LocalDateTime registeredAt;
    private LocalDateTime lastLogin;
    private LocalDateTime updatedAt;
    private String passwordResetToken;
    private LocalDateTime resetTokenExpire;
    private String emailVerifyToken;
    private LocalDateTime verifyTokenExpire;
    private Integer loginAttempts;
    private LocalDateTime lockedUntil;
    private String oauthProvider;
    private String oauthUid;
    private String oauthAccessToken;
    private String oauthRefreshToken;
    private String countryCode;
    private String timeZone;
    private String preferredLanguage;
    private String metadata;
}
src/main/java/com/weiwojc/security/JwtAuthenticationFilter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,57 @@
package com.weiwojc.security;
import com.weiwojc.utils.JwtUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtUtils jwtUtils;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String token = request.getHeader("token");
        if (token == null || token.isEmpty()) {
            filterChain.doFilter(request, response);
            return;
        }
        if (!jwtUtils.validateToken(token)) {
            filterChain.doFilter(request, response);
            return;
        }
        String username = jwtUtils.getUsernameFromToken(token);
        Long userId = jwtUtils.getUserIdFromToken(token);
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = new JwtUserDetails(userId, username);
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    userDetails, null, new ArrayList<>());
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        filterChain.doFilter(request, response);
    }
}
src/main/java/com/weiwojc/security/JwtUserDetails.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,54 @@
package com.weiwojc.security;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
@Getter
public class JwtUserDetails implements UserDetails {
    private final Long userId;
    private final String username;
    public JwtUserDetails(Long userId, String username) {
        this.userId = userId;
        this.username = username;
    }
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }
    @Override
    public String getPassword() {
        return null;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}
src/main/java/com/weiwojc/service/UserService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.weiwojc.service;
import com.weiwojc.model.dto.UserLoginDTO;
import com.weiwojc.model.dto.UserRegisterDTO;
import com.weiwojc.model.entity.User;
public interface UserService {
    /**
     * ç”¨æˆ·æ³¨å†Œ
     */
    User register(UserRegisterDTO registerDTO);
    /**
     * ç”¨æˆ·ç™»å½•
     */
    String login(UserLoginDTO loginDTO);
    /**
     * èŽ·å–ç”¨æˆ·ä¿¡æ¯
     */
    User getUserInfo(Long userId);
    /**
     * æ›´æ–°æœ€åŽç™»å½•æ—¶é—´
     */
    void updateLastLogin(Long userId);
    /**
     * æ£€æŸ¥ç”¨æˆ·æ˜¯å¦è¢«é”å®š
     */
    boolean isUserLocked(Long userId);
    /**
     * å¢žåŠ ç™»å½•å¤±è´¥æ¬¡æ•°
     */
    void incrementLoginAttempts(String username);
    /**
     * é‡ç½®ç™»å½•失败次数
     */
    void resetLoginAttempts(String username);
}
src/main/java/com/weiwojc/service/impl/UserServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
package com.weiwojc.service.impl;
import com.weiwojc.mapper.UserMapper;
import com.weiwojc.model.dto.UserLoginDTO;
import com.weiwojc.model.dto.UserRegisterDTO;
import com.weiwojc.model.entity.User;
import com.weiwojc.service.UserService;
import com.weiwojc.utils.JwtUtils;
import com.weiwojc.utils.PasswordUtils;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
    private final UserMapper userMapper;
    private final JwtUtils jwtUtils;
    @Override
    @Transactional
    public User register(UserRegisterDTO registerDTO) {
        // æ£€æŸ¥è´¦å·åæ˜¯å¦å·²å­˜åœ¨
        User existingUser = userMapper.findByUsername(registerDTO.getAccountName());
        if (existingUser != null) {
            throw new RuntimeException("账号名已存在");
        }
        // åˆ›å»ºæ–°ç”¨æˆ·
        User user = new User();
        user.setUuid(UUID.randomUUID().toString());
        user.setUsername(registerDTO.getAccountName());
        user.setNickname(registerDTO.getNickname());
        // ç”ŸæˆåŠ å¯†å¯†ç 
        String salt = PasswordUtils.generateSalt();
        String hashedPassword = PasswordUtils.hashPassword(registerDTO.getPassword(), salt);
        user.setPasswordHash(hashedPassword);
        user.setPasswordSalt(salt);  // ä¿å­˜ç›å€¼
        // è®¾ç½®å…¶ä»–字段
        user.setStatus(1); // æ­£å¸¸çŠ¶æ€
        user.setRegisteredAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        // ä¿å­˜ç”¨æˆ·
        userMapper.insert(user);
        return user;
    }
    @Override
    public String login(UserLoginDTO loginDTO) {
        User user = userMapper.findByUsername(loginDTO.getAccountName());
        if (user == null) {
            throw new BadCredentialsException("账号名或密码错误");
        }
        // æ£€æŸ¥è´¦æˆ·çŠ¶æ€
        if (user.getStatus() == 0) {
            throw new LockedException("账户已被禁用");
        }
        // æ£€æŸ¥æ˜¯å¦è¢«é”å®š
        if (isUserLocked(user.getUserId())) {
            throw new LockedException("账户已被锁定,请稍后再试");
        }
        // éªŒè¯å¯†ç 
        if (!PasswordUtils.verifyPassword(loginDTO.getPassword(), user.getPasswordHash())) {
            // å¢žåŠ ç™»å½•å¤±è´¥æ¬¡æ•°
            incrementLoginAttempts(user.getUsername());
            throw new BadCredentialsException("账号名或密码错误");
        }
        // é‡ç½®ç™»å½•失败次数
        resetLoginAttempts(user.getUsername());
        // æ›´æ–°æœ€åŽç™»å½•æ—¶é—´
        updateLastLogin(user.getUserId());
        // ç”ŸæˆJWT令牌
        return jwtUtils.generateToken(user);
    }
    @Override
    public User getUserInfo(Long userId) {
        return userMapper.findById(userId);
    }
    @Override
    public void updateLastLogin(Long userId) {
        userMapper.updateLastLogin(userId, LocalDateTime.now());
    }
    @Override
    public boolean isUserLocked(Long userId) {
        User user = userMapper.findById(userId);
        if (user == null) {
            return false;
        }
        // æ£€æŸ¥è´¦æˆ·é”å®šçŠ¶æ€
        if (user.getLockedUntil() != null &&
            LocalDateTime.now().isBefore(user.getLockedUntil())) {
            return true;
        }
        return false;
    }
    @Override
    public void incrementLoginAttempts(String username) {
        User user = userMapper.findByUsername(username);
        if (user != null) {
            int attempts = user.getLoginAttempts() == null ? 0 : user.getLoginAttempts();
            attempts++;
            LocalDateTime lockedUntil = null;
            // å¦‚果失败次数达到5次,锁定30分钟
            if (attempts >= 5) {
                lockedUntil = LocalDateTime.now().plusMinutes(30);
            }
            userMapper.updateLoginAttempts(user.getUserId(), attempts, lockedUntil);
        }
    }
    @Override
    public void resetLoginAttempts(String username) {
        userMapper.resetLoginAttempts(username);
    }
}
src/main/java/com/weiwojc/utils/JwtUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,109 @@
package com.weiwojc.utils;
import com.weiwojc.model.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class JwtUtils {
    private static final long SERVER_START_TIME = System.currentTimeMillis();
    @Value("${jwt.secret}")
    private String secret;
    @Value("${jwt.expiration}")
    private Long expiration;
    private final TokenBlacklistManager tokenBlacklistManager;
    private SecretKey getSigningKey() {
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("userId", user.getUserId());
        claims.put("username", user.getUsername());
        claims.put("instanceId", tokenBlacklistManager.getInstanceId());
        claims.put("serverStartTime", SERVER_START_TIME);
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(user.getUsername())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .signWith(getSigningKey())
                .compact();
    }
    public Claims getClaimsFromToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    public String getUsernameFromToken(String token) {
        return getClaimsFromToken(token).getSubject();
    }
    public Long getUserIdFromToken(String token) {
        return getClaimsFromToken(token).get("userId", Long.class);
    }
    public String getInstanceIdFromToken(String token) {
        return getClaimsFromToken(token).get("instanceId", String.class);
    }
    public Date getExpirationDateFromToken(String token) {
        return getClaimsFromToken(token).getExpiration();
    }
    public boolean isTokenExpired(String token) {
        Date expiration = getExpirationDateFromToken(token);
        return expiration.before(new Date());
    }
    public boolean validateToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            if (isTokenExpired(token)) {
                return false;
            }
            String tokenInstanceId = claims.get("instanceId", String.class);
            String currentInstanceId = tokenBlacklistManager.getInstanceId();
            if (!currentInstanceId.equals(tokenInstanceId)) {
                return false;
            }
            Long tokenServerStartTime = claims.get("serverStartTime", Long.class);
            if (tokenServerStartTime == null || tokenServerStartTime != SERVER_START_TIME) {
                return false;
            }
            if (tokenBlacklistManager.isBlacklisted(token)) {
                return false;
            }
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
src/main/java/com/weiwojc/utils/PasswordUtils.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.weiwojc.utils;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.security.crypto.bcrypt.BCrypt;
public class PasswordUtils {
    /**
     * ç”Ÿæˆéšæœºç›å€¼
     */
    public static String generateSalt() {
        return BCrypt.gensalt();
    }
    /**
     * ä½¿ç”¨ç›å€¼åŠ å¯†å¯†ç 
     */
    public static String hashPassword(String password, String salt) {
        return BCrypt.hashpw(password, salt);
    }
    /**
     * éªŒè¯å¯†ç 
     */
    public static boolean verifyPassword(String password, String hashedPassword) {
        return BCrypt.checkpw(password, hashedPassword);
    }
    /**
     * ä»ŽBCrypt哈希值中提取盐值
     */
    public static String extractSaltFromHash(String hashedPassword) {
        // BCrypt哈希的前29个字符就是盐值
        return hashedPassword.substring(0, 29);
    }
}
src/main/java/com/weiwojc/utils/TokenBlacklistManager.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,35 @@
package com.weiwojc.utils;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class TokenBlacklistManager {
    private static final ConcurrentHashMap<String, Boolean> tokenBlacklist = new ConcurrentHashMap<>();
    private static String INSTANCE_ID = null;
    private static long START_TIME;
    @PostConstruct
    public void init() {
        // æ¯æ¬¡æœåŠ¡å¯åŠ¨ç”Ÿæˆæ–°çš„å®žä¾‹ID和启动时间
        INSTANCE_ID = java.util.UUID.randomUUID().toString();
        START_TIME = System.currentTimeMillis();
    }
    public void addToBlacklist(String token) {
        tokenBlacklist.put(token, true);
    }
    public boolean isBlacklisted(String token) {
        return tokenBlacklist.containsKey(token);
    }
    public String getInstanceId() {
        return INSTANCE_ID;
    }
    public long getStartTime() {
        return START_TIME;
    }
}
src/main/resources/application.yml
@@ -1,15 +1,36 @@
server:
  port: 8080
  servlet:
    context-path: /api
spring:
  application:
    name: weiwojc
  profiles:
    active: dev
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://121.43.139.99:13306/wwjc?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: k6R8NeFcBfec55rc
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 5
      maximum-pool-size: 15
      auto-commit: true
      idle-timeout: 30000
      pool-name: HikariCP
      max-lifetime: 1800000
      connection-timeout: 30000
      connection-test-query: SELECT 1
mybatis:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
  secret: your-256-bit-secret-key-here-must-be-longer-than-256-bits
  expiration: 1800  # 30分钟过期
logging:
  level:
    root: INFO
    com.weiwojc: DEBUG
    com.weiwojc: DEBUG
    org.mybatis: DEBUG