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 org.mybatis: DEBUG