diff --git a/pom.xml b/pom.xml index 4720db3..4062886 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,24 @@ lombok true + + + io.jsonwebtoken + jjwt-api + 0.12.6 + + + io.jsonwebtoken + jjwt-impl + 0.12.6 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.6 + runtime + org.springframework.boot spring-boot-starter-data-redis-test diff --git a/src/main/java/com/bicloud/Interceptor/JwtAuthInterceptor.java b/src/main/java/com/bicloud/Interceptor/JwtAuthInterceptor.java new file mode 100644 index 0000000..784bda0 --- /dev/null +++ b/src/main/java/com/bicloud/Interceptor/JwtAuthInterceptor.java @@ -0,0 +1,85 @@ +package com.bicloud.Interceptor; + +import com.bicloud.common.context.UserContext; +import com.bicloud.common.utils.JwtUtils; +import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.HandlerInterceptor; + +import static com.bicloud.common.utils.RedisConstants.JWT_BLACKLIST_KEY; + +/** + * JWT认证拦截器 + */ +@Slf4j +@Component +public class JwtAuthInterceptor implements HandlerInterceptor { + + @Resource + private JwtUtils jwtUtils; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 1. 从请求头获取Token + String token = jwtUtils.extractToken(request); + + // 2. 验证Token是否存在 + if (!StringUtils.hasText(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未登录,请先登录!\"}"); + return false; + } + + // 3. 验证Token有效性 + if (!jwtUtils.validateToken(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"Token无效或已过期!\"}"); + return false; + } + + // 4. 检查Token是否在黑名单中 + String blacklistKey = JWT_BLACKLIST_KEY + token; + Boolean isBlacklisted = stringRedisTemplate.hasKey(blacklistKey); + if (Boolean.TRUE.equals(isBlacklisted)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"Token已失效,请重新登录!\"}"); + return false; + } + + // 5. 解析Token,获取用户信息 + Long userId = jwtUtils.getUserIdFromToken(token); + String username = jwtUtils.getUsernameFromToken(token); + + if (userId == null || username == null) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"Token解析失败!\"}"); + return false; + } + + // 6. 将用户信息存入ThreadLocal + UserContext.setUserId(userId); + UserContext.setUsername(username); + + log.debug("用户认证成功,userId: {}, username: {}", userId, username); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + // 清除ThreadLocal,防止内存泄漏 + UserContext.clear(); + } +} + diff --git a/src/main/java/com/bicloud/Interceptor/RequestLogInterceptor.java b/src/main/java/com/bicloud/Interceptor/RequestLogInterceptor.java deleted file mode 100644 index 7047f73..0000000 --- a/src/main/java/com/bicloud/Interceptor/RequestLogInterceptor.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.bicloud.Interceptor; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.time.LocalDateTime; - - -/* -拦截器,请求打印请求状态 - */ -@Component -public class RequestLogInterceptor implements HandlerInterceptor { - - private static final Logger log = LoggerFactory.getLogger(RequestLogInterceptor.class); - private static final String START_TIME = "REQ_START_TIME"; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - request.setAttribute(START_TIME, System.currentTimeMillis()); - - log.info("[REQ] time={}, method={}, uri={}, ip={}", - LocalDateTime.now(), - request.getMethod(), - request.getRequestURI(), - request.getRemoteAddr()); - - return true; - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, - Object handler, Exception ex) { - Long start = (Long) request.getAttribute(START_TIME); - long cost = (start == null) ? -1 : (System.currentTimeMillis() - start); - - if (ex == null) { - log.info("[RES] status={}, costMs={}, uri={}", - response.getStatus(), cost, request.getRequestURI()); - } else { - log.warn("[RES] status={}, costMs={}, uri={}, ex={}", - response.getStatus(), cost, request.getRequestURI(), ex.toString()); - } - } -} - diff --git a/src/main/java/com/bicloud/common/context/UserContext.java b/src/main/java/com/bicloud/common/context/UserContext.java new file mode 100644 index 0000000..58d229d --- /dev/null +++ b/src/main/java/com/bicloud/common/context/UserContext.java @@ -0,0 +1,46 @@ +package com.bicloud.common.context; + +/** + * 用户上下文 - 使用ThreadLocal存储当前请求的用户信息 + */ +public class UserContext { + + private static final ThreadLocal USER_ID = new ThreadLocal<>(); + private static final ThreadLocal USERNAME = new ThreadLocal<>(); + + /** + * 设置用户ID + */ + public static void setUserId(Long userId) { + USER_ID.set(userId); + } + + /** + * 获取用户ID + */ + public static Long getUserId() { + return USER_ID.get(); + } + + /** + * 设置用户名 + */ + public static void setUsername(String username) { + USERNAME.set(username); + } + + /** + * 获取用户名 + */ + public static String getUsername() { + return USERNAME.get(); + } + + /** + * 清除用户信息 + */ + public static void clear() { + USER_ID.remove(); + USERNAME.remove(); + } +} diff --git a/src/main/java/com/bicloud/common/exception/BusinessException.java b/src/main/java/com/bicloud/common/exception/BusinessException.java new file mode 100644 index 0000000..2c7de84 --- /dev/null +++ b/src/main/java/com/bicloud/common/exception/BusinessException.java @@ -0,0 +1,15 @@ +package com.bicloud.common.exception; + +/** + * 业务异常类 + */ +public class BusinessException extends RuntimeException { + + public BusinessException(String message) { + super(message); + } + + public BusinessException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/bicloud/common/exception/GlobalExceptionHandler.java b/src/main/java/com/bicloud/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..31ec136 --- /dev/null +++ b/src/main/java/com/bicloud/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +//package com.bicloud.common.exception; +// +//import com.bicloud.common.result.Result; +//import lombok.extern.slf4j.Slf4j; +//import org.springframework.validation.BindException; +//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; +// +///** +// * 全局异常处理器 +// * 统一处理各类异常,返回友好的错误提示 +// */ +//@Slf4j +//@RestControllerAdvice(basePackages = "com.bicloud.controller") +//public class GlobalExceptionHandler { +// +// /** +// * 处理业务异常 +// */ +// @ExceptionHandler(BusinessException.class) +// public Result handleBusinessException(BusinessException e) { +// log.warn("业务异常:{}", e.getMessage()); +// return Result.error(e.getMessage()); +// } +// +// /** +// * 处理 @Valid 参数校验异常(用于 @RequestBody) +// * 当 RegisterDto 等 DTO 的字段验证失败时触发 +// */ +// @ExceptionHandler(MethodArgumentNotValidException.class) +// public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { +// // 获取第一个错误信息 +// String errorMessage = e.getBindingResult().getFieldErrors() +// .stream() +// .map(FieldError::getDefaultMessage) +// .findFirst() +// .orElse("参数校验失败"); +// +// log.warn("参数校验失败:{}", errorMessage); +// return Result.error(errorMessage); +// } +// +// /** +// * 处理 @Validated 参数校验异常(用于 @RequestParam, @PathVariable) +// */ +// @ExceptionHandler(BindException.class) +// public Result handleBindException(BindException e) { +// String errorMessage = e.getFieldErrors() +// .stream() +// .map(FieldError::getDefaultMessage) +// .findFirst() +// .orElse("参数校验失败"); +// +// log.warn("参数绑定失败:{}", errorMessage); +// return Result.error(errorMessage); +// } +// +// /** +// * 处理其他未捕获的异常 +// */ +// @ExceptionHandler(Exception.class) +// public Result handleException(Exception e) { +// log.error("系统异常", e); +// return Result.error("系统异常,请联系管理员"); +// } +//} diff --git a/src/main/java/com/bicloud/common/utils/JwtUtils.java b/src/main/java/com/bicloud/common/utils/JwtUtils.java new file mode 100644 index 0000000..7374ffe --- /dev/null +++ b/src/main/java/com/bicloud/common/utils/JwtUtils.java @@ -0,0 +1,150 @@ +package com.bicloud.common.utils; + +import com.bicloud.config.JwtProperties; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +/** + * JWT工具类 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtUtils { + + private final JwtProperties jwtProperties; + + private SecretKey secretKey; + + @PostConstruct + public void init() { + // 初始化密钥 + this.secretKey = Keys.hmacShaKeyFor( + jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8) + ); + } + + /** + * 生成AccessToken + */ + public String generateAccessToken(Long userId, String username) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getAccessTokenExpiration() * 1000); + + return Jwts.builder() + .subject(String.valueOf(userId)) + .claim("username", username) + .issuedAt(now) + .expiration(expiryDate) + .signWith(secretKey) + .compact(); + } + + /** + * 生成RefreshToken + */ + public String generateRefreshToken(Long userId, String username) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + jwtProperties.getRefreshTokenExpiration() * 1000); + + return Jwts.builder() + .subject(String.valueOf(userId)) + .claim("username", username) + .issuedAt(now) + .expiration(expiryDate) + .signWith(secretKey) + .compact(); + } + + /** + * 解析Token + */ + public Claims parseToken(String token) { + try { + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } catch (Exception e) { + log.error("解析Token失败: {}", e.getMessage()); + return null; + } + } + + /** + * 从Token中获取用户ID + */ + public Long getUserIdFromToken(String token) { + Claims claims = parseToken(token); + if (claims == null) { + return null; + } + return Long.parseLong(claims.getSubject()); + } + + /** + * 从Token中获取用户名 + */ + public String getUsernameFromToken(String token) { + Claims claims = parseToken(token); + if (claims == null) { + return null; + } + return claims.get("username", String.class); + } + + /** + * 验证Token是否有效 + */ + public boolean validateToken(String token) { + try { + Claims claims = parseToken(token); + if (claims == null) { + return false; + } + // 检查是否过期 + Date expiration = claims.getExpiration(); + return expiration.after(new Date()); + } catch (Exception e) { + log.error("验证Token失败: {}", e.getMessage()); + return false; + } + } + + /** + * 从请求头中提取Token + */ + public String extractToken(HttpServletRequest request) { + String bearerToken = request.getHeader(jwtProperties.getTokenHeader()); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(jwtProperties.getTokenPrefix())) { + return bearerToken.substring(jwtProperties.getTokenPrefix().length()); + } + return null; + } + + /** + * 获取Token剩余有效时间(秒) + */ + public Long getTokenRemainingTime(String token) { + Claims claims = parseToken(token); + if (claims == null) { + return 0L; + } + Date expiration = claims.getExpiration(); + long remainingTime = (expiration.getTime() - System.currentTimeMillis()) / 1000; + return Math.max(remainingTime, 0L); + } +} + diff --git a/src/main/java/com/bicloud/common/utils/RedisConstants.java b/src/main/java/com/bicloud/common/utils/RedisConstants.java index a319d58..cf9a907 100644 --- a/src/main/java/com/bicloud/common/utils/RedisConstants.java +++ b/src/main/java/com/bicloud/common/utils/RedisConstants.java @@ -26,4 +26,8 @@ public class RedisConstants { public static final String FEED_KEY = "feed:"; public static final String SHOP_GEO_KEY = "shop:geo:"; public static final String USER_SIGN_KEY = "sign:"; + + // JWT相关常量 + public static final String JWT_BLACKLIST_KEY = "jwt:blacklist:"; + public static final Long JWT_BLACKLIST_TTL = 7L; // 7天,与RefreshToken过期时间一致 } diff --git a/src/main/java/com/bicloud/common/utils/RegexUtils.java b/src/main/java/com/bicloud/common/utils/RegexUtils.java index 05ef296..757e59b 100644 --- a/src/main/java/com/bicloud/common/utils/RegexUtils.java +++ b/src/main/java/com/bicloud/common/utils/RegexUtils.java @@ -29,6 +29,24 @@ public class RegexUtils { return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX); } + /** + * 是否是有效手机格式 + * @param phone 要校验的手机号 + * @return true:有效,false:无效 + */ + public static boolean isPhoneValid(String phone) { + return !isPhoneInvalid(phone); + } + + /** + * 是否是有效邮箱格式 + * @param email 要校验的邮箱 + * @return true:有效,false:无效 + */ + public static boolean isEmailValid(String email) { + return !isEmailInvalid(email); + } + // 校验是否不符合正则格式 private static boolean mismatch(String str, String regex){ if (StrUtil.isBlank(str)) { diff --git a/src/main/java/com/bicloud/config/JwtProperties.java b/src/main/java/com/bicloud/config/JwtProperties.java new file mode 100644 index 0000000..b80c423 --- /dev/null +++ b/src/main/java/com/bicloud/config/JwtProperties.java @@ -0,0 +1,39 @@ +package com.bicloud.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * JWT配置属性类 + */ +@Data +@Component +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + + /** + * JWT密钥 + */ + private String secret; + + /** + * AccessToken过期时间(秒) + */ + private Long accessTokenExpiration; + + /** + * RefreshToken过期时间(秒) + */ + private Long refreshTokenExpiration; + + /** + * Token请求头名称 + */ + private String tokenHeader; + + /** + * Token前缀 + */ + private String tokenPrefix; +} diff --git a/src/main/java/com/bicloud/config/WebMvcConfig.java b/src/main/java/com/bicloud/config/WebMvcConfig.java index 0a3474a..866acfc 100644 --- a/src/main/java/com/bicloud/config/WebMvcConfig.java +++ b/src/main/java/com/bicloud/config/WebMvcConfig.java @@ -1,6 +1,6 @@ package com.bicloud.config; -import com.bicloud.Interceptor.RequestLogInterceptor; +import com.bicloud.Interceptor.JwtAuthInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -8,24 +8,30 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { - private final RequestLogInterceptor requestLogInterceptor; + private final JwtAuthInterceptor jwtAuthInterceptor; - public WebMvcConfig(RequestLogInterceptor requestLogInterceptor) { - this.requestLogInterceptor = requestLogInterceptor; + public WebMvcConfig(JwtAuthInterceptor jwtAuthInterceptor) { + this.jwtAuthInterceptor = jwtAuthInterceptor; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(requestLogInterceptor) + // JWT认证拦截器 + registry.addInterceptor(jwtAuthInterceptor) .addPathPatterns("/**") - // 排除 Swagger/Knife4j 文档路径 .excludePathPatterns( + // Swagger/Knife4j 文档路径 "/doc.html", "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", "/webjars/**", - "/favicon.ico" + "/favicon.ico", + // 用户相关放行路径 + "/user/code", + "/user/register", + "/user/login", + "/user/refresh-token" ); } } diff --git a/src/main/java/com/bicloud/controller/UserController.java b/src/main/java/com/bicloud/controller/UserController.java index 78afe2c..71998eb 100644 --- a/src/main/java/com/bicloud/controller/UserController.java +++ b/src/main/java/com/bicloud/controller/UserController.java @@ -2,7 +2,11 @@ package com.bicloud.controller; import com.bicloud.common.result.Result; +import com.bicloud.common.utils.JwtUtils; +import com.bicloud.pojo.dto.LoginDto; import com.bicloud.pojo.dto.RegisterDto; +import com.bicloud.pojo.vo.LoginVo; +import com.bicloud.pojo.vo.UserInfoVo; import com.bicloud.service.UserService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -22,6 +26,9 @@ public class UserController { @Autowired private UserService userService; + @Autowired + private JwtUtils jwtUtils; + /** * 发送手机验证码 */ @@ -41,4 +48,45 @@ public class UserController { return userService.register(registerDto); } + /** + * 用户登录 + */ + @PostMapping("/login") + @Operation(summary = "用户登录") + public Result login(@Valid @RequestBody LoginDto loginDto) { + LoginVo loginVo = userService.login(loginDto); + return Result.success(loginVo); + } + + /** + * 刷新Token + */ + @PostMapping("/refresh-token") + @Operation(summary = "刷新Token") + public Result refreshToken(@RequestParam("refreshToken") String refreshToken) { + String newAccessToken = userService.refreshToken(refreshToken); + return Result.success(newAccessToken); + } + + /** + * 用户登出 + */ + @PostMapping("/logout") + @Operation(summary = "用户登出") + public Result logout(HttpServletRequest request) { + String token = jwtUtils.extractToken(request); + userService.logout(token); + return Result.success("登出成功!"); + } + + /** + * 获取当前用户信息 + */ + @GetMapping("/info") + @Operation(summary = "获取当前用户信息") + public Result getUserInfo() { + UserInfoVo userInfo = userService.getCurrentUserInfo(); + return Result.success(userInfo); + } + } diff --git a/src/main/java/com/bicloud/pojo/dto/LoginDto.java b/src/main/java/com/bicloud/pojo/dto/LoginDto.java new file mode 100644 index 0000000..c969810 --- /dev/null +++ b/src/main/java/com/bicloud/pojo/dto/LoginDto.java @@ -0,0 +1,21 @@ +package com.bicloud.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * 登录请求DTO + */ +@Data +@Schema(description = "登录请求") +public class LoginDto { + + @Schema(description = "登录标识(手机号/邮箱/用户名)", example = "13800138000") + @NotBlank(message = "登录标识不能为空") + private String identifier; + + @Schema(description = "密码", example = "Password123") + @NotBlank(message = "密码不能为空") + private String password; +} diff --git a/src/main/java/com/bicloud/pojo/vo/LoginVo.java b/src/main/java/com/bicloud/pojo/vo/LoginVo.java new file mode 100644 index 0000000..85c0d9a --- /dev/null +++ b/src/main/java/com/bicloud/pojo/vo/LoginVo.java @@ -0,0 +1,36 @@ +package com.bicloud.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 登录响应VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "登录响应") +public class LoginVo { + + @Schema(description = "访问令牌") + private String accessToken; + + @Schema(description = "刷新令牌") + private String refreshToken; + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "访问令牌过期时间(秒)", example = "7200") + private Long expiresIn; + + @Schema(description = "用户ID") + private Long userId; + + @Schema(description = "用户名") + private String username; +} diff --git a/src/main/java/com/bicloud/pojo/vo/UserInfoVo.java b/src/main/java/com/bicloud/pojo/vo/UserInfoVo.java new file mode 100644 index 0000000..ca0f8e7 --- /dev/null +++ b/src/main/java/com/bicloud/pojo/vo/UserInfoVo.java @@ -0,0 +1,35 @@ +package com.bicloud.pojo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户信息VO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "用户信息") +public class UserInfoVo { + + @Schema(description = "用户ID") + private Long id; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "手机号") + private String phone; + + @Schema(description = "邮箱") + private String email; + + @Schema(description = "创建时间") + private LocalDateTime createTime; +} diff --git a/src/main/java/com/bicloud/service/UserService.java b/src/main/java/com/bicloud/service/UserService.java index 813fbeb..517087c 100644 --- a/src/main/java/com/bicloud/service/UserService.java +++ b/src/main/java/com/bicloud/service/UserService.java @@ -1,7 +1,10 @@ package com.bicloud.service; import com.bicloud.common.result.Result; +import com.bicloud.pojo.dto.LoginDto; import com.bicloud.pojo.dto.RegisterDto; +import com.bicloud.pojo.vo.LoginVo; +import com.bicloud.pojo.vo.UserInfoVo; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; @@ -9,4 +12,24 @@ public interface UserService { Result sendCode(String mobile, HttpSession session, HttpServletRequest request); Result register(RegisterDto registerDto); + + /** + * 用户登录 + */ + LoginVo login(LoginDto loginDto); + + /** + * 刷新Token + */ + String refreshToken(String refreshToken); + + /** + * 用户登出 + */ + void logout(String token); + + /** + * 获取当前用户信息 + */ + UserInfoVo getCurrentUserInfo(); } diff --git a/src/main/java/com/bicloud/service/impl/UserServiceImpl.java b/src/main/java/com/bicloud/service/impl/UserServiceImpl.java index da2771c..2537756 100644 --- a/src/main/java/com/bicloud/service/impl/UserServiceImpl.java +++ b/src/main/java/com/bicloud/service/impl/UserServiceImpl.java @@ -1,11 +1,18 @@ package com.bicloud.service.impl; +import com.bicloud.common.context.UserContext; +import com.bicloud.common.exception.BusinessException; import com.bicloud.common.result.Result; +import com.bicloud.common.utils.JwtUtils; import com.bicloud.common.utils.PasswordUtils; import com.bicloud.common.utils.RegexUtils; +import com.bicloud.config.JwtProperties; import com.bicloud.mapper.UserMapper; +import com.bicloud.pojo.dto.LoginDto; import com.bicloud.pojo.dto.RegisterDto; import com.bicloud.pojo.entity.User; +import com.bicloud.pojo.vo.LoginVo; +import com.bicloud.pojo.vo.UserInfoVo; import com.bicloud.service.UserService; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletRequest; @@ -28,6 +35,12 @@ public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; + @Resource + private JwtUtils jwtUtils; + + @Resource + private JwtProperties jwtProperties; + @Override public Result sendCode(String mobile, HttpSession session, HttpServletRequest request) { // 1.校验手机号 @@ -147,5 +160,115 @@ public class UserServiceImpl implements UserService { return Result.success("注册成功!"); } + @Override + public LoginVo login(LoginDto loginDto) { + String identifier = loginDto.getIdentifier(); + String password = loginDto.getPassword(); + + // 1. 根据登录标识类型查询用户 + User user = null; + if (RegexUtils.isPhoneValid(identifier)) { + // 手机号登录 + user = userMapper.selectByMobile(identifier); + } else if (RegexUtils.isEmailValid(identifier)) { + // 邮箱登录 + user = userMapper.selectByEmail(identifier); + } else { + // 用户名登录 + user = userMapper.selectByUsername(identifier); + } + + // 2. 验证用户是否存在 + if (user == null) { + throw new BusinessException("用户不存在!"); + } + + // 3. 验证密码 + if (!PasswordUtils.verifyPassword(password, user.getSalt(), user.getPwd())) { + throw new BusinessException("密码错误!"); + } + + // 4. 生成Token + String accessToken = jwtUtils.generateAccessToken(user.getId(), user.getUsername()); + String refreshToken = jwtUtils.generateRefreshToken(user.getId(), user.getUsername()); + + // 5. 构建返回结果 + return LoginVo.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(jwtProperties.getAccessTokenExpiration()) + .userId(user.getId()) + .username(user.getUsername()) + .build(); + } + + @Override + public String refreshToken(String refreshToken) { + // 1. 验证RefreshToken + if (!jwtUtils.validateToken(refreshToken)) { + throw new BusinessException("RefreshToken无效或已过期!"); + } + + // 2. 检查黑名单 + String blacklistKey = JWT_BLACKLIST_KEY + refreshToken; + Boolean isBlacklisted = stringRedisTemplate.hasKey(blacklistKey); + if (Boolean.TRUE.equals(isBlacklisted)) { + throw new BusinessException("Token已失效,请重新登录!"); + } + + // 3. 从Token中获取用户信息 + Long userId = jwtUtils.getUserIdFromToken(refreshToken); + String username = jwtUtils.getUsernameFromToken(refreshToken); + + if (userId == null || username == null) { + throw new BusinessException("Token解析失败!"); + } + + // 4. 生成新的AccessToken + return jwtUtils.generateAccessToken(userId, username); + } + + @Override + public void logout(String token) { + // 1. 将Token加入黑名单 + String blacklistKey = JWT_BLACKLIST_KEY + token; + Long remainingTime = jwtUtils.getTokenRemainingTime(token); + + // 2. 设置黑名单过期时间为Token剩余有效时间 + stringRedisTemplate.opsForValue().set( + blacklistKey, + "1", + remainingTime, + TimeUnit.SECONDS + ); + + log.info("用户登出成功,Token已加入黑名单"); + } + + @Override + public UserInfoVo getCurrentUserInfo() { + // 1. 从上下文获取用户ID + Long userId = UserContext.getUserId(); + if (userId == null) { + throw new BusinessException("未登录!"); + } + + // 2. 查询用户信息 + User user = userMapper.selectById(userId); + if (user == null) { + throw new BusinessException("用户不存在!"); + } + + // 3. 构建返回结果 + return UserInfoVo.builder() + .id(user.getId()) + .username(user.getUsername()) + .phone(user.getMobile()) + .email(user.getEmail()) + .createTime(user.getCreateTime()) + .build(); + } + } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b4d8fe4..e827aa4 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -52,3 +52,10 @@ knife4j: logging: level: com.genepioneer.mapper: debug + +jwt: + secret: bicloud-jwt-secret-key-2024-spring-boot-application-secure-token-generation + access-token-expiration: 7200 + refresh-token-expiration: 604800 + token-header: Authorization + token-prefix: "Bearer "