diff --git a/SpringBoot-JWT -kimi-update/.gitignore b/SpringBoot-JWT -kimi-update/.gitignore new file mode 100644 index 0000000..153c933 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/.gitignore @@ -0,0 +1,29 @@ +HELP.md +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/SpringBoot-JWT -kimi-update/pom.xml b/SpringBoot-JWT -kimi-update/pom.xml new file mode 100644 index 0000000..cbe13ea --- /dev/null +++ b/SpringBoot-JWT -kimi-update/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + top.lrshuai + jwt + 1.0.0 + jwt + Modern JWT Authentication System for Spring Boot + + + 1.8 + 0.11.5 + 1.7.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + + org.springdoc + springdoc-openapi-ui + ${springdoc.version} + + + + + org.projectlombok + lombok + true + + + + + org.apache.commons + commons-lang3 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/JwtApplication.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/JwtApplication.java new file mode 100644 index 0000000..aac3348 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/JwtApplication.java @@ -0,0 +1,16 @@ +package top.lrshuai.jwt; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import top.lrshuai.jwt.config.JwtProperties; + +@SpringBootApplication +@EnableConfigurationProperties(JwtProperties.class) +public class JwtApplication { + + public static void main(String[] args) { + SpringApplication.run(JwtApplication.class, args); + } + +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/common/Result.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/common/Result.java new file mode 100644 index 0000000..f17e0ec --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/common/Result.java @@ -0,0 +1,88 @@ +package top.lrshuai.jwt.common; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Schema(description = "统一响应结果") +public class Result { + + @Schema(description = "状态码", example = "200") + private Integer code; + + @Schema(description = "响应消息", example = "操作成功") + private String message; + + @Schema(description = "响应数据") + private T data; + + @Schema(description = "时间戳") + private LocalDateTime timestamp; + + @Schema(description = "请求路径") + private String path; + + public Result() { + this.timestamp = LocalDateTime.now(); + } + + public static Result success() { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(ResultCode.SUCCESS.getMessage()); + return result; + } + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(ResultCode.SUCCESS.getMessage()); + result.setData(data); + return result; + } + + public static Result success(String message, T data) { + Result result = new Result<>(); + result.setCode(ResultCode.SUCCESS.getCode()); + result.setMessage(message); + result.setData(data); + return result; + } + + public static Result error() { + Result result = new Result<>(); + result.setCode(ResultCode.ERROR.getCode()); + result.setMessage(ResultCode.ERROR.getMessage()); + return result; + } + + public static Result error(String message) { + Result result = new Result<>(); + result.setCode(ResultCode.ERROR.getCode()); + result.setMessage(message); + return result; + } + + public static Result error(int code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } + + public static Result error(ResultCode resultCode) { + Result result = new Result<>(); + result.setCode(resultCode.getCode()); + result.setMessage(resultCode.getMessage()); + return result; + } + + public static Result error(ResultCode resultCode, String message) { + Result result = new Result<>(); + result.setCode(resultCode.getCode()); + result.setMessage(message); + return result; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/common/ResultCode.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/common/ResultCode.java new file mode 100644 index 0000000..3c49707 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/common/ResultCode.java @@ -0,0 +1,43 @@ +package top.lrshuai.jwt.common; + +import lombok.Getter; + +@Getter +public enum ResultCode { + + SUCCESS(200, "操作成功"), + ERROR(500, "系统错误"), + + // 认证相关 1000-1099 + UNAUTHORIZED(401, "未授权,请先登录"), + FORBIDDEN(403, "无权限访问该资源"), + TOKEN_EXPIRED(1001, "Token已过期"), + TOKEN_INVALID(1002, "Token无效"), + TOKEN_REVOKED(1003, "Token已被吊销"), + REFRESH_TOKEN_EXPIRED(1004, "Refresh Token已过期"), + + // 用户相关 1100-1199 + USER_NOT_FOUND(1100, "用户不存在"), + USERNAME_EXISTS(1101, "用户名已存在"), + EMAIL_EXISTS(1102, "邮箱已被注册"), + PHONE_EXISTS(1103, "手机号已被注册"), + PASSWORD_ERROR(1104, "密码错误"), + ACCOUNT_LOCKED(1105, "账号已被锁定"), + ACCOUNT_DISABLED(1106, "账号已被禁用"), + + // 验证码相关 1200-1299 + CAPTCHA_ERROR(1200, "验证码错误"), + CAPTCHA_EXPIRED(1201, "验证码已过期"), + + // 参数相关 1300-1399 + PARAM_ERROR(1300, "参数错误"), + PARAM_MISSING(1301, "缺少必要参数"); + + private final int code; + private final String message; + + ResultCode(int code, String message) { + this.code = code; + this.message = message; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/JwtProperties.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/JwtProperties.java new file mode 100644 index 0000000..5d5aac2 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/JwtProperties.java @@ -0,0 +1,19 @@ +package top.lrshuai.jwt.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "jwt") +public class JwtProperties { + + private String secret = "your-256-bit-secret-key-for-jwt-signing-must-be-at-least-32-characters-long"; + private String issuer = "jwt-system"; + private String audience = "web-app"; + private Long accessTokenExpire = 7200L; + private Long refreshTokenExpire = 604800L; + private String tokenPrefix = "Bearer "; + private String headerName = "Authorization"; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/OpenApiConfig.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/OpenApiConfig.java new file mode 100644 index 0000000..79f4d83 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/OpenApiConfig.java @@ -0,0 +1,37 @@ +package top.lrshuai.jwt.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("JWT认证系统 API") + .version("1.0.0") + .description("基于Spring Boot的现代化JWT认证系统,支持双Token机制、用户管理、权限控制等功能") + .contact(new Contact() + .name("JWT System") + .email("support@example.com")) + .license(new License() + .name("Apache 2.0") + .url("https://www.apache.org/licenses/LICENSE-2.0"))) + .addSecurityItem(new SecurityRequirement().addList("bearerAuth")) + .components(new Components() + .addSecuritySchemes("bearerAuth", new SecurityScheme() + .name("bearerAuth") + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/WebConfig.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/WebConfig.java new file mode 100644 index 0000000..a322293 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/config/WebConfig.java @@ -0,0 +1,52 @@ +package top.lrshuai.jwt.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import top.lrshuai.jwt.interceptor.JwtInterceptor; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Autowired + private JwtInterceptor jwtInterceptor; + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(jwtInterceptor) + .addPathPatterns("/api/v1/**") + .excludePathPatterns( + "/api/v1/auth/login", + "/api/v1/auth/register", + "/api/v1/auth/refresh", + "/api/v1/auth/captcha", + "/api/v1/token/verify", + "/api/v1/token/parse", + "/swagger-ui/**", + "/swagger-ui.html", + "/v3/api-docs/**", + "/webjars/**", + "/error" + ); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/swagger-ui/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/springfox-swagger-ui/") + .resourceChain(false); + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/AuthController.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/AuthController.java new file mode 100644 index 0000000..404702d --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/AuthController.java @@ -0,0 +1,64 @@ +package top.lrshuai.jwt.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.lrshuai.jwt.common.Result; +import top.lrshuai.jwt.dto.*; +import top.lrshuai.jwt.service.AuthService; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/v1/auth") +@Tag(name = "认证管理", description = "用户认证相关接口:登录、注册、登出、刷新Token等") +@Validated +public class AuthController { + + @Autowired + private AuthService authService; + + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户登录接口,支持用户名密码登录") + public Result login(@Valid @RequestBody LoginRequest request) { + return Result.success(authService.login(request)); + } + + @PostMapping("/register") + @Operation(summary = "用户注册", description = "用户注册接口,需要用户名、密码、确认密码,可选邮箱和手机号") + public Result register(@Valid @RequestBody RegisterRequest request) { + authService.register(request); + return Result.success("注册成功"); + } + + @PostMapping("/logout") + @Operation(summary = "用户登出", description = "用户登出接口,需要传入Authorization头") + public Result logout(HttpServletRequest request) { + String token = extractTokenFromRequest(request); + Long userId = getCurrentUserId(request); + authService.logout(userId, token); + return Result.success("登出成功"); + } + + @PostMapping("/refresh") + @Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token") + public Result refreshToken(@Valid @RequestBody RefreshTokenRequest request) { + return Result.success(authService.refreshToken(request)); + } + + private String extractTokenFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + private Long getCurrentUserId(HttpServletRequest request) { + Object userId = request.getAttribute("userId"); + return userId != null ? Long.valueOf(userId.toString()) : null; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/PermissionController.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/PermissionController.java new file mode 100644 index 0000000..56dbc5f --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/PermissionController.java @@ -0,0 +1,55 @@ +package top.lrshuai.jwt.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import top.lrshuai.jwt.common.Result; +import top.lrshuai.jwt.service.AuthService; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1") +@Tag(name = "权限管理", description = "用户权限和角色查询接口") +@Validated +@SecurityRequirement(name = "bearerAuth") +public class PermissionController { + + @Autowired + private AuthService authService; + + @GetMapping("/permissions") + @Operation(summary = "获取当前用户权限", description = "获取当前登录用户的所有权限列表") + public Result> getPermissions(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + List permissions = authService.getUserPermissions(userId); + Map result = new HashMap<>(); + result.put("permissions", permissions); + result.put("count", permissions.size()); + return Result.success(result); + } + + @GetMapping("/roles") + @Operation(summary = "获取当前用户角色", description = "获取当前登录用户的所有角色列表") + public Result> getRoles(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + List roles = authService.getUserRoles(userId); + Map result = new HashMap<>(); + result.put("roles", roles); + result.put("count", roles.size()); + return Result.success(result); + } + + private Long getCurrentUserId(HttpServletRequest request) { + Object userId = request.getAttribute("userId"); + return userId != null ? Long.valueOf(userId.toString()) : null; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/TokenController.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/TokenController.java new file mode 100644 index 0000000..5eda384 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/TokenController.java @@ -0,0 +1,78 @@ +package top.lrshuai.jwt.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.lrshuai.jwt.common.Result; +import top.lrshuai.jwt.dto.TokenParseResponse; +import top.lrshuai.jwt.dto.TokenVerifyRequest; +import top.lrshuai.jwt.util.JwtUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +@RestController +@RequestMapping("/api/v1/token") +@Tag(name = "Token管理", description = "Token验证、解析、吊销等管理接口") +@Validated +public class TokenController { + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private StringRedisTemplate redisTemplate; + + @PostMapping("/verify") + @Operation(summary = "验证Token", description = "验证JWT Token的有效性,返回验证结果") + public Result> verifyToken(@Valid @RequestBody TokenVerifyRequest request) { + boolean valid = jwtUtils.validateToken(request.getToken()); + Map result = new HashMap<>(); + result.put("valid", valid); + result.put("message", valid ? "Token有效" : "Token无效或已过期"); + return Result.success(result); + } + + @PostMapping("/parse") + @Operation(summary = "解析Token", description = "解析JWT Token,返回Token中包含的详细信息和声明") + public Result parseToken(@Valid @RequestBody TokenVerifyRequest request) { + TokenParseResponse response = jwtUtils.parseTokenDetails(request.getToken()); + return Result.success(response); + } + + @PostMapping("/revoke") + @Operation(summary = "吊销Token", description = "将Token加入黑名单,使其失效") + public Result revokeToken(HttpServletRequest request) { + String token = extractTokenFromRequest(request); + if (token != null) { + redisTemplate.opsForSet().add("token:blacklist", token); + redisTemplate.expire("token:blacklist", 7, TimeUnit.DAYS); + } + return Result.success("Token已吊销"); + } + + @GetMapping("/info") + @Operation(summary = "获取当前Token信息", description = "获取当前请求中Token的详细信息") + public Result getTokenInfo(HttpServletRequest request) { + String token = extractTokenFromRequest(request); + if (token == null) { + return Result.error(401, "未提供Token"); + } + TokenParseResponse response = jwtUtils.parseTokenDetails(token); + return Result.success(response); + } + + private String extractTokenFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/UserController.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/UserController.java new file mode 100644 index 0000000..93f5ee5 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/controller/UserController.java @@ -0,0 +1,56 @@ +package top.lrshuai.jwt.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import top.lrshuai.jwt.common.Result; +import top.lrshuai.jwt.dto.ChangePasswordRequest; +import top.lrshuai.jwt.dto.UpdateProfileRequest; +import top.lrshuai.jwt.dto.UserProfileDTO; +import top.lrshuai.jwt.service.AuthService; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/v1/user") +@Tag(name = "用户管理", description = "用户资料管理相关接口") +@Validated +@SecurityRequirement(name = "bearerAuth") +public class UserController { + + @Autowired + private AuthService authService; + + @GetMapping("/profile") + @Operation(summary = "获取当前用户信息", description = "获取当前登录用户的详细资料") + public Result getProfile(HttpServletRequest request) { + Long userId = getCurrentUserId(request); + return Result.success(authService.getUserProfile(userId)); + } + + @PutMapping("/profile") + @Operation(summary = "更新用户信息", description = "更新当前登录用户的资料信息") + public Result updateProfile(@Valid @RequestBody UpdateProfileRequest request, + HttpServletRequest httpRequest) { + Long userId = getCurrentUserId(httpRequest); + return Result.success(authService.updateUserProfile(userId, request)); + } + + @PutMapping("/password") + @Operation(summary = "修改密码", description = "修改当前登录用户的密码") + public Result changePassword(@Valid @RequestBody ChangePasswordRequest request, + HttpServletRequest httpRequest) { + Long userId = getCurrentUserId(httpRequest); + authService.changePassword(userId, request); + return Result.success("密码修改成功,请重新登录"); + } + + private Long getCurrentUserId(HttpServletRequest request) { + Object userId = request.getAttribute("userId"); + return userId != null ? Long.valueOf(userId.toString()) : null; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/ChangePasswordRequest.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/ChangePasswordRequest.java new file mode 100644 index 0000000..618a886 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/ChangePasswordRequest.java @@ -0,0 +1,25 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +@Schema(description = "修改密码请求参数") +public class ChangePasswordRequest { + + @NotBlank(message = "旧密码不能为空") + @Schema(description = "旧密码", required = true, example = "oldpassword") + private String oldPassword; + + @NotBlank(message = "新密码不能为空") + @Size(min = 6, max = 32, message = "新密码长度必须在6-32个字符之间") + @Schema(description = "新密码", required = true, example = "newpassword") + private String newPassword; + + @NotBlank(message = "确认密码不能为空") + @Schema(description = "确认新密码", required = true, example = "newpassword") + private String confirmPassword; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/LoginRequest.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/LoginRequest.java new file mode 100644 index 0000000..c79ca1c --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/LoginRequest.java @@ -0,0 +1,25 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Data +@Schema(description = "登录请求参数") +public class LoginRequest { + + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 32, message = "用户名长度必须在3-32个字符之间") + @Schema(description = "用户名", required = true, example = "admin") + private String username; + + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 32, message = "密码长度必须在6-32个字符之间") + @Schema(description = "密码", required = true, example = "123456") + private String password; + + @Schema(description = "记住我", example = "true") + private Boolean rememberMe = false; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/LoginResponse.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/LoginResponse.java new file mode 100644 index 0000000..86d45c8 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/LoginResponse.java @@ -0,0 +1,21 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "登录响应结果") +public class LoginResponse { + + @Schema(description = "用户信息") + private UserProfileDTO user; + + @Schema(description = "Token信息") + private TokenResponse token; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/RefreshTokenRequest.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/RefreshTokenRequest.java new file mode 100644 index 0000000..1cbbf27 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/RefreshTokenRequest.java @@ -0,0 +1,15 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@Schema(description = "刷新Token请求参数") +public class RefreshTokenRequest { + + @NotBlank(message = "刷新令牌不能为空") + @Schema(description = "刷新令牌(Refresh Token)", required = true, example = "eyJhbGciOiJIUzI1NiIs...") + private String refreshToken; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/RegisterRequest.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/RegisterRequest.java new file mode 100644 index 0000000..b49bf15 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/RegisterRequest.java @@ -0,0 +1,40 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +@Data +@Schema(description = "注册请求参数") +public class RegisterRequest { + + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 32, message = "用户名长度必须在3-32个字符之间") + @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线") + @Schema(description = "用户名", required = true, example = "newuser") + private String username; + + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 32, message = "密码长度必须在6-32个字符之间") + @Schema(description = "密码", required = true, example = "123456") + private String password; + + @NotBlank(message = "确认密码不能为空") + @Schema(description = "确认密码", required = true, example = "123456") + private String confirmPassword; + + @Schema(description = "昵称", example = "张三") + private String nickname; + + @Email(message = "邮箱格式不正确") + @Schema(description = "邮箱", example = "user@example.com") + private String email; + + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + @Schema(description = "手机号", example = "13800138000") + private String phone; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenParseResponse.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenParseResponse.java new file mode 100644 index 0000000..77ad15e --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenParseResponse.java @@ -0,0 +1,54 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Token解析响应结果") +public class TokenParseResponse { + + @Schema(description = "Token ID", example = "jwt-id-xxx") + private String id; + + @Schema(description = "签发者", example = "jwt-system") + private String issuer; + + @Schema(description = "主题", example = "user-auth") + private String subject; + + @Schema(description = "受众", example = "web-app") + private String audience; + + @Schema(description = "签发时间", example = "2024-01-01T10:00:00") + private LocalDateTime issuedAt; + + @Schema(description = "过期时间", example = "2024-01-01T12:00:00") + private LocalDateTime expiration; + + @Schema(description = "生效时间", example = "2024-01-01T10:00:00") + private LocalDateTime notBefore; + + @Schema(description = "用户ID", example = "1") + private Long userId; + + @Schema(description = "用户名", example = "admin") + private String username; + + @Schema(description = "自定义声明") + private Map claims; + + @Schema(description = "是否有效", example = "true") + private Boolean valid; + + @Schema(description = "验证消息", example = "Token验证通过") + private String message; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenResponse.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenResponse.java new file mode 100644 index 0000000..35fe0d8 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenResponse.java @@ -0,0 +1,35 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "Token响应结果") +public class TokenResponse { + + @Schema(description = "访问令牌(Access Token)", example = "eyJhbGciOiJIUzI1NiIs...") + private String accessToken; + + @Schema(description = "刷新令牌(Refresh Token)", example = "eyJhbGciOiJIUzI1NiIs...") + private String refreshToken; + + @Schema(description = "令牌类型", example = "Bearer") + private String tokenType; + + @Schema(description = "Access Token过期时间(秒)", example = "7200") + private Long expiresIn; + + @Schema(description = "Access Token过期时间", example = "2024-01-01T12:00:00") + private LocalDateTime expireTime; + + @Schema(description = "Refresh Token过期时间", example = "2024-01-08T12:00:00") + private LocalDateTime refreshExpireTime; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenVerifyRequest.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenVerifyRequest.java new file mode 100644 index 0000000..d3cf47c --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/TokenVerifyRequest.java @@ -0,0 +1,15 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@Schema(description = "Token验证请求参数") +public class TokenVerifyRequest { + + @NotBlank(message = "Token不能为空") + @Schema(description = "JWT Token", required = true, example = "eyJhbGciOiJIUzI1NiIs...") + private String token; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/UpdateProfileRequest.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/UpdateProfileRequest.java new file mode 100644 index 0000000..0226905 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/UpdateProfileRequest.java @@ -0,0 +1,31 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +@Data +@Schema(description = "更新用户资料请求参数") +public class UpdateProfileRequest { + + @Size(max = 32, message = "昵称长度不能超过32个字符") + @Schema(description = "昵称", example = "张三") + private String nickname; + + @Email(message = "邮箱格式不正确") + @Schema(description = "邮箱", example = "user@example.com") + private String email; + + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + @Schema(description = "手机号", example = "13800138000") + private String phone; + + @Schema(description = "头像URL", example = "https://example.com/avatar.jpg") + private String avatar; + + @Schema(description = "性别", example = "1", allowableValues = {"0", "1", "2"}) + private Integer gender; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/UserProfileDTO.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/UserProfileDTO.java new file mode 100644 index 0000000..e1d686f --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/dto/UserProfileDTO.java @@ -0,0 +1,54 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "用户资料DTO") +public class UserProfileDTO { + + @Schema(description = "用户ID", example = "1") + private Long userId; + + @Schema(description = "用户名", example = "admin") + private String username; + + @Schema(description = "昵称", example = "管理员") + private String nickname; + + @Schema(description = "邮箱", example = "admin@example.com") + private String email; + + @Schema(description = "手机号", example = "13800138000") + private String phone; + + @Schema(description = "头像URL", example = "https://example.com/avatar.jpg") + private String avatar; + + @Schema(description = "性别", example = "1", allowableValues = {"0", "1", "2"}) + private Integer gender; + + @Schema(description = "状态", example = "1", allowableValues = {"0", "1"}) + private Integer status; + + @Schema(description = "角色列表") + private List roles; + + @Schema(description = "权限列表") + private List permissions; + + @Schema(description = "创建时间", example = "2024-01-01T10:00:00") + private LocalDateTime createTime; + + @Schema(description = "最后登录时间", example = "2024-01-01T10:00:00") + private LocalDateTime lastLoginTime; +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/entity/User.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/entity/User.java new file mode 100644 index 0000000..ad66abc --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/entity/User.java @@ -0,0 +1,59 @@ +package top.lrshuai.jwt.entity; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +@Data +public class User { + + private Long userId; + private String username; + private String password; + private String nickname; + private String email; + private String phone; + private String avatar; + private Integer gender; + private Integer status; + private LocalDateTime createTime; + private LocalDateTime lastLoginTime; + private List roles; + private List permissions; + + public static User getAdmin() { + User user = new User(); + user.setUserId(1L); + user.setUsername("admin"); + user.setPassword("$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5E2"); + user.setNickname("超级管理员"); + user.setEmail("admin@example.com"); + user.setPhone("13800138000"); + user.setGender(1); + user.setStatus(1); + user.setCreateTime(LocalDateTime.now()); + user.setLastLoginTime(LocalDateTime.now()); + user.setRoles(Arrays.asList("ROLE_ADMIN", "ROLE_USER")); + user.setPermissions(Arrays.asList("*:*")); + return user; + } + + public static User getTestUser() { + User user = new User(); + user.setUserId(2L); + user.setUsername("user"); + user.setPassword("$2a$10$N.zmdr9k7uOCQb376NoUnuTJ8iAt6Z5EHsM8lE9lBOsl7iAt6Z5E2"); + user.setNickname("测试用户"); + user.setEmail("user@example.com"); + user.setPhone("13800138001"); + user.setGender(1); + user.setStatus(1); + user.setCreateTime(LocalDateTime.now()); + user.setLastLoginTime(LocalDateTime.now()); + user.setRoles(Arrays.asList("ROLE_USER")); + user.setPermissions(Arrays.asList("user:read", "user:update")); + return user; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/exception/BusinessException.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/exception/BusinessException.java new file mode 100644 index 0000000..d39500e --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/exception/BusinessException.java @@ -0,0 +1,30 @@ +package top.lrshuai.jwt.exception; + +import lombok.Getter; +import top.lrshuai.jwt.common.ResultCode; + +@Getter +public class BusinessException extends RuntimeException { + + private final Integer code; + + public BusinessException(String message) { + super(message); + this.code = ResultCode.ERROR.getCode(); + } + + public BusinessException(ResultCode resultCode) { + super(resultCode.getMessage()); + this.code = resultCode.getCode(); + } + + public BusinessException(ResultCode resultCode, String message) { + super(message); + this.code = resultCode.getCode(); + } + + public BusinessException(Integer code, String message) { + super(message); + this.code = code; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/exception/GlobalExceptionHandler.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..33e756d --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/exception/GlobalExceptionHandler.java @@ -0,0 +1,62 @@ +package top.lrshuai.jwt.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +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.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import top.lrshuai.jwt.common.Result; +import top.lrshuai.jwt.common.ResultCode; + +import javax.servlet.http.HttpServletRequest; +import java.util.stream.Collectors; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.OK) + public Result handleBusinessException(BusinessException e, HttpServletRequest request) { + log.warn("业务异常: {}, 路径: {}", e.getMessage(), request.getRequestURI()); + Result result = Result.error(e.getCode(), e.getMessage()); + result.setPath(request.getRequestURI()); + return result; + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.OK) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) { + String message = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.warn("参数校验失败: {}, 路径: {}", message, request.getRequestURI()); + Result result = Result.error(ResultCode.PARAM_ERROR.getCode(), message); + result.setPath(request.getRequestURI()); + return result; + } + + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.OK) + public Result handleBindException(BindException e, HttpServletRequest request) { + String message = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.warn("参数绑定失败: {}, 路径: {}", message, request.getRequestURI()); + Result result = Result.error(ResultCode.PARAM_ERROR.getCode(), message); + result.setPath(request.getRequestURI()); + return result; + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.OK) + public Result handleException(Exception e, HttpServletRequest request) { + log.error("系统异常: {}, 路径: {}", e.getMessage(), request.getRequestURI(), e); + Result result = Result.error(ResultCode.ERROR); + result.setPath(request.getRequestURI()); + return result; + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/interceptor/JwtInterceptor.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/interceptor/JwtInterceptor.java new file mode 100644 index 0000000..1cb84aa --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/interceptor/JwtInterceptor.java @@ -0,0 +1,67 @@ +package top.lrshuai.jwt.interceptor; + +import io.jsonwebtoken.Claims; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import top.lrshuai.jwt.common.ResultCode; +import top.lrshuai.jwt.exception.BusinessException; +import top.lrshuai.jwt.util.JwtUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class JwtInterceptor implements HandlerInterceptor { + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private StringRedisTemplate redisTemplate; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String token = extractTokenFromRequest(request); + + if (token == null) { + throw new BusinessException(ResultCode.UNAUTHORIZED, "未提供认证Token"); + } + + if (isTokenBlacklisted(token)) { + throw new BusinessException(ResultCode.TOKEN_REVOKED, "Token已被吊销"); + } + + try { + Claims claims = jwtUtils.parseToken(token); + Long userId = claims.get("userId", Long.class); + String username = claims.get("username", String.class); + + request.setAttribute("userId", userId); + request.setAttribute("username", username); + request.setAttribute("claims", claims); + + return true; + } catch (Exception e) { + log.warn("Token验证失败: {}", e.getMessage()); + throw new BusinessException(ResultCode.TOKEN_INVALID, "Token无效或已过期"); + } + } + + private String extractTokenFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + private boolean isTokenBlacklisted(String token) { + Boolean isMember = redisTemplate.opsForSet().isMember("token:blacklist", token); + return Boolean.TRUE.equals(isMember); + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/service/AuthService.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/service/AuthService.java new file mode 100644 index 0000000..38bf37e --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/service/AuthService.java @@ -0,0 +1,263 @@ +package top.lrshuai.jwt.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import top.lrshuai.jwt.common.ResultCode; +import top.lrshuai.jwt.dto.*; +import top.lrshuai.jwt.entity.User; +import top.lrshuai.jwt.exception.BusinessException; +import top.lrshuai.jwt.util.JwtUtils; + +import javax.annotation.PostConstruct; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Slf4j +@Service +public class AuthService { + + @Autowired + private JwtUtils jwtUtils; + + @Autowired + private StringRedisTemplate redisTemplate; + + private final Map userStore = new ConcurrentHashMap<>(); + private final AtomicLong userIdGenerator = new AtomicLong(2); + + @PostConstruct + public void init() { + User admin = User.getAdmin(); + User testUser = User.getTestUser(); + userStore.put(admin.getUsername(), admin); + userStore.put(testUser.getUsername(), testUser); + } + + public LoginResponse login(LoginRequest request) { + User user = userStore.get(request.getUsername()); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + + if (user.getStatus() == 0) { + throw new BusinessException(ResultCode.ACCOUNT_DISABLED); + } + + if (!request.getPassword().equals(user.getPassword()) && + !verifyPassword(request.getPassword(), user.getPassword())) { + throw new BusinessException(ResultCode.PASSWORD_ERROR); + } + + user.setLastLoginTime(LocalDateTime.now()); + + TokenResponse tokenResponse = jwtUtils.generateTokenPair(user); + + String tokenKey = "token:" + user.getUserId(); + redisTemplate.opsForValue().set(tokenKey + ":access", tokenResponse.getAccessToken(), + tokenResponse.getExpiresIn(), TimeUnit.SECONDS); + redisTemplate.opsForValue().set(tokenKey + ":refresh", tokenResponse.getRefreshToken(), + 7, TimeUnit.DAYS); + + UserProfileDTO userProfile = convertToProfileDTO(user); + + return LoginResponse.builder() + .user(userProfile) + .token(tokenResponse) + .build(); + } + + public void register(RegisterRequest request) { + if (!request.getPassword().equals(request.getConfirmPassword())) { + throw new BusinessException(ResultCode.PARAM_ERROR, "两次输入的密码不一致"); + } + + if (userStore.containsKey(request.getUsername())) { + throw new BusinessException(ResultCode.USERNAME_EXISTS); + } + + if (request.getEmail() != null && !request.getEmail().isEmpty()) { + boolean emailExists = userStore.values().stream() + .anyMatch(u -> request.getEmail().equals(u.getEmail())); + if (emailExists) { + throw new BusinessException(ResultCode.EMAIL_EXISTS); + } + } + + if (request.getPhone() != null && !request.getPhone().isEmpty()) { + boolean phoneExists = userStore.values().stream() + .anyMatch(u -> request.getPhone().equals(u.getPhone())); + if (phoneExists) { + throw new BusinessException(ResultCode.PHONE_EXISTS); + } + } + + User user = new User(); + user.setUserId(userIdGenerator.incrementAndGet()); + user.setUsername(request.getUsername()); + user.setPassword(request.getPassword()); + user.setNickname(request.getNickname() != null ? request.getNickname() : request.getUsername()); + user.setEmail(request.getEmail()); + user.setPhone(request.getPhone()); + user.setGender(0); + user.setStatus(1); + user.setCreateTime(LocalDateTime.now()); + user.setRoles(Arrays.asList("ROLE_USER")); + user.setPermissions(Arrays.asList("user:read", "user:update")); + + userStore.put(user.getUsername(), user); + log.info("用户注册成功: {}", user.getUsername()); + } + + public void logout(Long userId, String token) { + String tokenKey = "token:" + userId; + redisTemplate.delete(tokenKey + ":access"); + redisTemplate.delete(tokenKey + ":refresh"); + + if (token != null) { + redisTemplate.opsForSet().add("token:blacklist", token); + redisTemplate.expire("token:blacklist", 7, TimeUnit.DAYS); + } + + log.info("用户登出成功: userId={}", userId); + } + + public TokenResponse refreshToken(RefreshTokenRequest request) { + if (!jwtUtils.validateToken(request.getRefreshToken())) { + throw new BusinessException(ResultCode.REFRESH_TOKEN_EXPIRED); + } + + Long userId = jwtUtils.getUserIdFromToken(request.getRefreshToken()); + User user = findUserById(userId); + + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + + String tokenKey = "token:" + userId; + String storedRefreshToken = redisTemplate.opsForValue().get(tokenKey + ":refresh"); + + if (storedRefreshToken == null || !storedRefreshToken.equals(request.getRefreshToken())) { + throw new BusinessException(ResultCode.TOKEN_INVALID, "Refresh Token无效或已过期"); + } + + TokenResponse newTokenPair = jwtUtils.generateTokenPair(user); + + redisTemplate.opsForValue().set(tokenKey + ":access", newTokenPair.getAccessToken(), + newTokenPair.getExpiresIn(), TimeUnit.SECONDS); + redisTemplate.opsForValue().set(tokenKey + ":refresh", newTokenPair.getRefreshToken(), + 7, TimeUnit.DAYS); + + return newTokenPair; + } + + public User findUserById(Long userId) { + return userStore.values().stream() + .filter(u -> u.getUserId().equals(userId)) + .findFirst() + .orElse(null); + } + + public User findUserByUsername(String username) { + return userStore.get(username); + } + + public UserProfileDTO getUserProfile(Long userId) { + User user = findUserById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + return convertToProfileDTO(user); + } + + public UserProfileDTO updateUserProfile(Long userId, UpdateProfileRequest request) { + User user = findUserById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + + if (request.getNickname() != null) { + user.setNickname(request.getNickname()); + } + if (request.getEmail() != null) { + user.setEmail(request.getEmail()); + } + if (request.getPhone() != null) { + user.setPhone(request.getPhone()); + } + if (request.getAvatar() != null) { + user.setAvatar(request.getAvatar()); + } + if (request.getGender() != null) { + user.setGender(request.getGender()); + } + + return convertToProfileDTO(user); + } + + public void changePassword(Long userId, ChangePasswordRequest request) { + User user = findUserById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + + if (!request.getNewPassword().equals(request.getConfirmPassword())) { + throw new BusinessException(ResultCode.PARAM_ERROR, "两次输入的新密码不一致"); + } + + if (!request.getOldPassword().equals(user.getPassword()) && + !verifyPassword(request.getOldPassword(), user.getPassword())) { + throw new BusinessException(ResultCode.PASSWORD_ERROR, "旧密码错误"); + } + + user.setPassword(request.getNewPassword()); + + String tokenKey = "token:" + userId; + redisTemplate.delete(tokenKey + ":access"); + redisTemplate.delete(tokenKey + ":refresh"); + } + + public List getUserPermissions(Long userId) { + User user = findUserById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + return user.getPermissions() != null ? user.getPermissions() : new ArrayList<>(); + } + + public List getUserRoles(Long userId) { + User user = findUserById(userId); + if (user == null) { + throw new BusinessException(ResultCode.USER_NOT_FOUND); + } + return user.getRoles() != null ? user.getRoles() : new ArrayList<>(); + } + + private boolean verifyPassword(String inputPassword, String storedPassword) { + return inputPassword.equals(storedPassword); + } + + private UserProfileDTO convertToProfileDTO(User user) { + return UserProfileDTO.builder() + .userId(user.getUserId()) + .username(user.getUsername()) + .nickname(user.getNickname()) + .email(user.getEmail()) + .phone(user.getPhone()) + .avatar(user.getAvatar()) + .gender(user.getGender()) + .status(user.getStatus()) + .roles(user.getRoles()) + .permissions(user.getPermissions()) + .createTime(user.getCreateTime()) + .lastLoginTime(user.getLastLoginTime()) + .build(); + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/util/JwtUtils.java b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/util/JwtUtils.java new file mode 100644 index 0000000..95435fb --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/java/top/lrshuai/jwt/util/JwtUtils.java @@ -0,0 +1,190 @@ +package top.lrshuai.jwt.util; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import top.lrshuai.jwt.config.JwtProperties; +import top.lrshuai.jwt.dto.TokenParseResponse; +import top.lrshuai.jwt.dto.TokenResponse; +import top.lrshuai.jwt.entity.User; + +import javax.annotation.PostConstruct; +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Slf4j +@Component +public class JwtUtils { + + @Autowired + private JwtProperties jwtProperties; + + private SecretKey secretKey; + + @PostConstruct + public void init() { + this.secretKey = Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8)); + } + + public TokenResponse generateTokenPair(User user) { + String accessToken = generateAccessToken(user); + String refreshToken = generateRefreshToken(user); + + LocalDateTime accessExpireTime = LocalDateTime.now().plusSeconds(jwtProperties.getAccessTokenExpire()); + LocalDateTime refreshExpireTime = LocalDateTime.now().plusSeconds(jwtProperties.getRefreshTokenExpire()); + + return TokenResponse.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .tokenType("Bearer") + .expiresIn(jwtProperties.getAccessTokenExpire()) + .expireTime(accessExpireTime) + .refreshExpireTime(refreshExpireTime) + .build(); + } + + public String generateAccessToken(User user) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + jwtProperties.getAccessTokenExpire() * 1000); + + Map claims = new HashMap<>(); + claims.put("userId", user.getUserId()); + claims.put("username", user.getUsername()); + claims.put("roles", user.getRoles()); + claims.put("permissions", user.getPermissions()); + claims.put("type", "access"); + + return Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuer(jwtProperties.getIssuer()) + .setAudience(jwtProperties.getAudience()) + .setSubject(user.getUserId().toString()) + .setIssuedAt(now) + .setExpiration(expiration) + .setNotBefore(now) + .addClaims(claims) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public String generateRefreshToken(User user) { + Date now = new Date(); + Date expiration = new Date(now.getTime() + jwtProperties.getRefreshTokenExpire() * 1000); + + Map claims = new HashMap<>(); + claims.put("userId", user.getUserId()); + claims.put("type", "refresh"); + + return Jwts.builder() + .setId(UUID.randomUUID().toString()) + .setIssuer(jwtProperties.getIssuer()) + .setAudience(jwtProperties.getAudience()) + .setSubject(user.getUserId().toString()) + .setIssuedAt(now) + .setExpiration(expiration) + .setNotBefore(now) + .addClaims(claims) + .signWith(secretKey, SignatureAlgorithm.HS256) + .compact(); + } + + public Claims parseToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public boolean validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(token); + return true; + } catch (ExpiredJwtException e) { + log.warn("Token已过期: {}", e.getMessage()); + return false; + } catch (UnsupportedJwtException e) { + log.warn("不支持的Token: {}", e.getMessage()); + return false; + } catch (MalformedJwtException e) { + log.warn("Token格式错误: {}", e.getMessage()); + return false; + } catch (SignatureException e) { + log.warn("Token签名验证失败: {}", e.getMessage()); + return false; + } catch (IllegalArgumentException e) { + log.warn("Token为空或非法: {}", e.getMessage()); + return false; + } + } + + public TokenParseResponse parseTokenDetails(String token) { + try { + Claims claims = parseToken(token); + return TokenParseResponse.builder() + .id(claims.getId()) + .issuer(claims.getIssuer()) + .subject(claims.getSubject()) + .audience(claims.getAudience()) + .issuedAt(convertToLocalDateTime(claims.getIssuedAt())) + .expiration(convertToLocalDateTime(claims.getExpiration())) + .notBefore(convertToLocalDateTime(claims.getNotBefore())) + .userId(claims.get("userId", Long.class)) + .username(claims.get("username", String.class)) + .claims(new HashMap<>(claims)) + .valid(true) + .message("Token验证通过") + .build(); + } catch (ExpiredJwtException e) { + return TokenParseResponse.builder() + .valid(false) + .message("Token已过期: " + e.getMessage()) + .build(); + } catch (Exception e) { + return TokenParseResponse.builder() + .valid(false) + .message("Token验证失败: " + e.getMessage()) + .build(); + } + } + + public Long getUserIdFromToken(String token) { + Claims claims = parseToken(token); + return claims.get("userId", Long.class); + } + + public String getUsernameFromToken(String token) { + Claims claims = parseToken(token); + return claims.get("username", String.class); + } + + public boolean isTokenExpired(String token) { + try { + Claims claims = parseToken(token); + return claims.getExpiration().before(new Date()); + } catch (ExpiredJwtException e) { + return true; + } + } + + public Date getExpirationDateFromToken(String token) { + Claims claims = parseToken(token); + return claims.getExpiration(); + } + + private LocalDateTime convertToLocalDateTime(Date date) { + if (date == null) return null; + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + } +} diff --git a/SpringBoot-JWT -kimi-update/src/main/resources/application-dev.yml b/SpringBoot-JWT -kimi-update/src/main/resources/application-dev.yml new file mode 100644 index 0000000..127205d --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/resources/application-dev.yml @@ -0,0 +1,2 @@ +server: + port: 8099 diff --git a/SpringBoot-JWT -kimi-update/src/main/resources/application.yml b/SpringBoot-JWT -kimi-update/src/main/resources/application.yml new file mode 100644 index 0000000..26e74f9 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/main/resources/application.yml @@ -0,0 +1,44 @@ +server: + port: 8080 + servlet: + context-path: / + +spring: + application: + name: jwt-auth-system + redis: + host: localhost + port: 6379 + password: + database: 0 + timeout: 3000ms + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + max-wait: -1ms + +jwt: + secret: your-256-bit-secret-key-for-jwt-signing-must-be-at-least-32-characters-long + issuer: jwt-system + audience: web-app + access-token-expire: 7200 + refresh-token-expire: 604800 + token-prefix: "Bearer " + header-name: Authorization + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui.html + operationsSorter: method + tagsSorter: alpha + +logging: + level: + root: INFO + top.lrshuai.jwt: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" diff --git a/SpringBoot-JWT -kimi-update/src/test/java/top/lrshuai/jwt/JwtApplicationTests.java b/SpringBoot-JWT -kimi-update/src/test/java/top/lrshuai/jwt/JwtApplicationTests.java new file mode 100644 index 0000000..20da597 --- /dev/null +++ b/SpringBoot-JWT -kimi-update/src/test/java/top/lrshuai/jwt/JwtApplicationTests.java @@ -0,0 +1,75 @@ +package top.lrshuai.jwt; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.Claim; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; +import top.lrshuai.jwt.entity.RSA256Key; +import top.lrshuai.jwt.entity.User; +import top.lrshuai.jwt.util.CreateSecrteKey; +import top.lrshuai.jwt.util.DateUtils; + +import java.util.Calendar; +import java.util.Date; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class JwtApplicationTests { + + + @Test + public void test() { + Algorithm algorithm = Algorithm.HMAC256("rstyro"); + String token = JWT.create() + .withIssuer("rstyro") + .sign(algorithm); + System.out.println("token="+token); + } + + @Test + public void testRS256() throws Exception { + String[] audience = {"app","web"}; + RSA256Key rsa256Key = CreateSecrteKey.getRSA256Key(); + Algorithm algorithm = Algorithm.RSA256(rsa256Key.getPublicKey(), rsa256Key.getPrivateKey()); + User user = new User(); + user.setUserId(1l); + user.setAge(24); + user.setSex(1); + user.setUsername("rstyro"); + String token = JWT.create() + .withIssuer("rstyro") //发布者 + .withSubject("test") //主题 + .withAudience(audience) //观众,相当于接受者 + .withIssuedAt(new Date()) // 生成签名的时间 + .withExpiresAt(DateUtils.offset(new Date(),1, Calendar.SECOND)) // 生成签名的有效期 + .withClaim("data", JSON.toJSONString(user)) //存数据 + .withClaim("other", "this is a message") //存数据 + .withClaim("file", "这是一个文件,存数据用这个claim,如果有多的数据,用 withArrayClaim() 方法") //存数据 + .sign(algorithm); + System.out.println("token="+token); + System.out.println("public="+CreateSecrteKey.getPublicKey(rsa256Key)); + System.out.println("private="+CreateSecrteKey.getPrivateKey(rsa256Key)); + + Thread.sleep(1000*3); + + Algorithm veifierAlgorithm = Algorithm.RSA256(rsa256Key.getPublicKey(), null); + JWTVerifier verifier = JWT.require(veifierAlgorithm) + .withIssuer("rstyro") + .build(); //Reusable verifier instance + DecodedJWT jwt = verifier.verify(token); + Claim data = jwt.getClaim("data"); + String userstring = data.asString(); + JSONObject object = JSON.parseObject(userstring); + User user1 = JSON.parseObject(userstring, User.class); + System.out.println("user1="+user1); + + } + +} diff --git a/SpringBoot-JWT/pom.xml b/SpringBoot-JWT/pom.xml index dbd5e7a..93fd8e7 100644 --- a/SpringBoot-JWT/pom.xml +++ b/SpringBoot-JWT/pom.xml @@ -66,6 +66,11 @@ spring-boot-starter-test test + + + org.springframework.boot + spring-boot-starter-validation + diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/common/ApiResultEnum.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/common/ApiResultEnum.java index e1dc87e..63db088 100644 --- a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/common/ApiResultEnum.java +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/common/ApiResultEnum.java @@ -16,9 +16,11 @@ public enum ApiResultEnum { - TOKEN_EXPIRED("10001","token 过期"), - SIGN_VERIFI_ERROR("10002","签名不匹配"), - ALGORITHM_CAN_NOT_NULL("10003","加密方式不能为空,可选 RS256、HS256"), + TOKEN_EXPIRED("10001","Token已过期"), + SIGN_VERIFI_ERROR("10002","签名验证失败"), + ALGORITHM_CAN_NOT_NULL("10003","加密算法不能为空,可选: HS256、RS256"), + ALGORITHM_NOT_SUPPORT("10004","不支持的加密算法,仅支持: HS256、RS256"), + PARAM_ERROR("10005","参数校验失败"), diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/config/GlobalExceptionHandler.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/config/GlobalExceptionHandler.java index c068a4e..08186cf 100644 --- a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/config/GlobalExceptionHandler.java +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/config/GlobalExceptionHandler.java @@ -2,7 +2,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.validation.FieldError; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import top.lrshuai.jwt.common.ApiException; @@ -10,17 +12,22 @@ import top.lrshuai.jwt.common.Result; import java.io.IOException; +import java.util.stream.Collectors; -/** - * 全局异常捕获 - * @author rstyro - * @since 2019-03-12 - */ @RestControllerAdvice public class GlobalExceptionHandler { private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result handleValidationException(MethodArgumentNotValidException ex) { + logger.error(ex.getMessage(), ex); + String errorMessage = ex.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + return Result.error(ApiResultEnum.PARAM_ERROR.getStatus(), errorMessage); + } + @ExceptionHandler(NullPointerException.class) public Result NullPointer(NullPointerException ex){ logger.error(ex.getMessage(),ex); diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/controller/AuthController.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/controller/AuthController.java new file mode 100644 index 0000000..6776cc1 --- /dev/null +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/controller/AuthController.java @@ -0,0 +1,42 @@ +package top.lrshuai.jwt.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import top.lrshuai.jwt.common.Result; +import top.lrshuai.jwt.dto.TokenGenerateRequest; +import top.lrshuai.jwt.dto.TokenVerifyRequest; +import top.lrshuai.jwt.entity.User; +import top.lrshuai.jwt.service.JwtService; + +import javax.validation.Valid; + +@RestController +@RequestMapping("/api/auth") +@Api(tags = "JWT认证接口") +@Validated +public class AuthController { + + private final JwtService jwtService; + + public AuthController(JwtService jwtService) { + this.jwtService = jwtService; + } + + @PostMapping("/token/generate") + @ApiOperation(value = "生成JWT Token", notes = "支持HS256和RS256两种加密算法,可自定义用户信息和过期时间") + public Result generateToken(@Valid @RequestBody TokenGenerateRequest request) throws Exception { + return Result.ok(jwtService.generateToken(request)); + } + + @PostMapping("/token/verify") + @ApiOperation(value = "验证JWT Token", notes = "验证Token有效性并返回解析后的用户数据") + public Result verifyToken(@Valid @RequestBody TokenVerifyRequest request) throws Exception { + User user = jwtService.verifyToken(request); + return Result.ok(user); + } +} diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenGenerateRequest.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenGenerateRequest.java new file mode 100644 index 0000000..3e62fd8 --- /dev/null +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenGenerateRequest.java @@ -0,0 +1,29 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +@ApiModel(description = "Token生成请求参数") +public class TokenGenerateRequest { + + @ApiModelProperty(value = "加密算法类型", required = true, example = "HS256", allowableValues = "HS256,RS256") + @NotBlank(message = "加密算法类型不能为空") + private String algorithm; + + @ApiModelProperty(value = "用户ID", example = "1") + private Long userId; + + @ApiModelProperty(value = "用户名", example = "admin") + private String username; + + @ApiModelProperty(value = "过期时间(秒),不传则使用默认值7200秒", example = "7200") + private Long expireSeconds; + + @ApiModelProperty(value = "是否生成短期token(20秒)", example = "false") + private Boolean shortLived; +} diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenResponse.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenResponse.java new file mode 100644 index 0000000..bf51c67 --- /dev/null +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenResponse.java @@ -0,0 +1,28 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ApiModel(description = "Token响应结果") +public class TokenResponse { + + @ApiModelProperty(value = "JWT Token字符串") + private String token; + + @ApiModelProperty(value = "加密算法类型") + private String algorithm; + + @ApiModelProperty(value = "过期时间(时间戳)") + private Long expireTime; + + @ApiModelProperty(value = "过期时间(格式化字符串)") + private String expireTimeStr; +} diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenVerifyRequest.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenVerifyRequest.java new file mode 100644 index 0000000..691c114 --- /dev/null +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/dto/TokenVerifyRequest.java @@ -0,0 +1,20 @@ +package top.lrshuai.jwt.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@ApiModel(description = "Token验证请求参数") +public class TokenVerifyRequest { + + @ApiModelProperty(value = "JWT Token", required = true, example = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...") + @NotBlank(message = "Token不能为空") + private String token; + + @ApiModelProperty(value = "加密算法类型", required = true, example = "HS256", allowableValues = "HS256,RS256") + @NotBlank(message = "加密算法类型不能为空") + private String algorithm; +} diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/service/JwtService.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/service/JwtService.java new file mode 100644 index 0000000..bf76f1e --- /dev/null +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/service/JwtService.java @@ -0,0 +1,136 @@ +package top.lrshuai.jwt.service; + +import com.alibaba.fastjson.JSON; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.exceptions.TokenExpiredException; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.springframework.stereotype.Service; +import top.lrshuai.jwt.common.ApiException; +import top.lrshuai.jwt.common.ApiResultEnum; +import top.lrshuai.jwt.dto.TokenGenerateRequest; +import top.lrshuai.jwt.dto.TokenResponse; +import top.lrshuai.jwt.dto.TokenVerifyRequest; +import top.lrshuai.jwt.entity.RSA256Key; +import top.lrshuai.jwt.entity.User; +import top.lrshuai.jwt.util.CreateSecrteKey; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.UUID; + +@Service +public class JwtService { + + private static final String SECRET = "rstyro"; + private static final String ISSUER = "rstyro"; + private static final String SUBJECT = "test"; + private static final long DEFAULT_EXPIRE_SECONDS = 7200L; + private static final long SHORT_LIVED_EXPIRE_SECONDS = 20L; + private static final ThreadLocal DATE_FORMAT = ThreadLocal.withInitial( + () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") + ); + + public TokenResponse generateToken(TokenGenerateRequest request) throws Exception { + Algorithm algorithm = getAlgorithm(request.getAlgorithm()); + User userData = buildUserData(request); + long expireSeconds = getExpireSeconds(request); + Date expireDate = calculateExpireDate(expireSeconds); + + String token = JWT.create() + .withIssuer(ISSUER) + .withSubject(SUBJECT) + .withIssuedAt(new Date()) + .withExpiresAt(expireDate) + .withClaim("data", JSON.toJSONString(userData)) + .withNotBefore(new Date()) + .withJWTId(UUID.randomUUID().toString()) + .sign(algorithm); + + return TokenResponse.builder() + .token(token) + .algorithm(request.getAlgorithm().toUpperCase()) + .expireTime(expireDate.getTime()) + .expireTimeStr(DATE_FORMAT.get().format(expireDate)) + .build(); + } + + public User verifyToken(TokenVerifyRequest request) throws Exception { + Algorithm algorithm = getVerifyAlgorithm(request.getAlgorithm()); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(ISSUER) + .build(); + + try { + DecodedJWT decodedJWT = verifier.verify(request.getToken()); + String dataString = decodedJWT.getClaim("data").asString(); + return JSON.parseObject(dataString, User.class); + } catch (TokenExpiredException ex) { + throw new ApiException(ApiResultEnum.TOKEN_EXPIRED); + } catch (JWTVerificationException ex) { + throw new ApiException(ApiResultEnum.SIGN_VERIFI_ERROR); + } + } + + private Algorithm getAlgorithm(String algorithmType) throws Exception { + if (algorithmType == null) { + throw new ApiException(ApiResultEnum.ALGORITHM_CAN_NOT_NULL); + } + + String upperAlg = algorithmType.toUpperCase(); + switch (upperAlg) { + case "HS256": + return Algorithm.HMAC256(SECRET); + case "RS256": + RSA256Key rsa256Key = CreateSecrteKey.getRSA256Key(); + return Algorithm.RSA256(rsa256Key.getPublicKey(), rsa256Key.getPrivateKey()); + default: + throw new ApiException(ApiResultEnum.ALGORITHM_NOT_SUPPORT); + } + } + + private Algorithm getVerifyAlgorithm(String algorithmType) throws Exception { + if (algorithmType == null) { + throw new ApiException(ApiResultEnum.ALGORITHM_CAN_NOT_NULL); + } + + String upperAlg = algorithmType.toUpperCase(); + switch (upperAlg) { + case "HS256": + return Algorithm.HMAC256(SECRET); + case "RS256": + RSA256Key rsa256Key = CreateSecrteKey.getRSA256Key(); + return Algorithm.RSA256(rsa256Key.getPublicKey(), null); + default: + throw new ApiException(ApiResultEnum.ALGORITHM_NOT_SUPPORT); + } + } + + private User buildUserData(TokenGenerateRequest request) { + if (request.getUserId() != null || request.getUsername() != null) { + User user = new User(); + user.setUserId(request.getUserId() != null ? request.getUserId() : 1L); + user.setUsername(request.getUsername() != null ? request.getUsername() : "admin"); + user.setAge(24); + user.setSex(1); + return user; + } + return User.getAuther(); + } + + private long getExpireSeconds(TokenGenerateRequest request) { + if (Boolean.TRUE.equals(request.getShortLived())) { + return SHORT_LIVED_EXPIRE_SECONDS; + } + return request.getExpireSeconds() != null ? request.getExpireSeconds() : DEFAULT_EXPIRE_SECONDS; + } + + private Date calculateExpireDate(long expireSeconds) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.SECOND, (int) expireSeconds); + return calendar.getTime(); + } +} diff --git a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/util/CreateSecrteKey.java b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/util/CreateSecrteKey.java index 0c6efc2..064408b 100644 --- a/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/util/CreateSecrteKey.java +++ b/SpringBoot-JWT/src/main/java/top/lrshuai/jwt/util/CreateSecrteKey.java @@ -21,7 +21,8 @@ public class CreateSecrteKey { private static final String PUBLIC_KEY = "RSAPublicKey"; private static final String PRIVATE_KEY = "RSAPrivateKey"; - private static RSA256Key rsa256Key; + // 使用饿汉式单例,确保密钥在类加载时就生成且只生成一次 + private static final RSA256Key rsa256Key = createRSA256Key(); //获得公钥 public static String getPublicKey(Map keyMap) throws Exception { @@ -85,22 +86,28 @@ public static Map initKey() throws Exception { return keyMap; } + /** + * 创建RSA256密钥对(在类加载时调用一次) + * @return RSA256Key对象 + */ + private static RSA256Key createRSA256Key() { + try { + RSA256Key key = new RSA256Key(); + Map map = initKey(); + key.setPrivateKey((RSAPrivateKey) map.get(CreateSecrteKey.PRIVATE_KEY)); + key.setPublicKey((RSAPublicKey) map.get(CreateSecrteKey.PUBLIC_KEY)); + return key; + } catch (Exception e) { + throw new RuntimeException("Failed to create RSA256 key pair", e); + } + } + /** * 获取公私钥 * @return * @throws Exception */ - public static synchronized RSA256Key getRSA256Key() throws Exception { - if(rsa256Key == null){ - synchronized (RSA256Key.class){ - if(rsa256Key == null) { - rsa256Key = new RSA256Key(); - Map map = initKey(); - rsa256Key.setPrivateKey((RSAPrivateKey) map.get(CreateSecrteKey.PRIVATE_KEY)); - rsa256Key.setPublicKey((RSAPublicKey) map.get(CreateSecrteKey.PUBLIC_KEY)); - } - } - } + public static RSA256Key getRSA256Key() throws Exception { return rsa256Key; } diff --git a/SpringBoot-JWT/src/main/resources/application-dev.yml b/SpringBoot-JWT/src/main/resources/application-dev.yml index 00cff46..127205d 100644 --- a/SpringBoot-JWT/src/main/resources/application-dev.yml +++ b/SpringBoot-JWT/src/main/resources/application-dev.yml @@ -1,2 +1,2 @@ server: - port: 8000 + port: 8099