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 "