From 1acf9a48021d0af1d81fdf3ed8fcf8dffd020f6b Mon Sep 17 00:00:00 2001
From: hongjli <3117313295@qq.com>
Date: 星期二, 15 四月 2025 14:20:51 +0800
Subject: [PATCH] 登录,注册,获取用户信息---接口
---
src/main/java/com/weiwojc/utils/PasswordUtils.java | 36 ++
src/main/java/com/weiwojc/WeiwojcApplication.java | 2
src/main/java/com/weiwojc/security/JwtAuthenticationFilter.java | 57 +++
src/main/java/com/weiwojc/service/impl/UserServiceImpl.java | 140 +++++++++
src/main/java/com/weiwojc/mapper/UserMapper.java | 26 +
src/main/java/com/weiwojc/model/common/Result.java | 48 +++
src/main/java/com/weiwojc/controller/UserController.java | 54 +++
src/main/java/com/weiwojc/service/UserService.java | 42 ++
pom.xml | 76 ++++
src/main/java/com/weiwojc/exception/GlobalExceptionHandler.java | 47 +++
src/main/java/com/weiwojc/model/dto/UserRegisterDTO.java | 19 +
src/main/java/com/weiwojc/security/JwtUserDetails.java | 54 +++
src/main/java/com/weiwojc/utils/TokenBlacklistManager.java | 35 ++
src/main/java/com/weiwojc/config/MybatisPlusConfig.java | 19 +
src/main/java/com/weiwojc/config/SecurityConfig.java | 41 ++
src/main/java/com/weiwojc/utils/JwtUtils.java | 109 +++++++
src/main/java/com/weiwojc/model/dto/UserLoginDTO.java | 13
src/main/java/com/weiwojc/model/entity/User.java | 51 +++
src/main/resources/application.yml | 31 +
19 files changed, 891 insertions(+), 9 deletions(-)
diff --git a/pom.xml b/pom.xml
index 750936e..9dd98db 100644
--- a/pom.xml
+++ b/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>
diff --git a/src/main/java/com/weiwojc/WeiwojcApplication.java b/src/main/java/com/weiwojc/WeiwojcApplication.java
index 242e37a..ea4bbd1 100644
--- a/src/main/java/com/weiwojc/WeiwojcApplication.java
+++ b/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) {
diff --git a/src/main/java/com/weiwojc/config/MybatisPlusConfig.java b/src/main/java/com/weiwojc/config/MybatisPlusConfig.java
new file mode 100644
index 0000000..aaaecd1
--- /dev/null
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/config/SecurityConfig.java b/src/main/java/com/weiwojc/config/SecurityConfig.java
new file mode 100644
index 0000000..025950f
--- /dev/null
+++ b/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();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/controller/UserController.java b/src/main/java/com/weiwojc/controller/UserController.java
new file mode 100644
index 0000000..40cb2fb
--- /dev/null
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/exception/GlobalExceptionHandler.java b/src/main/java/com/weiwojc/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..45e377d
--- /dev/null
+++ b/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());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/mapper/UserMapper.java b/src/main/java/com/weiwojc/mapper/UserMapper.java
new file mode 100644
index 0000000..6087e4d
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/model/common/Result.java b/src/main/java/com/weiwojc/model/common/Result.java
new file mode 100644
index 0000000..c641a53
--- /dev/null
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/model/dto/UserLoginDTO.java b/src/main/java/com/weiwojc/model/dto/UserLoginDTO.java
new file mode 100644
index 0000000..47e6a88
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/model/dto/UserRegisterDTO.java b/src/main/java/com/weiwojc/model/dto/UserRegisterDTO.java
new file mode 100644
index 0000000..9dfb088
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/model/entity/User.java b/src/main/java/com/weiwojc/model/entity/User.java
new file mode 100644
index 0000000..00411c2
--- /dev/null
+++ b/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;
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/security/JwtAuthenticationFilter.java b/src/main/java/com/weiwojc/security/JwtAuthenticationFilter.java
new file mode 100644
index 0000000..25783e8
--- /dev/null
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/security/JwtUserDetails.java b/src/main/java/com/weiwojc/security/JwtUserDetails.java
new file mode 100644
index 0000000..3514ba6
--- /dev/null
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/service/UserService.java b/src/main/java/com/weiwojc/service/UserService.java
new file mode 100644
index 0000000..7297732
--- /dev/null
+++ b/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);
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/service/impl/UserServiceImpl.java b/src/main/java/com/weiwojc/service/impl/UserServiceImpl.java
new file mode 100644
index 0000000..50455cb
--- /dev/null
+++ b/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); // 姝e父鐘舵��
+ 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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/utils/JwtUtils.java b/src/main/java/com/weiwojc/utils/JwtUtils.java
new file mode 100644
index 0000000..4d4b673
--- /dev/null
+++ b/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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/utils/PasswordUtils.java b/src/main/java/com/weiwojc/utils/PasswordUtils.java
new file mode 100644
index 0000000..9bc1300
--- /dev/null
+++ b/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);
+ }
+
+ /**
+ * 浠嶣Crypt鍝堝笇鍊间腑鎻愬彇鐩愬��
+ */
+ public static String extractSaltFromHash(String hashedPassword) {
+ // BCrypt鍝堝笇鐨勫墠29涓瓧绗﹀氨鏄洂鍊�
+ return hashedPassword.substring(0, 29);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/weiwojc/utils/TokenBlacklistManager.java b/src/main/java/com/weiwojc/utils/TokenBlacklistManager.java
new file mode 100644
index 0000000..a317a14
--- /dev/null
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 8674f9e..24b99e8 100644
--- a/src/main/resources/application.yml
+++ b/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
\ No newline at end of file
+ com.weiwojc: DEBUG
+ org.mybatis: DEBUG
\ No newline at end of file
--
Gitblit v1.9.3