登录接口
This commit is contained in:
18
pom.xml
18
pom.xml
@@ -68,6 +68,24 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- JWT依赖 -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.6</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis-test</artifactId>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
46
src/main/java/com/bicloud/common/context/UserContext.java
Normal file
46
src/main/java/com/bicloud/common/context/UserContext.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.bicloud.common.context;
|
||||
|
||||
/**
|
||||
* 用户上下文 - 使用ThreadLocal存储当前请求的用户信息
|
||||
*/
|
||||
public class UserContext {
|
||||
|
||||
private static final ThreadLocal<Long> USER_ID = new ThreadLocal<>();
|
||||
private static final ThreadLocal<String> 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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("系统异常,请联系管理员");
|
||||
// }
|
||||
//}
|
||||
150
src/main/java/com/bicloud/common/utils/JwtUtils.java
Normal file
150
src/main/java/com/bicloud/common/utils/JwtUtils.java
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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过期时间一致
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
39
src/main/java/com/bicloud/config/JwtProperties.java
Normal file
39
src/main/java/com/bicloud/config/JwtProperties.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LoginVo> login(@Valid @RequestBody LoginDto loginDto) {
|
||||
LoginVo loginVo = userService.login(loginDto);
|
||||
return Result.success(loginVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新Token
|
||||
*/
|
||||
@PostMapping("/refresh-token")
|
||||
@Operation(summary = "刷新Token")
|
||||
public Result<String> 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<UserInfoVo> getUserInfo() {
|
||||
UserInfoVo userInfo = userService.getCurrentUserInfo();
|
||||
return Result.success(userInfo);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
21
src/main/java/com/bicloud/pojo/dto/LoginDto.java
Normal file
21
src/main/java/com/bicloud/pojo/dto/LoginDto.java
Normal file
@@ -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;
|
||||
}
|
||||
36
src/main/java/com/bicloud/pojo/vo/LoginVo.java
Normal file
36
src/main/java/com/bicloud/pojo/vo/LoginVo.java
Normal file
@@ -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;
|
||||
}
|
||||
35
src/main/java/com/bicloud/pojo/vo/UserInfoVo.java
Normal file
35
src/main/java/com/bicloud/pojo/vo/UserInfoVo.java
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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 "
|
||||
|
||||
Reference in New Issue
Block a user