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