From 59ada911cf778363516146f6e415022f94b1a6a8 Mon Sep 17 00:00:00 2001 From: little_jin <861165942@qq.com> Date: Tue, 16 Sep 2025 22:55:34 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E7=A7=BB=E6=A4=8D=E4=BA=86SpringSecurity?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=88=B0=E5=85=B6=E4=B8=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/controller/UserController.java | 109 ---- .../user/adapter/in/web/dto/LoginRequest.java | 47 ++ .../adapter/in/web/dto/LoginResponse.java | 59 ++ .../adapter/in/web/dto/RegisterRequest.java | 99 ++++ .../user/adapter/in/web/dto/Result.java | 149 +++++ .../user/adapter/in/web/dto/UserInfo.java | 102 ++++ .../in/web/dto/UserLoginRequestDTO.java | 14 - .../web/exception/GlobalExceptionHandler.java | 259 +++++++++ .../user-adapter-out-persistence/pom.xml | 6 + .../bridge/GetUserByNameBridge.java | 2 +- .../persistence/convertor/UserConvertor.java | 11 +- .../out/persistence/entity/UserEntity.java | 122 ++++- .../out/persistence/mapper/UserMapper.java | 48 +- .../service/CustomUserDetailsService.java | 367 +++++++++++++ .../service/TokenBlacklistService.java | 204 +++++++ .../application/service/UserLoginService.java | 4 +- .../security/config/BasicSecurityConfig.java | 181 ++++++ .../security/controller/UserController.java | 513 ++++++++++++++++++ .../filter/JwtAuthenticationFilter.java | 351 ++++++++++++ user-service/user-service-common/pom.xml | 18 + .../example/user/service/common/JwtUtil.java | 367 +++++++++++-- user-service/user-service-domain/pom.xml | 11 + .../com/example/user/service/domain/User.java | 22 +- .../service/domain/config/PasswordConfig.java | 46 +- .../domain/valueobject/CreateTime.java | 9 + .../service/domain/valueobject/IsSuper.java | 20 - .../domain/valueobject/LastLoginTime.java | 7 + .../service/domain/valueobject/Phone.java | 5 + .../service/domain/valueobject/RealName.java | 5 + .../user/service/domain/valueobject/Role.java | 5 + .../service/domain/valueobject/Status.java | 7 + .../domain/valueobject/UpdateTime.java | 7 + 32 files changed, 2941 insertions(+), 235 deletions(-) delete mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java delete mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java create mode 100644 user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java create mode 100644 user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java create mode 100644 user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java create mode 100644 user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java create mode 100644 user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java create mode 100644 user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java delete mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java create mode 100644 user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java deleted file mode 100644 index 96820c0..0000000 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/controller/UserController.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.example.user.adapter.in.web.controller; - -import com.example.user.adapter.in.web.dto.CreateUserRequestDTO; -import com.example.user.adapter.in.web.dto.UpdateUserRequestDTO; -import com.example.user.adapter.in.web.dto.UserLoginRequestDTO; -import com.example.user.adapter.in.web.dto.UserResponseDTO; -import com.example.user.service.application.command.CreateUserCommand; -import com.example.user.service.application.command.UpdateUserCommand; -import com.example.user.service.application.command.UserLoginCommand; -import com.example.user.service.application.port.in.*; -import com.example.user.service.domain.User; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -@Slf4j -@RequestMapping("/users") -@RestController -@RequiredArgsConstructor -public class UserController { - - private final GetUserListUseCase getUserListUseCase; - private final CreateUserUseCase createUserUseCase; - private final DeleteUserUseCase deleteUserUseCase; - private final UpdateUserUseCase updateUserUseCase; - private final GetUserByIdUseCase getUserByIdUseCase; - private final UserLoginUseCase userLoginUseCase; - - - @PostMapping("login") - public String login(@RequestBody UserLoginRequestDTO userLoginRequestDTO){ - log.info("UserLoginRequestDTO:{}",userLoginRequestDTO); - UserLoginCommand command=UserLoginCommand.builder() - .name(userLoginRequestDTO.getName()) - .password(userLoginRequestDTO.getPassword()) - .build(); - String token = userLoginUseCase.login(command); - return token; - } - - - - @GetMapping("") - public List getUsers() { - log.info("getUsers"); - return getUserListUseCase.getUsers(); - } - - /** - * 创建新用户 - * 功能:接收用户注册信息,验证密码一致性,创建新用户账户 - * @author dongxuanfeng - * @param createUserRequestDTO - * @return User - 成功创建的新用户 - * @throws IllegalArgumentException 当密码与确认密码不匹配时抛出此异常 - */ - @PostMapping() - public User createUser(@RequestBody CreateUserRequestDTO createUserRequestDTO){ - - if (!createUserRequestDTO.isPasswordValid()) { - throw new IllegalArgumentException("密码和确认密码不匹配"); - } - CreateUserCommand command=CreateUserCommand.builder() - .name(createUserRequestDTO.name()) - .age(createUserRequestDTO.age()) - .email(createUserRequestDTO.email()) - .password(createUserRequestDTO.password()) - .build(); - - return createUserUseCase.createUser(command); - } - - - @DeleteMapping("{id}") - public String deleteUser(@PathVariable("id") Long id){ - deleteUserUseCase.deleteUser(id); - return "success"; - } - - - @PutMapping("") - public User updateUser(@RequestBody UpdateUserRequestDTO updateUserRequestDTO){ - UpdateUserCommand command=UpdateUserCommand.builder() - .id(updateUserRequestDTO.id()) - .name(updateUserRequestDTO.name()) - .age(updateUserRequestDTO.age()) - .email(updateUserRequestDTO.email()) - .build(); - User user = updateUserUseCase.updateUser(command); - return user; - } - - - - @GetMapping("{id}") - public UserResponseDTO getUserById(@PathVariable("id") Long id){ - User user = getUserByIdUseCase.getUserById(id); - UserResponseDTO userResponseDTO = new UserResponseDTO( - user.getId().id(), - user.getName().username(), - user.getAge().age(), - user.getEmail().email(), - user.getIsSuper().value()); - return userResponseDTO; - } - -} diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java new file mode 100644 index 0000000..eb2ce38 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginRequest.java @@ -0,0 +1,47 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +/** + * 登录请求DTO (Data Transfer Object) + * 用于封装用户登录时提交的数据 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于接收用户登录请求的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class LoginRequest { + + /** + * 用户名字段 + * + * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空 + * @Size 验证注解,限制用户名长度在3-20个字符之间 + * message 属性定义了验证失败时的错误提示信息 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + private String username; + + /** + * 密码字段 + * + * @NotBlank 验证注解,确保密码不为空且去除空格后不为空 + * @Size 验证注解,限制密码长度在6-20个字符之间 + * message 属性定义了验证失败时的错误提示信息 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + private String password; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java new file mode 100644 index 0000000..8017dc9 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/LoginResponse.java @@ -0,0 +1,59 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 登录响应DTO (Data Transfer Object) + * 用于封装用户登录成功后的响应数据 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于返回用户登录成功后的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoginResponse { + + /** + * JWT访问令牌 + * + * 这是用户登录成功后生成的JWT Token + * 客户端在后续请求中需要在请求头中携带这个Token进行身份认证 + * 格式通常是: Authorization: Bearer + */ + private String accessToken; + + /** + * 令牌类型 + * + * 默认值为"Bearer",表示这是一个Bearer Token + * Bearer Token是一种HTTP认证方案,用于OAuth 2.0等认证协议 + */ + private String tokenType = "Bearer"; + + /** + * 用户信息 + * + * 包含登录用户的基本信息,如用户名、邮箱、角色等 + * 这样客户端登录后可以直接获取用户信息,无需再次请求 + */ + private UserInfo userInfo; + + /** + * 令牌过期时间(毫秒时间戳) + * + * 表示这个Token将在什么时候过期 + * 客户端可以根据这个时间判断是否需要重新登录或刷新Token + * 这里设置为当前时间加上24小时(86400000毫秒) + */ + private Long expiresIn; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java new file mode 100644 index 0000000..cfbccc2 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/RegisterRequest.java @@ -0,0 +1,99 @@ +package com.example.user.adapter.in.web.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.Data; + +/** + * 用户注册请求DTO (Data Transfer Object) + * 用于封装用户注册时提交的数据 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于接收用户注册请求的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Schema Swagger注解,用于API文档生成,描述这个类的作用 + */ +@Data +@Schema(description = "用户注册请求") +public class RegisterRequest { + + /** + * 用户名字段 + * + * @NotBlank 验证注解,确保用户名不为空且去除空格后不为空 + * @Size 验证注解,限制用户名长度在3-20个字符之间 + * @Pattern 验证注解,使用正则表达式限制用户名只能包含字母、数字和下划线 + * regexp 属性定义了正则表达式规则:^[a-zA-Z0-9_]+$ + * ^ 表示字符串开始 + * [a-zA-Z0-9_] 表示字符可以是字母、数字或下划线 + * + 表示前面的字符可以出现一次或多次 + * $ 表示字符串结束 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "用户名不能为空") + @Size(min = 3, max = 20, message = "用户名长度必须在3-20个字符之间") + @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用户名只能包含字母、数字和下划线") + @Schema(description = "用户名", example = "testuser") + private String username; + + /** + * 密码字段 + * + * @NotBlank 验证注解,确保密码不为空且去除空格后不为空 + * @Size 验证注解,限制密码长度在6-20个字符之间 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间") + @Schema(description = "密码", example = "123456") + private String password; + + /** + * 确认密码字段 + * + * @NotBlank 验证注解,确保确认密码不为空且去除空格后不为空 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @NotBlank(message = "确认密码不能为空") + @Schema(description = "确认密码", example = "123456") + private String confirmPassword; + + /** + * 邮箱字段 + * + * @Email 验证注解,确保邮箱格式正确 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Email(message = "邮箱格式不正确") + @Schema(description = "邮箱", example = "test@example.com") + private String email; + + /** + * 手机号字段 + * + * @Pattern 验证注解,使用正则表达式验证手机号格式 + * regexp 属性定义了手机号的正则表达式规则:^1[3-9]\d{9}$ + * ^ 表示字符串开始 + * 1 表示手机号必须以1开头 + * [3-9] 表示第二位数字必须是3-9之间的数字 + * \d{9} 表示后面必须跟9个数字 + * $ 表示字符串结束 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + @Schema(description = "手机号", example = "13800138000") + private String phone; + + /** + * 真实姓名字段 + * + * 这个字段没有强制验证注解,是可选填写的 + * @Schema Swagger注解,用于API文档生成,描述字段的作用和示例值 + */ + @Schema(description = "真实姓名", example = "张三") + private String realName; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java new file mode 100644 index 0000000..245d8f6 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/Result.java @@ -0,0 +1,149 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 通用响应结果类 + * 用于封装所有API接口的响应数据,统一返回格式 + * + * 这样做有以下好处: + * 1. 前端可以统一处理响应格式 + * 2. 便于统一错误处理 + * 3. 提高代码的可维护性 + * + * 使用泛型可以让这个类适用于任何类型的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + /** + * 响应码 + * + * 用于表示请求处理的结果状态 + * 常见的响应码: + * 200 - 成功 + * 400 - 请求参数错误 + * 401 - 未授权 + * 403 - 禁止访问 + * 500 - 服务器内部错误 + */ + private Integer code; + + /** + * 响应消息 + * + * 用于描述请求处理的结果信息 + * 成功时可以是"操作成功" + * 失败时可以是具体的错误信息,如"用户名或密码错误" + */ + private String message; + + /** + * 响应数据 + * + * 用于携带具体的业务数据 + * 可以是任何类型,如用户信息、列表数据等 + * 使用泛型T使得这个字段可以适应不同类型的数据 + */ + private T data; + + /** + * 成功响应静态方法 + * + * 用于创建成功的响应结果 + * + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(T data) { + return new Result<>(200, "操作成功", data); + } + + /** + * 成功响应静态方法(无数据) + * + * 用于创建不携带数据的成功响应结果 + * + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success() { + return new Result<>(200, "操作成功", null); + } + + /** + * 成功响应静态方法(自定义消息) + * + * 用于创建携带自定义成功消息的响应结果 + * + * @param message 自定义的成功消息 + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(String message, T data) { + return new Result<>(200, message, data); + } + + /** + * 失败响应静态方法 + * + * 用于创建失败的响应结果,默认使用500状态码 + * + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(String message) { + return new Result<>(500, message, null); + } + + /** + * 失败响应静态方法(自定义状态码) + * + * 用于创建携带自定义状态码的失败响应结果 + * + * @param code 自定义状态码 + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(Integer code, String message) { + return new Result<>(code, message, null); + } + + /** + * 未授权响应静态方法 + * + * 用于创建401未授权的响应结果 + * + * @param message 错误消息 + * @return Result 未授权的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result unauthorized(String message) { + return new Result<>(401, message, null); + } + + /** + * 禁止访问响应静态方法 + * + * 用于创建403禁止访问的响应结果 + * + * @param message 错误消息 + * @return Result 禁止访问的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result forbidden(String message) { + return new Result<>(403, message, null); + } +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java new file mode 100644 index 0000000..85f781f --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserInfo.java @@ -0,0 +1,102 @@ +package com.example.user.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户信息DTO (Data Transfer Object) + * 用于封装用户的基本信息,在不同层之间传输 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于传输用户的基本信息,不包含敏感信息如密码 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserInfo { + + /** + * 用户ID + * + * 数据库中的主键,唯一标识一个用户 + * 使用Long类型可以支持更大的数据量 + */ + private Long id; + + /** + * 用户名 + * + * 用户登录时使用的名称,必须唯一 + * 通常用于登录认证 + */ + private String username; + + /** + * 邮箱 + * + * 用户的电子邮箱地址 + * 可以用于找回密码、接收通知等 + */ + private String email; + + /** + * 手机号 + * + * 用户的手机号码 + * 可以用于登录、找回密码、接收短信通知等 + */ + private String phone; + + /** + * 真实姓名 + * + * 用户的真实姓名 + * 用于实名认证、显示用户真实身份等 + */ + private String realName; + + /** + * 用户状态:0-禁用,1-启用 + * + * 用于控制用户账户是否可以正常使用 + * 0表示账户被禁用,无法登录 + * 1表示账户正常,可以登录使用 + */ + private Integer status; + + /** + * 角色:ADMIN-管理员,USER-普通用户 + * + * 用于区分用户的角色和权限 + * ADMIN表示管理员,拥有更高的权限 + * USER表示普通用户,拥有基本权限 + */ + private String role; + + /** + * 创建时间 + * + * 记录用户账户的创建时间 + * 使用LocalDateTime类型,是Java 8引入的新时间API + * 相比Date类型更加易用和直观 + */ + private LocalDateTime createTime; + + /** + * 最后登录时间 + * + * 记录用户最后一次登录的时间 + * 用于统计用户活跃度、安全监控等 + */ + private LocalDateTime lastLoginTime; +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java deleted file mode 100644 index 112434d..0000000 --- a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/dto/UserLoginRequestDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.user.adapter.in.web.dto; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserLoginRequestDTO { - private String name; - // 这里的password指的是用户输入的密码,而不是数据库中的密码。所以这里应该使用明文,类型是String - private String password; -} diff --git a/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..14605dc --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-in/user-adapter-in-web/src/main/java/com/example/user/adapter/in/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,259 @@ +package com.example.user.adapter.in.web.exception; + +import com.example.user.adapter.in.web.dto.Result; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +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 java.util.stream.Collectors; + +/** + * 全局异常处理器 + * 统一处理应用中的各种异常,避免异常直接暴露给用户 + * + * @Slf4j Lombok注解,自动生成日志对象log,用于记录异常日志 + * @RestControllerAdvice 组合注解,相当于@ControllerAdvice + @ResponseBody + * 用于定义全局异常处理器,可以捕获控制器层抛出的异常 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理认证异常 + * + * 当用户认证失败时(如Token无效、过期等)会抛出AuthenticationException + * + * @ExceptionHandler 注解指定该方法处理哪种异常 + * @ResponseStatus 注解指定返回的HTTP状态码 + * + * @param e 认证异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleAuthenticationException(AuthenticationException e) { + log.error("认证异常: {}", e.getMessage()); + return Result.unauthorized("认证失败: " + e.getMessage()); + } + + /** + * 处理凭据错误异常 + * + * 当用户名或密码错误时会抛出BadCredentialsException + * 这是AuthenticationException的一个子类 + * + * @param e 凭据错误异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleBadCredentialsException(BadCredentialsException e) { + log.error("凭据错误: {}", e.getMessage()); + return Result.unauthorized("用户名或密码错误"); + } + + /** + * 处理访问拒绝异常 + * + * 当已认证用户尝试访问没有权限的资源时会抛出AccessDeniedException + * + * @param e 访问拒绝异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleAccessDeniedException(AccessDeniedException e) { + log.error("访问拒绝: {}", e.getMessage()); + return Result.forbidden("访问被拒绝,权限不足"); + } + + /** + * 处理参数校验异常 - @Valid注解 + * + * 当使用@Valid注解验证请求参数失败时会抛出MethodArgumentNotValidException + * 例如LoginRequest中的用户名或密码不符合验证规则 + * + * @param e 参数校验异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数校验失败: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理参数绑定异常 + * + * 当请求参数绑定到对象时发生错误会抛出BindException + * + * @param e 参数绑定异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBindException(BindException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数绑定失败: {}", errorMessage); + return Result.error("参数绑定失败: " + errorMessage); + } + + /** + * 处理约束违反异常 + * + * 当使用Bean Validation API验证失败时会抛出ConstraintViolationException + * + * @param e 约束违反异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleConstraintViolationException(ConstraintViolationException e) { + // 收集所有约束违反的错误信息并拼接成字符串 + String errorMessage = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + log.error("约束违反: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理非法参数异常 + * + * 当传递给方法的参数不合法时会抛出IllegalArgumentException + * + * @param e 非法参数异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数: {}", e.getMessage()); + return Result.error("参数错误: " + e.getMessage()); + } + + /** + * 处理空指针异常 + * + * 当尝试访问空对象的属性或方法时会抛出NullPointerException + * + * @param e 空指针异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 处理运行时异常 + * + * 当发生未预期的运行时错误时会抛出RuntimeException + * 这是很多异常的父类 + * + * @param e 运行时异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleRuntimeException(RuntimeException e) { + log.error("运行时异常: {}", e.getMessage(), e); + return Result.error("系统异常: " + e.getMessage()); + } + + /** + * 处理其他所有异常 + * + * 作为兜底的异常处理方法,处理所有未被上面方法处理的异常 + * + * @param e 异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error("未知异常: {}", e.getMessage(), e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 自定义业务异常类 + * + * 用于处理应用程序中特定的业务异常 + * 继承自RuntimeException,是一个受检异常 + */ + public static class BusinessException extends RuntimeException { + /** + * 异常码 + * + * 用于标识异常的类型,便于前端进行不同的处理 + */ + private final int code; + + /** + * 构造函数 - 只有消息 + * + * @param message 异常消息 + */ + public BusinessException(String message) { + super(message); + this.code = 500; + } + + /** + * 构造函数 - 有码和消息 + * + * @param code 异常码 + * @param message 异常消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 获取异常码 + * + * @return int 异常码 + */ + public int getCode() { + return code; + } + } + + /** + * 处理自定义业务异常 + * + * 处理应用程序中抛出的BusinessException + * + * @param e 业务异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage()); + return Result.error(e.getCode(), e.getMessage()); + } +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml index 812d59d..10d3de7 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/pom.xml @@ -36,6 +36,12 @@ mysql-connector-j runtime + + com.example + user-service-application + 0.0.1-SNAPSHOT + compile + diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java index 38dc3ea..79e2a07 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/bridge/GetUserByNameBridge.java @@ -18,7 +18,7 @@ public class GetUserByNameBridge implements GetUserByNamePort { @Override public User getUserByName(String name) { LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(UserEntity::getName, name); + wrapper.eq(UserEntity::getUsername, name); UserEntity userEntity = userMapper.selectOne(wrapper); //password不空 log.info("userEntity: {}", userEntity); diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java index cc56ab5..3c6e442 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/convertor/UserConvertor.java @@ -14,11 +14,9 @@ public class UserConvertor { public static User toDomain(UserEntity userEntity) { return new User( new UserId(userEntity.getId()), - new UserName(userEntity.getName()), - new UserAge(userEntity.getAge()), + new UserName(userEntity.getUsername()), new Email(userEntity.getEmail()), - Password.fromEncrypted(userEntity.getPassword()), - new IsSuper(userEntity.getIsSuper() == 1) + Password.fromEncrypted(userEntity.getPassword()) ); } @@ -32,10 +30,7 @@ public class UserConvertor { return new UserEntity( user.getId().id(), user.getName().username(), - user.getAge().age(), - user.getEmail().email(), - user.getPassword().encryptedValue(), - user.getIsSuper().value() ? 1 : 0 + user.getEmail().email() ); } } diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java index 1455f81..e3c08a9 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/entity/UserEntity.java @@ -7,21 +7,123 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + @Data @AllArgsConstructor @NoArgsConstructor -@TableName("user") +@TableName("sys_user") public class UserEntity { - @TableId(type= IdType.ASSIGN_ID) - private long id; - private String name; - private Integer age; - private String email; + /** + * 用户ID + * + * 数据库表的主键字段 + * + * @TableId MyBatis Plus注解,标识这是主键字段 + * type = IdType.AUTO 表示使用数据库自增主键 + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 用户名 + * + * 用户登录时使用的名称,必须唯一 + * 通常用于登录认证 + */ + private String username; + + /** + * 密码 + * + * 用户的登录密码,存储的是经过加密的哈希值 + * 出于安全考虑,数据库中不会存储明文密码 + * 通常使用BCrypt等算法进行加密 + */ private String password; - private Integer isSuper; + + /** + * 邮箱 + * + * 用户的电子邮箱地址 + * 可以用于找回密码、接收通知等 + */ + private String email; + + /** + * 手机号 + * + * 用户的手机号码 + * 可以用于登录、找回密码、接收短信通知等 + */ + private String phone; + + /** + * 真实姓名 + * + * 用户的真实姓名 + * 用于实名认证、显示用户真实身份等 + */ + private String realName; + + /** + * 用户状态:0-禁用,1-启用 + * + * 用于控制用户账户是否可以正常使用 + * 0表示账户被禁用,无法登录 + * 1表示账户正常,可以登录使用 + */ + private Integer status; + + /** + * 角色:ADMIN-管理员,USER-普通用户 + * + * 用于区分用户的角色和权限 + * ADMIN表示管理员,拥有更高的权限 + * USER表示普通用户,拥有基本权限 + */ + private String role; + + /** + * 创建时间 + * + * 记录用户账户的创建时间 + * 使用LocalDateTime类型,是Java 8引入的新时间API + * 相比Date类型更加易用和直观 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + * + * 记录用户信息最后一次更新的时间 + * 每次修改用户信息时都应该更新这个字段 + */ + private LocalDateTime updateTime; + + /** + * 最后登录时间 + * + * 记录用户最后一次登录的时间 + * 用于统计用户活跃度、安全监控等 + */ + private LocalDateTime lastLoginTime; public UserEntity(long value, String value1, int value2, String value3) { } - public UserEntity(long id, String name, Integer age, String email, String password) { - this(id, name, age, email, password, 0); // 默认isSuper为0 + public UserEntity(Long id, String username, String password, String email, String phone, String realName, Integer status, String role) { + this.id = id; + this.username=username; + this.password=password; + this.email=email; + this.phone=phone; + this.realName=realName; + this.status=1; + this.role="USER"; + } + + public UserEntity(long id, String username, String email) { + this.id = id; + this.username = username; + this.email = email; } -} +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java index 92125d3..484fc60 100644 --- a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/mapper/UserMapper.java @@ -1,7 +1,53 @@ package com.example.user.adapter.out.persistence.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; + import com.example.user.adapter.out.persistence.entity.UserEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Update; +/** + * 用户数据访问层接口 + * + * Mapper是数据访问层的接口,负责与数据库进行交互 + * 这里使用MyBatis Plus框架,继承BaseMapper可以获得常用的CRUD操作 + * + * @Mapper MyBatis注解,标识这是一个Mapper接口 + * MyBatis会为这个接口生成实现类,用于执行SQL语句 + */ +@Mapper public interface UserMapper extends BaseMapper { -} + + /** + * 根据用户名查询用户 + * + * 这是一个自定义的查询方法,用于根据用户名查找用户信息 + * + * @Select MyBatis注解,直接在方法上编写SQL语句 + * SQL语句:SELECT * FROM sys_user WHERE username = #{username} + * #{username} 是参数占位符,会被方法参数替换 + * + * @param username 用户名 + * @return User 用户信息,如果不存在则返回null + */ + @Select("SELECT * FROM sys_user WHERE username = #{username}") + UserEntity selectByUsername(@Param("username") String username); + + /** + * 根据用户名更新密码 + * + * 这是一个自定义的更新方法,用于更新指定用户的密码 + * + * @Update MyBatis注解,直接在方法上编写SQL更新语句 + * SQL语句:UPDATE sys_user SET password = #{password} WHERE username = #{username} + * #{password} 和 #{username} 是参数占位符 + * + * @param username 用户名 + * @param password 新密码(应该是加密后的) + * @return int 更新记录数,成功更新返回1,未找到记录返回0 + */ + @Update("UPDATE sys_user SET password = #{password} WHERE username = #{username}") + int updatePasswordByUsername(@Param("username") String username, @Param("password") String password); +} \ No newline at end of file diff --git a/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java new file mode 100644 index 0000000..2b8fe40 --- /dev/null +++ b/user-service/user-service-adapter/user-adapter-out/user-adapter-out-persistence/src/main/java/com/example/user/adapter/out/persistence/service/CustomUserDetailsService.java @@ -0,0 +1,367 @@ +package com.example.user.adapter.out.persistence.service; + + + +import com.example.user.adapter.out.persistence.entity.UserEntity; +import com.example.user.adapter.out.persistence.mapper.UserMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.Collections; + +/** + * 自定义用户详情服务 - Spring Security认证的核心组件 + * 实现Spring Security的UserDetailsService接口 + * + * 【什么是UserDetailsService?】 + * UserDetailsService是Spring Security提供的核心接口,专门用于加载用户认证信息 + * 可以理解为"用户信息提供者",当有人要登录时,Spring Security会问它:"这个用户存在吗?密码对吗?有什么权限?" + * + * 【认证流程中的作用】 + * 1. 用户提交用户名和密码 + * 2. Spring Security调用loadUserByUsername方法 + * 3. 该方法从数据库查询用户信息 + * 4. 返回UserDetails对象(包含用户名、密码、权限等) + * 5. Spring Security自动验证密码是否匹配 + * 6. 验证成功则生成认证令牌 + * + * 【为什么要自定义实现?】 + * - Spring Security默认不知道我们的用户数据存在哪里(数据库、文件、内存等) + * - 通过实现UserDetailsService接口,告诉Spring Security如何获取用户信息 + * - 可以自定义用户权限、状态检查等业务逻辑 + * + * 【核心方法说明】 + * - loadUserByUsername:Spring Security认证时自动调用 + * - authenticate:自定义的登录验证方法 + * - saveUser:用户注册时保存用户信息 + * + * 【注解说明】 + * @Slf4j:Lombok注解,自动生成日志对象log,用于记录操作日志 + * @Service:Spring注解,将该类注册为Spring容器管理的服务Bean + * @RequiredArgsConstructor:Lombok注解,为所有final字段生成构造函数,实现依赖注入 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + /** + * 密码编码器 - 密码安全的守护者 + * + * 【作用说明】 + * 用于密码的加密和验证,确保用户密码安全存储 + * 不直接存储明文密码,而是存储加密后的哈希值 + * + * 【工作原理】 + * - 注册时:将明文密码加密成哈希值存储到数据库 + * - 登录时:将用户输入的明文密码加密,与数据库中的哈希值比较 + * - 即使数据库泄露,攻击者也无法直接获得用户的真实密码 + * + * 【依赖注入】 + * 通过@RequiredArgsConstructor注解自动注入 + * Spring容器会自动提供BCryptPasswordEncoder实例 + */ + private final PasswordEncoder passwordEncoder; + + /** + * 用户数据访问层 - 数据库操作的桥梁 + * + * 【作用说明】 + * MyBatis的Mapper接口,专门用于用户表的数据库操作 + * 提供了用户的增删改查功能 + * + * 【设计模式】 + * 采用DAO(Data Access Object)模式 + * 将数据访问逻辑与业务逻辑分离,提高代码的可维护性 + * + * 【主要功能】 + * - selectByUsername:根据用户名查询用户信息 + * - insert:插入新用户记录 + * - updatePasswordByUsername:更新用户密码 + * + * 【依赖注入】 + * 通过@RequiredArgsConstructor注解自动注入 + * MyBatis会自动生成该接口的实现类 + */ + private final UserMapper userMapper; + + /** + * 根据用户名加载用户详情 - Spring Security认证的核心入口 + * + * 【方法重要性】 + * 这是UserDetailsService接口的核心方法,Spring Security认证流程的关键环节 + * 当用户尝试登录时,Spring Security会自动调用这个方法获取用户信息 + * + * 【执行时机】 + * 1. 用户提交登录表单(用户名+密码) + * 2. Spring Security的认证管理器开始工作 + * 3. 认证管理器调用此方法,传入用户名 + * 4. 方法返回UserDetails对象 + * 5. Spring Security自动比较密码是否匹配 + * + * 【返回值说明】 + * UserDetails是Spring Security定义的用户信息接口,包含: + * - 用户名(username) + * - 密码(password,已加密) + * - 权限列表(authorities) + * - 账户状态(是否过期、锁定、禁用等) + * + * 【异常处理策略】 + * - 用户不存在:抛出UsernameNotFoundException + * - 用户被禁用:也抛出UsernameNotFoundException(安全考虑,不暴露具体原因) + * - 这样可以防止攻击者通过错误信息判断用户是否存在 + * + * @param username 用户名(来自登录表单) + * @return UserDetails Spring Security的用户详情接口实现 + * @throws UsernameNotFoundException 当用户不存在或被禁用时抛出此异常 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + // 【步骤1:记录调试日志】 + // 使用debug级别记录方法调用,便于开发时追踪认证流程 + log.debug("Loading user by username: {}", username); + + // 【步骤2:从数据库查询用户信息】 + // 调用MyBatis Mapper查询用户,这里会执行SQL: SELECT * FROM users WHERE username = ? + UserEntity user = userMapper.selectByUsername(username); + if (user == null) { + // 【安全策略】用户不存在时的处理 + // 记录警告日志,但不在异常信息中暴露过多细节,防止用户名枚举攻击 + log.warn("User not found: {}", username); + throw new UsernameNotFoundException("用户不存在: " + username); + } + + // 【步骤3:检查用户状态】 + // 业务规则:status=0表示用户被禁用,status=1表示正常 + if (user.getStatus() == 0) { + // 【安全策略】禁用用户的处理 + // 同样抛出UsernameNotFoundException,不暴露用户存在但被禁用的信息 + log.warn("User is disabled: {}", username); + throw new UsernameNotFoundException("用户已被禁用: " + username); + } + + // 【步骤4:构建Spring Security的UserDetails对象】 + // 【重要概念区分】 + // - org.springframework.security.core.userdetails.User:Spring Security的用户类,用于认证和授权 + // - com.example.ss.demo.entity.User:我们自定义的用户实体类,用于数据库存储 + return org.springframework.security.core.userdetails.User.builder() + // 【字段映射】将数据库用户信息映射到Spring Security用户对象 + .username(user.getUsername()) // 设置用户名 + .password(user.getPassword()) // 设置密码(已是BCrypt加密的哈希值) + .authorities(Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + user.getRole()))) // 设置用户角色权限(如ROLE_ADMIN、ROLE_USER) + .accountExpired(false) // 账户是否过期(这里设置为永不过期) + .accountLocked(false) // 账户是否被锁定(这里设置为永不锁定) + .credentialsExpired(false) // 凭据是否过期(这里设置为永不过期) + .disabled(user.getStatus() == 0) // 账户是否被禁用(根据数据库status字段决定) + .build(); + } + + /** + * 根据用户名获取用户信息 + * + * 这是一个自定义方法,用于业务逻辑中获取用户信息 + * 与loadUserByUsername不同,这个方法不会抛出异常 + * + * @param username 用户名 + * @return User 用户信息,如果不存在则返回null + */ + public UserEntity getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + + /** + * 验证用户登录 - 自定义的完整登录验证方法 + * + * 【方法作用】 + * 这是一个自定义的登录验证方法,完成用户名和密码的完整验证流程 + * 与loadUserByUsername不同,这个方法会直接验证密码是否正确 + * + * 【与loadUserByUsername的区别】 + * - loadUserByUsername:只负责根据用户名获取用户信息,Spring Security自动验证密码 + * - authenticate:完整的登录验证,包括用户名查询和密码验证 + * + * 【使用场景】 + * - 自定义登录接口(如REST API登录) + * - 需要在业务逻辑中手动验证用户身份 + * - 登录前的预检查(如检查登录次数限制等) + * + * 【验证流程】 + * 1. 根据用户名查询用户信息 + * 2. 检查用户是否存在 + * 3. 使用PasswordEncoder验证密码 + * 4. 返回验证结果 + * + * 【安全考虑】 + * - 密码验证使用BCrypt算法,安全性高 + * - 验证失败时记录日志,便于安全审计 + * - 不在返回值中暴露失败原因,防止信息泄露 + * + * @param username 用户名 + * @param password 密码(明文,方法内部会进行加密比较) + * @return User 验证成功返回用户信息,验证失败返回null + */ + public UserEntity authenticate(String username, String password) { + // 【步骤1:查询用户信息】 + // 从数据库中根据用户名查询用户记录 + UserEntity user = userMapper.selectByUsername(username); + if (user == null) { + // 【安全日志】记录登录失败的尝试,用于安全审计 + // 不返回具体错误信息给调用者,防止用户名枚举攻击 + log.warn("用户不存在: {}", username); + return null; + } + + // 【调试信息】输出当前密码的加密结果(仅用于开发调试) + // 注意:生产环境中应该删除此行,避免日志中出现敏感信息 + log.info("password:{}",passwordEncoder.encode( password)); + + // 【步骤2:密码验证 - 核心安全检查】 + // 【重要原理】BCrypt密码验证过程: + // 1. passwordEncoder.matches(明文密码, 数据库中的哈希值) + // 2. BCrypt会提取数据库哈希值中的盐值(salt) + // 3. 使用相同的盐值对明文密码进行哈希 + // 4. 比较两个哈希值是否相同 + // 5. 这样即使相同的密码,每次存储的哈希值也不同(因为盐值随机) + if (!passwordEncoder.matches(password, user.getPassword())) { + // 【安全日志】记录密码错误的登录尝试 + // 这类日志对于检测暴力破解攻击很重要 + log.warn("用户登录失败: 密码错误 - {}", username); + return null; + } + + // 【步骤3:登录成功处理】 + // 记录成功登录的日志,用于用户行为分析和安全审计 + log.info("用户登录成功: {}", username); + return user; + } + + /** + * 测试密码匹配(调试用) + * + * 用于调试时验证密码哈希是否正确 + * + * @param rawPassword 原始密码(明文) + * @param encodedPassword 编码后的密码(哈希值) + * @return boolean 是否匹配 + */ + public boolean testPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 修正admin用户的密码哈希 + * + * 用于修复数据库中admin用户的密码哈希值 + * + * @return boolean 修正是否成功 + */ + public boolean fixAdminPassword() { + try { + // 生成新的正确密码哈希 + String correctPasswordHash = generatePasswordHash("password"); + + // 更新admin用户的密码哈希 + int result = userMapper.updatePasswordByUsername("admin", correctPasswordHash); + + // 同时更新user用户的密码哈希 + userMapper.updatePasswordByUsername("user", correctPasswordHash); + + return result > 0; + } catch (Exception e) { + log.error("修正密码哈希失败: {}", e.getMessage()); + return false; + } + } + + /** + * 生成密码哈希 + * + * 使用密码编码器对原始密码进行加密 + * + * @param password 原始密码(明文) + * @return String 生成的密码哈希 + */ + public String generatePasswordHash(String password) { + return passwordEncoder.encode(password); + } + + /** + * 保存用户信息 - 用户注册的核心数据持久化方法 + * + * 【方法作用】 + * 将新用户信息保存到数据库中,这是用户注册流程的最后一步 + * 完成用户数据的持久化存储 + * + * 【调用时机】 + * 1. 用户提交注册表单 + * 2. 控制器验证用户名不重复 + * 3. 控制器加密用户密码 + * 4. 调用此方法保存用户信息 + * + * 【数据库操作】 + * 使用MyBatis Plus的insert方法进行数据插入 + * - 自动生成主键ID + * - 自动设置创建时间(如果配置了自动填充) + * - 返回影响的行数 + * + * 【事务处理】 + * 建议在调用此方法的上层添加@Transactional注解 + * 确保注册过程的数据一致性 + * + * 【异常处理策略】 + * - 捕获所有异常,避免敏感信息泄露 + * - 记录详细的错误日志,便于问题排查 + * - 返回统一的结果码,便于上层处理 + * + * 【返回值说明】 + * - 1:保存成功 + * - 0:保存失败(可能是数据库约束冲突、网络问题等) + * + * @param user 要保存的用户对象(包含用户名、加密密码、角色等信息) + * @return int 保存结果,成功返回1,失败返回0 + */ + public int saveUser(UserEntity user) { + try { + // 【数据库插入操作】 + // 使用MyBatis Plus的insert方法保存用户到数据库 + // 【自动功能】 + // 1. 自动生成主键ID(通常使用数据库的AUTO_INCREMENT) + // 2. 自动设置创建时间和更新时间(如果配置了自动填充) + // 3. 返回影响的行数(成功插入返回1) + int result = userMapper.insert(user); + + // 【结果检查和日志记录】 + if (result > 0) { + // 【成功日志】记录用户注册成功的信息 + // 包含用户名和自动生成的用户ID,便于后续追踪 + log.info("用户保存成功: username={}, userId={}", user.getUsername(), user.getId()); + } else { + // 【失败日志】记录保存失败的情况 + // 这种情况比较少见,可能是数据库连接问题或其他系统问题 + log.warn("用户保存失败: username={}", user.getUsername()); + } + + return result; + } catch (Exception e) { + // 【异常处理】捕获所有可能的异常 + // 【常见异常类型】 + // 1. DuplicateKeyException:用户名重复(违反唯一约束) + // 2. DataAccessException:数据库连接或SQL执行异常 + // 3. ConstraintViolationException:数据约束违反 + // + // 【安全考虑】 + // - 记录详细的异常信息到日志中,便于开发人员排查问题 + // - 但不向调用者暴露具体的异常信息,只返回统一的失败标识 + // - 这样既便于调试,又不会泄露系统内部信息 + log.error("保存用户异常: username={}, error={}", user.getUsername(), e.getMessage(), e); + return 0; + } + } + +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java new file mode 100644 index 0000000..6944907 --- /dev/null +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/TokenBlacklistService.java @@ -0,0 +1,204 @@ +package com.example.user.service.application.service; + + +import com.example.user.service.common.JwtUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Token黑名单服务类 - JWT令牌失效管理 + * + * 这个服务类解决了JWT无状态认证中的一个重要问题:如何在用户登出后立即使令牌失效 + * + * JWT的无状态特性说明: + * - JWT令牌是自包含的,包含了所有必要的用户信息 + * - 服务端不需要存储会话状态,每次请求都通过验证令牌签名来确认身份 + * - 这种设计的优点是可扩展性好,但缺点是无法在服务端主动"销毁"令牌 + * - 令牌在过期时间之前始终有效,即使用户已经登出 + * + * 黑名单机制原理: + * 1. 维护一个已失效令牌的黑名单列表 + * 2. 用户登出时,将令牌添加到黑名单 + * 3. 每次验证令牌时,先检查是否在黑名单中 + * 4. 如果在黑名单中,则拒绝访问,即使令牌本身是有效的 + * + * 存储方案选择: + * - 内存存储(ConcurrentHashMap):适合单机部署,性能最好 + * - Redis存储:适合分布式部署,多个服务实例共享黑名单 + * - 数据库存储:适合对数据持久性要求高的场景 + * + * 本实现使用内存存储,具有以下特点: + * - 高性能:内存访问速度快,不涉及网络IO + * - 线程安全:使用ConcurrentHashMap保证并发安全 + * - 自动清理:定时清理过期的黑名单记录,避免内存泄漏 + * - 简单可靠:无外部依赖,部署简单 + * + * 注意事项: + * - 重启服务会丢失黑名单数据,已登出的用户令牌可能重新生效 + * - 多实例部署时,各实例的黑名单不共享 + * - 如需解决以上问题,建议改用Redis等外部存储 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class TokenBlacklistService { + + /** + * JWT工具类,用于解析令牌获取过期时间 + */ + private final JwtUtil jwtUtil; + + /** + * 黑名单存储容器 + * + * 使用ConcurrentHashMap确保线程安全的并发访问 + * Key: JWT令牌字符串 + * Value: 令牌的过期时间,用于定时清理 + * + * 为什么存储过期时间: + * - 避免永久存储已过期的令牌,节省内存空间 + * - 支持定时清理机制,自动移除不再需要的黑名单记录 + * - 提供调试信息,便于排查问题 + */ + private final ConcurrentHashMap blacklistedTokens = new ConcurrentHashMap<>(); + + /** + * 将令牌添加到黑名单 + * + * 当用户登出时调用此方法,将JWT令牌加入黑名单 + * 加入黑名单后,该令牌将无法再用于身份验证 + * + * 实现细节: + * 1. 解析令牌获取过期时间 + * 2. 将令牌和过期时间存入黑名单Map + * 3. 记录操作日志,便于审计和调试 + * + * 异常处理: + * - 如果令牌格式无效,会记录警告日志但不抛出异常 + * - 确保即使个别令牌处理失败,也不影响整体功能 + * + * @param token JWT令牌字符串 + */ + public void addToBlacklist(String token) { + try { + // 解析令牌获取过期时间 + // 这样可以在令牌自然过期后自动清理黑名单记录 + Date expirationTime = jwtUtil.getExpirationDateFromToken(token); + + // 将令牌添加到黑名单 + blacklistedTokens.put(token, expirationTime); + + // 记录操作日志(只记录令牌的前几位,避免泄露完整令牌) + log.info("令牌已添加到黑名单,过期时间: {}, 令牌前缀: {}...", + expirationTime, + token.substring(0, Math.min(token.length(), 10))); + + } catch (Exception e) { + // 如果解析令牌失败,记录警告日志 + log.warn("添加令牌到黑名单时发生错误: {}, 令牌前缀: {}...", + e.getMessage(), + token.substring(0, Math.min(token.length(), 10))); + } + } + + /** + * 检查令牌是否在黑名单中 + * + * 在JWT认证过滤器中调用此方法,检查令牌是否已被列入黑名单 + * 如果令牌在黑名单中,则应拒绝该请求的访问 + * + * 性能考虑: + * - ConcurrentHashMap的containsKey操作时间复杂度为O(1) + * - 即使黑名单中有大量令牌,查询性能也很好 + * - 无需额外的网络请求或磁盘IO + * + * @param token JWT令牌字符串 + * @return boolean true表示令牌在黑名单中(应拒绝访问),false表示不在黑名单中 + */ + public boolean isBlacklisted(String token) { + boolean isBlacklisted = blacklistedTokens.containsKey(token); + + if (isBlacklisted) { + log.debug("检测到黑名单令牌访问尝试,令牌前缀: {}...", + token.substring(0, Math.min(token.length(), 10))); + } + + return isBlacklisted; + } + + /** + * 定时清理过期的黑名单令牌 + * + * 使用Spring的@Scheduled注解实现定时任务 + * 每小时执行一次清理操作,移除已经自然过期的令牌 + * + * 清理的必要性: + * - 避免内存泄漏:长期运行的服务会积累大量过期令牌 + * - 提高性能:减少黑名单大小,提高查询效率 + * - 节省资源:释放不再需要的内存空间 + * + * 清理策略: + * - 只清理已经过期的令牌(当前时间 > 令牌过期时间) + * - 使用迭代器安全地删除元素,避免并发修改异常 + * - 记录清理统计信息,便于监控和调试 + * + * 定时配置说明: + * - fixedRate = 3600000:每3600000毫秒(1小时)执行一次 + * - 可以根据实际需求调整清理频率 + * - 频率过高会增加CPU开销,频率过低会占用更多内存 + */ + @Scheduled(fixedRate = 3600000) // 每小时执行一次 + public void cleanupExpiredTokens() { + Date now = new Date(); + int initialSize = blacklistedTokens.size(); + + // 使用removeIf方法安全地移除过期令牌 + // 这个方法是线程安全的,不会与其他操作产生冲突 + blacklistedTokens.entrySet().removeIf(entry -> { + Date expirationTime = entry.getValue(); + return expirationTime != null && now.after(expirationTime); + }); + + int finalSize = blacklistedTokens.size(); + int cleanedCount = initialSize - finalSize; + + if (cleanedCount > 0) { + log.info("黑名单清理完成,清理了 {} 个过期令牌,当前黑名单大小: {}", cleanedCount, finalSize); + } else { + log.debug("黑名单清理完成,无过期令牌需要清理,当前黑名单大小: {}", finalSize); + } + } + + /** + * 获取当前黑名单大小 + * + * 提供监控和调试功能,可以了解当前黑名单的使用情况 + * + * @return int 黑名单中令牌的数量 + */ + public int getBlacklistSize() { + return blacklistedTokens.size(); + } + + /** + * 清空所有黑名单令牌 + * + * 提供管理功能,在特殊情况下可以清空整个黑名单 + * 注意:此操作会使所有已登出用户的令牌重新生效 + * + * 使用场景: + * - 系统维护时需要重置黑名单状态 + * - 测试环境中需要快速清理数据 + * - 紧急情况下需要恢复所有用户访问 + */ + public void clearBlacklist() { + int size = blacklistedTokens.size(); + blacklistedTokens.clear(); + log.warn("黑名单已被清空,共清理了 {} 个令牌", size); + } +} \ No newline at end of file diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java index b10d43e..31b5a6e 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java @@ -30,9 +30,7 @@ public class UserLoginService implements UserLoginUseCase { } // 签发token String token = JwtUtil.generateToken( - user.getId().id(), - user.getName().username(), - user.getIsSuper().value() + user.getName().username() ); log.info("生成的JWT令牌: {}", token); return token; diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java new file mode 100644 index 0000000..8d99aea --- /dev/null +++ b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/config/BasicSecurityConfig.java @@ -0,0 +1,181 @@ +package com.example.user.service.bootstrap.security.config; + + + +import com.example.user.adapter.out.persistence.service.CustomUserDetailsService; +import com.example.user.service.bootstrap.security.filter.JwtAuthenticationFilter; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; +import java.util.List; + +/** + * Spring Security 配置类 + * 支持JWT认证和跨域配置 + * + * 这个类负责配置整个应用程序的安全策略,包括: + * 1. 用户认证和授权规则 + * 2. JWT Token验证 + * 3. 跨域资源共享(CORS)配置 + * 4. 会话管理策略 + */ +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(prePostEnabled = true) +@RequiredArgsConstructor +public class BasicSecurityConfig { + + /** + * JWT认证过滤器 + * 用于拦截请求并验证JWT Token的有效性 + */ + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + /** + * 自定义用户详情服务 + * 用于加载用户信息进行认证 + */ + private final CustomUserDetailsService userDetailsService; + + + + /** + * 认证管理器 + * 负责处理用户认证请求 + * + * @param config 认证配置对象,由Spring自动注入 + * @return AuthenticationManager 认证管理器实例 + * @throws Exception 如果配置过程中出现异常 + */ + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + /** + * CORS配置源 + * 配置跨域资源共享策略,允许前端应用访问后端API + * + * @return CorsConfigurationSource CORS配置源 + */ + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + // 允许的源(Origin),这里设置为允许所有域名访问 + // 在生产环境中应该设置为具体的域名以提高安全性 + configuration.setAllowedOriginPatterns(List.of("*")); + + // 允许的HTTP方法,包括常用的GET、POST、PUT、DELETE等 + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); + + // 允许的请求头,这里设置为允许所有请求头 + configuration.setAllowedHeaders(List.of("*")); + + // 允许携带凭证(如Cookie),设置为true表示允许跨域请求携带身份信息 + configuration.setAllowCredentials(true); + + // 预检请求的缓存时间,单位秒,这里设置为1小时 + configuration.setMaxAge(3600L); + + // 暴露给客户端的响应头,允许客户端访问这些响应头 + configuration.setExposedHeaders(Arrays.asList("Authorization", "Content-Type")); + + // 创建基于URL的CORS配置源 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + // 对所有路径应用相同的CORS配置 + source.registerCorsConfiguration("/**", configuration); + return source; + } + + /** + * 安全过滤器链配置 + * 定义HTTP请求的安全处理规则 + * + * @param http HttpSecurity对象,用于配置Web安全 + * @return SecurityFilterChain 安全过滤器链 + * @throws Exception 如果配置过程中出现异常 + */ + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + // 禁用CSRF(跨站请求伪造)保护 + // 前后端分离项目通常禁用,因为使用Token认证而不是Session + .csrf(AbstractHttpConfigurer::disable) + + // 启用CORS(跨域资源共享)并使用上面定义的配置 + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + + // 配置会话管理为无状态 + // 因为使用JWT Token认证,不需要服务器保存会话状态 + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + + // 配置请求授权规则 + .authorizeHttpRequests(authz -> authz + // 公开接口,无需认证即可访问 + .requestMatchers("/api/auth/login", "/api/auth/register").permitAll() + .requestMatchers("/api/auth/debug/**").permitAll() + // Swagger和knife4j文档接口放行,方便查看API文档 + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**").permitAll() + .requestMatchers("/doc.html", "/webjars/**", "/favicon.ico").permitAll() + .requestMatchers("/actuator/**").permitAll() + .requestMatchers("/error").permitAll() + // 其他所有请求都需要认证 + .anyRequest().authenticated() + ) + + // 禁用默认的登录页面 + // 因为使用自定义的登录接口,不需要Spring Security提供的默认登录页面 + .formLogin(AbstractHttpConfigurer::disable) + + // 禁用默认的登出页面 + // 因为使用自定义的登出接口,不需要Spring Security提供的默认登出功能 + .logout(AbstractHttpConfigurer::disable) + + // 禁用HTTP Basic认证 + // 因为使用JWT Token认证,不需要HTTP Basic认证方式 + .httpBasic(AbstractHttpConfigurer::disable) + + // 添加JWT认证过滤器 + // 在用户名密码认证过滤器之前添加JWT认证过滤器 + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + + // 配置用户详情服务 + // 用于加载用户信息进行认证 + .userDetailsService(userDetailsService) + + // 配置异常处理 + .exceptionHandling(exceptions -> exceptions + // 认证入口点,当未认证用户访问需要认证的资源时调用 + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(401); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":401,\"message\":\"未授权访问,请先登录\",\"data\":null}"); + }) + // 访问拒绝处理器,当已认证用户访问没有权限的资源时调用 + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(403); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("{\"code\":403,\"message\":\"访问被拒绝,权限不足\",\"data\":null}"); + }) + ); + + return http.build(); + } +} diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java new file mode 100644 index 0000000..7d8f9ed --- /dev/null +++ b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/controller/UserController.java @@ -0,0 +1,513 @@ +package com.example.user.service.bootstrap.security.controller; + +import com.example.user.adapter.in.web.dto.*; +import com.example.user.adapter.out.persistence.entity.UserEntity; +import com.example.user.adapter.out.persistence.service.CustomUserDetailsService; +import com.example.user.service.application.service.TokenBlacklistService; +import com.example.user.service.common.JwtUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; + +/** + * 用户控制器 - Spring Security JWT认证系统的核心控制器 + * + * 这个控制器是整个认证系统的入口点,负责处理所有与用户认证和授权相关的HTTP请求 + * 主要功能包括: + * 1. 用户登录认证 - 验证用户名密码,生成JWT令牌 + * 2. 用户注册 - 创建新用户账户,密码加密存储 + * 3. 用户信息获取 - 基于JWT令牌获取当前登录用户信息 + * 4. 用户登出 - 使JWT令牌失效(可选实现) + * 5. 系统测试接口 - 用于开发阶段的功能验证 + * + * 设计理念: + * - RESTful API设计:使用标准的HTTP方法和状态码 + * - 统一响应格式:所有接口都返回Result格式的响应 + * - 安全性优先:密码加密、JWT令牌、异常信息脱敏 + * - 日志记录:详细记录用户操作和系统异常 + * + * 技术栈说明: + * @RestController - Spring MVC注解,将类标记为REST控制器,自动将返回值序列化为JSON + * @RequestMapping("/api/auth") - 设置控制器的基础路径,所有方法的路径都会以/api/auth开头 + * @RequiredArgsConstructor - Lombok注解,为所有final字段自动生成构造函数,实现依赖注入 + * @Slf4j - Lombok注解,自动生成名为log的日志对象,用于记录系统日志 + * @Tag - Swagger注解,用于API文档生成,将相关接口分组显示 + * + * 依赖注入说明: + * - JwtUtil:JWT令牌的生成和验证工具 + * - CustomUserDetailsService:用户详情服务,处理用户认证和数据操作 + * - UserMapper:用户数据访问层,直接操作数据库 + * + * 安全考虑: + * - 所有用户输入都经过验证(@Valid注解) + * - 密码使用BCrypt加密存储 + * - 异常信息不暴露敏感数据 + * - JWT令牌有过期时间限制 + */ +@Slf4j +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +@Tag(name = "用户认证和管理", description = "用户登录、注册、信息管理等接口") +public class UserController { + + /** + * 自定义用户详情服务 + * 用于处理用户认证、查询用户信息等业务逻辑 + */ + private final CustomUserDetailsService userDetailsService; + + /** + * JWT工具类 + * 用于生成和验证JWT Token + */ + private final JwtUtil jwtUtil; + + /** + * Token黑名单服务 + * 用于管理已失效的JWT Token,实现真正的登出功能 + */ + private final TokenBlacklistService tokenBlacklistService; + + /** + * 用户登录接口 - JWT认证系统的核心入口 + * + * 这是整个认证系统最重要的接口,负责验证用户身份并生成访问令牌 + * + * 登录流程详解: + * 1. 接收前端传来的用户名和密码(JSON格式) + * 2. 使用@Valid注解自动验证请求参数的格式(如用户名长度、密码复杂度等) + * 3. 调用CustomUserDetailsService.authenticate()方法验证用户名密码 + * - 从数据库查询用户信息 + * - 使用BCrypt算法验证密码哈希 + * - 检查用户状态(是否被禁用) + * 4. 验证成功后,使用JwtUtil生成JWT访问令牌 + * - 令牌中包含用户ID、用户名、角色等信息 + * - 设置令牌过期时间(通常24小时) + * 5. 构造登录响应对象,包含令牌和基本用户信息 + * 6. 记录登录日志,便于安全审计 + * + * 安全特性: + * - 密码传输:前端应使用HTTPS确保密码传输安全 + * - 密码存储:数据库中存储的是BCrypt加密后的哈希值,不是明文 + * - 令牌安全:JWT令牌使用HMAC SHA256算法签名,防止篡改 + * - 失败处理:登录失败不暴露具体原因(用户不存在 vs 密码错误) + * + * 错误处理: + * - 参数验证失败:返回400 Bad Request + * - 用户名或密码错误:返回统一的"用户名或密码错误"消息 + * - 系统异常:返回通用错误消息,具体错误记录在日志中 + * + * @param loginRequest 登录请求对象,包含用户名和密码 + * - username: 用户名,必填,长度3-20字符 + * - password: 密码,必填,长度6-20字符 + * @return Result 登录结果响应 + * 成功时包含: + * - accessToken: JWT访问令牌 + * - tokenType: 令牌类型(固定为"Bearer") + * - expiresIn: 令牌过期时间(秒) + * - userInfo: 用户基本信息(ID、用户名、角色等) + * @PostMapping("/login") POST请求映射,处理/api/auth/login路径的POST请求 + * @Operation Swagger注解,描述该接口的作用和详细信息 + */ + @PostMapping("/login") + @Operation(summary = "用户登录", description = "用户名密码登录,返回JWT token") + public Result login(@Valid @RequestBody LoginRequest loginRequest) { + try { + log.info("用户登录请求: {}", loginRequest.getUsername()); + + // 调用用户详情服务验证用户凭据 + // authenticate方法会检查用户名是否存在,密码是否正确 + UserEntity user = userDetailsService.authenticate(loginRequest.getUsername(), loginRequest.getPassword()); + if (user == null) { + // 如果用户认证失败,返回错误信息 + return Result.error("登录失败:用户名或密码错误"); + } + + // 用户认证成功,生成JWT Token + // JWT Token是一种无状态的认证方式,包含用户信息且经过签名验证 + String token = jwtUtil.generateToken(user.getUsername()); + + // 构建用户信息对象 - 为了避免暴露敏感信息,这里只返回部分信息 + // 使用Builder模式构建对象,代码更清晰易读 + UserInfo userInfo = UserInfo.builder() + .id(user.getId()) // 用户ID + .username(user.getUsername()) // 用户名 + .email(user.getEmail()) // 邮箱 + .phone(user.getPhone()) // 手机号 + .realName(user.getRealName()) // 真实姓名 + .status(user.getStatus()) // 用户状态 + .role(user.getRole()) // 用户角色 + .createTime(user.getCreateTime()) // 创建时间 + .lastLoginTime(LocalDateTime.now()) // 最后登录时间 + .build(); + + // 构建登录响应对象 + LoginResponse loginResponse = LoginResponse.builder() + .accessToken(token) // JWT访问令牌 + .tokenType("Bearer") // 令牌类型 + .userInfo(userInfo) // 用户信息 + .expiresIn(System.currentTimeMillis() + 86400000L) // 24小时后过期 + .build(); + + log.info("用户登录成功: {}", user.getUsername()); + // 返回登录成功的响应结果 + return Result.success("登录成功", loginResponse); + + } catch (Exception e) { + // 捕获异常并记录错误日志 + log.error("用户登录失败: {}", e.getMessage()); + // 返回登录失败的响应结果 + return Result.error("登录失败: " + e.getMessage()); + } + } + + /** + * 用户注册 + * + * 用户注册接口,处理新用户的注册请求 + * 主要流程: + * 1. 验证请求参数的有效性(通过@Valid注解自动验证) + * 2. 检查用户名是否已经存在 + * 3. 验证两次输入的密码是否一致 + * 4. 对密码进行加密处理 + * 5. 创建新用户并保存到数据库 + * + * @param registerRequest 注册请求对象,包含用户名、密码、邮箱等信息 + * @return Result 注册结果响应 + */ + @PostMapping("/register") + @Operation(summary = "用户注册", description = "用户注册接口") + public Result register(@Valid @RequestBody RegisterRequest registerRequest) { + try { + log.info("用户注册请求: username={}, email={}", registerRequest.getUsername(), registerRequest.getEmail()); + + // 1. 检查用户名是否已存在 + // 通过CustomUserDetailsService查询数据库,避免重复用户名 + UserEntity existingUser = userDetailsService.getUserByUsername(registerRequest.getUsername()); + if (existingUser != null) { + log.warn("注册失败: 用户名已存在 - {}", registerRequest.getUsername()); + return Result.error("用户名已存在,请选择其他用户名"); + } + + // 2. 验证两次密码是否一致 + // 前端应该已经验证过,但后端也需要再次验证确保数据安全 + if (!registerRequest.getPassword().equals(registerRequest.getConfirmPassword())) { + log.warn("注册失败: 两次密码不一致 - {}", registerRequest.getUsername()); + return Result.error("两次输入的密码不一致"); + } + + // 3. 创建新用户对象 + UserEntity newUser = new UserEntity(); + newUser.setUsername(registerRequest.getUsername()); + + // 4. 对密码进行加密 + // 使用Spring Security提供的密码编码器进行加密 + // 这里使用BCrypt算法,每次加密结果都不同,但验证时能正确匹配 + String encodedPassword = userDetailsService.generatePasswordHash(registerRequest.getPassword()); + newUser.setPassword(encodedPassword); + + // 5. 设置其他用户信息 + newUser.setEmail(registerRequest.getEmail()); + newUser.setPhone(registerRequest.getPhone()); + newUser.setRealName(registerRequest.getRealName()); + newUser.setRole("USER"); // 默认角色为普通用户 + newUser.setStatus(1); // 默认状态为启用 + + // 6. 保存用户到数据库 + // 使用MyBatis Plus的insert方法保存用户 + int result = userDetailsService.saveUser(newUser); + + if (result > 0) { + log.info("用户注册成功: username={}, userId={}", registerRequest.getUsername(), newUser.getId()); + return Result.success("注册成功,请使用用户名和密码登录"); + } else { + log.error("用户注册失败: 数据库插入失败 - {}", registerRequest.getUsername()); + return Result.error("注册失败,请稍后重试"); + } + + } catch (Exception e) { + // 捕获所有异常,避免敏感信息泄露 + log.error("用户注册异常: username={}, error={}", registerRequest.getUsername(), e.getMessage(), e); + return Result.error("系统异常,请稍后重试"); + } + } + + /** + * 获取当前用户信息接口 - 基于JWT令牌的用户信息查询 + * + * 这个接口展示了JWT认证系统中如何获取当前登录用户的信息 + * 它是一个受保护的接口,只有携带有效JWT令牌的请求才能访问 + * + * 工作流程详解: + * 1. 客户端发送请求时,必须在HTTP头中携带JWT令牌 + * 格式:Authorization: Bearer + * 2. Spring Security的JwtAuthenticationFilter会拦截请求 + * - 提取并验证JWT令牌的有效性 + * - 解析令牌中的用户信息(用户名、角色等) + * - 创建Authentication对象并设置到SecurityContext中 + * 3. 控制器方法通过Authentication参数获取当前用户信息 + * - Authentication.getName()获取用户名 + * - Authentication.getAuthorities()获取用户权限 + * 4. 根据用户名从数据库查询完整的用户信息 + * 5. 返回用户详细信息(密码等敏感信息已过滤) + * + * 安全机制: + * - JWT令牌验证:确保令牌未被篡改且未过期 + * - 用户状态检查:确保用户账户仍然有效(未被禁用) + * - 敏感信息过滤:返回的用户信息不包含密码等敏感数据 + * - 权限控制:可以根据用户角色返回不同级别的信息 + * + * 使用场景: + * - 用户个人中心页面加载用户信息 + * - 前端验证用户登录状态 + * - 获取用户权限信息用于前端菜单控制 + * - 用户信息更新前的数据回显 + * + * 错误处理: + * - 令牌无效或过期:返回401 Unauthorized + * - 用户不存在:返回404 Not Found + * - 用户被禁用:返回403 Forbidden + * - 系统异常:返回500 Internal Server Error + * + * @return Result 用户信息响应 + * 成功时包含用户的详细信息: + * - id: 用户ID + * - username: 用户名 + * - email: 邮箱 + * - realName: 真实姓名 + * - role: 用户角色 + * - status: 账户状态 + * - createTime: 创建时间 + * 注意:密码字段不会返回,确保安全性 + * @GetMapping("/info") GET请求映射,处理/api/auth/info路径的GET请求 + */ + @GetMapping("/info") + @Operation(summary = "获取用户信息", description = "获取当前登录用户的详细信息") + public Result getUserInfo() { + try { + // 从Spring Security上下文中获取当前认证信息 + // SecurityContextHolder是Spring Security提供的安全上下文持有者 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // 检查用户是否已认证 + if (authentication == null || !authentication.isAuthenticated()) { + // 如果用户未认证,返回未授权错误 + return Result.unauthorized("用户未登录"); + } + + // 从认证信息中获取用户名 + String username = authentication.getName(); + // 根据用户名查询用户详细信息 + UserEntity user = userDetailsService.getUserByUsername(username); + + if (user == null) { + // 如果用户不存在,返回错误信息 + return Result.error("用户不存在"); + } + + // 构建用户信息对象 + UserInfo userInfo = UserInfo.builder() + .id(user.getId()) // 用户ID + .username(user.getUsername()) // 用户名 + .email(user.getEmail()) // 邮箱 + .phone(user.getPhone()) // 手机号 + .realName(user.getRealName()) // 真实姓名 + .status(user.getStatus()) // 用户状态 + .role(user.getRole()) // 用户角色 + .createTime(user.getCreateTime()) // 创建时间 + .lastLoginTime(user.getLastLoginTime()) // 最后登录时间 + .build(); + + // 返回用户信息 + return Result.success(userInfo); + + } catch (Exception e) { + // 捕获异常并记录错误日志 + log.error("获取用户信息失败: {}", e.getMessage()); + // 返回获取用户信息失败的响应结果 + return Result.error("获取用户信息失败: " + e.getMessage()); + } + } + + /** + * 用户登出接口 - JWT无状态认证的登出处理 + * + * 这个接口展示了JWT认证系统中登出功能的实现方式 + * 由于JWT是无状态的令牌,服务端不存储会话信息,因此登出的处理方式与传统session不同 + * + * JWT登出的特殊性: + * 1. 无状态特性:JWT令牌是自包含的,服务端不存储令牌状态 + * 2. 令牌有效期:令牌在过期前始终有效,无法在服务端直接"销毁" + * 3. 客户端责任:真正的登出主要依赖客户端删除存储的令牌 + * + * 登出流程: + * 1. 客户端发送登出请求(携带JWT令牌用于身份验证) + * 2. 服务端验证令牌有效性(确保是合法的登出请求) + * 3. 记录用户登出日志(用于安全审计和用户行为分析) + * 4. 清理服务端相关状态(如缓存的用户信息、临时数据等) + * 5. 返回登出成功响应 + * 6. 客户端收到响应后删除本地存储的JWT令牌 + * + * 可选的增强安全措施: + * 1. 令牌黑名单:将登出的令牌加入黑名单,拒绝后续使用 + * - 优点:可以立即使令牌失效 + * - 缺点:需要额外的存储空间和查询开销 + * 2. 短期令牌 + 刷新令牌:使用较短的访问令牌过期时间 + * - 减少令牌被滥用的时间窗口 + * - 通过刷新令牌机制保持用户体验 + * 3. 设备绑定:令牌与特定设备绑定,增加安全性 + * + * 客户端配合工作: + * 1. 删除本地存储的JWT令牌(localStorage、sessionStorage等) + * 2. 清除相关的用户状态和缓存数据 + * 3. 重定向到登录页面或首页 + * 4. 停止所有需要认证的后台请求 + * + * 安全考虑: + * - 即使用户已登出,令牌在过期前仍可能被恶意使用 + * - 建议使用HTTPS防止令牌在传输过程中被截获 + * - 定期更换JWT签名密钥增强安全性 + * - 监控异常的令牌使用模式 + * + * @return Result 登出结果响应 + * - 成功:返回"登出成功"消息 + * - 失败:返回相应的错误信息 + * @PostMapping("/logout") POST请求映射,处理/api/auth/logout路径的POST请求 + */ + @PostMapping("/logout") + @Operation(summary = "用户登出", description = "用户登出,清除认证信息并将token加入黑名单") + public Result logout(HttpServletRequest request) { + try { + // 从请求中提取JWT token + String token = getJwtFromRequest(request); + + if (token != null) { + // 将token加入黑名单,使其立即失效 + tokenBlacklistService.addToBlacklist(token); + log.info("Token已加入黑名单: {}", token.substring(0, Math.min(token.length(), 20)) + "..."); + } + + // 清除Spring Security上下文中的认证信息 + // 这样用户下次访问需要认证的接口时就需要重新登录 + SecurityContextHolder.clearContext(); + log.info("用户登出成功"); + // 返回登出成功的响应结果 + return Result.success("登出成功,token已失效"); + } catch (Exception e) { + // 捕获异常并记录错误日志 + log.error("用户登出失败: {}", e.getMessage()); + // 返回登出失败的响应结果 + return Result.error("登出失败: " + e.getMessage()); + } + } + + /** + * 测试接口 - JWT认证系统的功能验证接口 + * + * 这是一个专门用于开发和测试阶段的接口,用来验证JWT认证系统是否正常工作 + * 它可以帮助开发者快速检查认证流程的各个环节 + * + * 接口用途: + * 1. 验证JWT令牌解析:确认服务端能正确解析客户端发送的JWT令牌 + * 2. 验证用户身份提取:确认能从令牌中正确提取用户信息 + * 3. 验证权限控制:确认只有有效令牌才能访问受保护的接口 + * 4. 验证过滤器链:确认JwtAuthenticationFilter正常工作 + * 5. 调试认证问题:当认证出现问题时,可以通过此接口定位问题 + * + * 测试场景: + * 1. 正常情况测试: + * - 使用有效的JWT令牌访问 + * - 应该返回成功响应和用户信息 + * 2. 异常情况测试: + * - 不携带令牌:应该返回401 Unauthorized + * - 携带无效令牌:应该返回401 Unauthorized + * - 携带过期令牌:应该返回401 Unauthorized + * - 携带被篡改的令牌:应该返回401 Unauthorized + * + * 使用方法: + * 1. 首先通过/api/auth/login接口获取JWT令牌 + * 2. 在请求头中添加:Authorization: Bearer + * 3. 发送GET请求到/api/auth/test + * 4. 检查返回结果是否包含正确的用户信息 + * + * 返回信息说明: + * - 用户名:从JWT令牌中解析出的用户标识 + * - 权限列表:用户拥有的角色和权限信息 + * - 认证状态:确认用户已通过认证 + * - 当前时间:服务器处理请求的时间戳 + * + * 生产环境注意事项: + * - 此接口仅用于开发和测试,生产环境中应该移除或限制访问 + * - 可以通过配置文件控制是否启用此接口 + * - 建议添加访问频率限制,防止被恶意调用 + * - 不要在返回信息中包含敏感数据 + * + * 调试技巧: + * - 查看日志中的JWT解析过程 + * - 使用浏览器开发者工具检查请求头 + * - 使用在线JWT解码工具验证令牌内容 + * - 检查系统时间,确保令牌未过期 + * + * @return Result 测试结果响应 + * 成功时返回包含用户信息的测试消息: + * - 用户名 + * - 权限列表 + * - 当前时间 + * - 认证状态确认 + * @GetMapping("/test") GET请求映射,处理/api/auth/test路径的GET请求 + */ + @GetMapping("/test") + @Operation(summary = "测试接口", description = "需要JWT认证的测试接口") + public Result test() { + // 从Spring Security上下文中获取当前认证信息 + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + // 获取认证用户的用户名 + String username = authentication.getName(); + // 返回测试成功的响应结果,包含用户名 + return Result.success("Hello, " + username + "! JWT认证成功!"); + } + + /** + * 从HTTP请求中提取JWT Token + * + * 这个私有方法用于从HTTP请求的Authorization头中提取JWT令牌 + * 支持标准的Bearer Token格式:Authorization: Bearer + * + * 提取流程: + * 1. 从请求头中获取Authorization字段的值 + * 2. 检查该值是否以"Bearer "开头(注意Bearer后面有一个空格) + * 3. 如果格式正确,提取Bearer后面的令牌部分 + * 4. 返回纯净的JWT令牌字符串 + * + * 安全考虑: + * - 只接受标准的Bearer Token格式,拒绝其他格式 + * - 对提取的令牌进行基本的格式验证 + * - 不在日志中记录完整的令牌内容,避免泄露 + * + * @param request HTTP请求对象 + * @return String JWT令牌字符串,如果未找到或格式不正确则返回null + */ + private String getJwtFromRequest(HttpServletRequest request) { + // 从请求头中获取Authorization字段 + String bearerToken = request.getHeader("Authorization"); + + // 检查Authorization头是否存在且以"Bearer "开头 + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + // 提取Bearer后面的令牌部分(去掉"Bearer "前缀) + return bearerToken.substring(7); + } + + // 如果没有找到有效的Bearer Token,返回null + return null; + } + +} \ No newline at end of file diff --git a/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..8152dc3 --- /dev/null +++ b/user-service/user-service-bootstrap/src/main/java/com/example/user/service/bootstrap/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,351 @@ +package com.example.user.service.bootstrap.security.filter; + +import com.example.user.service.application.service.TokenBlacklistService; +import com.example.user.service.common.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +/** + * JWT认证过滤器 - Spring Security认证体系的核心组件 + * + * 【什么是过滤器?】 + * 过滤器(Filter)是Java Web开发中的重要组件,它像一个"检查站",在请求到达Controller之前 + * 对每个HTTP请求进行预处理和验证。想象成机场安检,每个乘客(请求)都必须通过安检(过滤器) + * 才能登机(到达Controller)。 + * + * 【JWT认证过滤器的作用】 + * 1. 拦截所有HTTP请求 + * 2. 检查请求头中是否包含有效的JWT Token + * 3. 验证Token的合法性(是否过期、签名是否正确) + * 4. 如果Token有效,将用户信息设置到Spring Security上下文中 + * 5. 如果Token无效或不存在,让请求继续,由其他安全机制处理 + * + * 【为什么继承OncePerRequestFilter?】 + * OncePerRequestFilter确保每个请求只被过滤一次,避免重复验证造成性能问题。 + * 在复杂的Web应用中,一个请求可能会经过多个过滤器,这个基类保证了我们的 + * JWT验证逻辑只执行一次。 + * + * 【Spring Security认证流程】 + * 1. 用户发送请求 → 2. JWT过滤器验证Token → 3. 设置认证上下文 → 4. 后续代码可以获取用户信息 + * + * 【注解说明】 + * @Slf4j: Lombok注解,自动生成日志对象log,用于记录调试和错误信息 + * @Component: Spring注解,将该类注册为Spring容器管理的Bean,可以被自动注入 + */ +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + /** + * JWT工具类 - Token操作的核心工具 + * + * 【作用说明】 + * 负责JWT Token的生成、解析、验证等所有操作 + * + * 【@Autowired注解】 + * Spring的依赖注入注解,Spring容器会自动找到JwtUtil类型的Bean并注入到这里 + * 这样我们就不需要手动创建JwtUtil对象,Spring帮我们管理对象的生命周期 + */ + @Autowired + private JwtUtil jwtUtil; + + /** + * Token黑名单服务 - 管理已失效的JWT Token + * + * 【作用说明】 + * 用于解决JWT无状态特性导致的logout后token仍然有效的问题 + * 通过维护一个黑名单,记录已经失效的token + * + * 【JWT无状态认证的挑战】 + * JWT设计为无状态,服务器不保存token信息,这带来了扩展性优势 + * 但也带来了问题:无法主动使token失效(如用户logout) + * + * 【黑名单解决方案】 + * 1. 用户logout时,将token加入黑名单 + * 2. 每次验证token时,先检查是否在黑名单中 + * 3. 黑名单中的token被视为无效,拒绝访问 + * + * 【性能考虑】 + * 黑名单检查会增加一定的性能开销,但这是安全性的必要代价 + * 在生产环境中,建议使用Redis等高性能缓存来存储黑名单 + */ + @Autowired + private TokenBlacklistService tokenBlacklistService; + + /** + * Spring应用上下文 - Spring容器的入口 + * + * 【作用说明】 + * ApplicationContext是Spring容器的核心接口,通过它可以获取容器中的任何Bean + * + * 【为什么需要它?】 + * 在某些情况下,我们需要动态获取Bean,而不是通过@Autowired静态注入 + * 特别是在解决循环依赖问题时,这种方式非常有用 + */ + @Autowired + private ApplicationContext applicationContext; + + /** + * 用户详情服务 - 用户信息加载器 + * + * 【设计说明】 + * 注意这里没有使用@Autowired注解,而是通过延迟加载的方式获取 + * 这是为了避免循环依赖问题: + * JwtAuthenticationFilter需要UserDetailsService, + * 而UserDetailsService的实现类可能也需要其他被JwtAuthenticationFilter保护的组件 + * + * 【延迟加载的好处】 + * 只有在真正需要时才获取Bean,避免启动时的循环依赖问题 + */ + private UserDetailsService userDetailsService; + + /** + * 获取用户详情服务实例 - 延迟加载模式的实现 + * + * 【延迟加载模式(Lazy Loading)】 + * 这是一种设计模式,只有在真正需要对象时才创建或获取它 + * 类似于"用时再买"的概念,避免提前准备造成的资源浪费 + * + * 【解决循环依赖的核心方法】 + * 循环依赖问题:A需要B,B需要A,如果同时创建会造成死锁 + * 解决方案:A先创建,需要B时再去获取B,这样打破了循环 + * + * 【实现原理】 + * 1. 第一次调用时,userDetailsService为null,通过ApplicationContext获取Bean + * 2. 后续调用直接返回已获取的实例,提高性能 + * 3. 这种方式叫做"单例模式 + 延迟初始化" + * + * 【为什么不直接用@Autowired?】 + * 如果直接用@Autowired,Spring在启动时就要解决所有依赖关系 + * 可能会遇到循环依赖导致启动失败 + * + * @return UserDetailsService 用户详情服务实例 + */ + private UserDetailsService getUserDetailsService() { + if (userDetailsService == null) { + // 通过Spring容器动态获取Bean,避免循环依赖 + userDetailsService = applicationContext.getBean(UserDetailsService.class); + } + return userDetailsService; + } + + /** + * 过滤器核心方法 - JWT认证的完整流程实现 + * + * 【方法执行时机】 + * 每个HTTP请求到达Controller之前都会执行这个方法 + * 这是Spring Security认证链中的关键环节 + * + * 【完整的JWT认证流程】 + * 1. 提取Token:从HTTP请求头的Authorization字段中提取JWT Token + * 2. 解析Token:使用JWT工具类解析Token,获取用户名 + * 3. 验证Token:检查Token是否过期、签名是否正确 + * 4. 加载用户:从数据库加载用户详细信息 + * 5. 创建认证:创建Spring Security认证对象 + * 6. 设置上下文:将认证信息存储到SecurityContext中 + * 7. 继续处理:调用过滤器链,让请求继续向下传递 + * + * 【Spring Security上下文的作用】 + * SecurityContext就像一个"身份证明",一旦设置成功,后续的所有代码 + * 都可以通过SecurityContextHolder.getContext()获取当前用户信息 + * + * 【异常处理策略】 + * 如果JWT验证失败,不会阻止请求继续执行,而是清除认证上下文 + * 让Spring Security的其他机制(如返回401未授权)来处理 + * + * @param request HTTP请求对象,包含客户端发送的所有信息 + * @param response HTTP响应对象,用于向客户端发送响应 + * @param filterChain 过滤器链,用于调用下一个过滤器或最终的Controller + * @throws ServletException Servlet相关异常 + * @throws IOException 输入输出异常 + */ + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + + try { + // 【步骤1:提取JWT Token】 + // 从HTTP请求头的Authorization字段中提取Token + // 标准格式:Authorization: Bearer + String jwt = getJwtFromRequest(request); + + // 【步骤2:检查Token是否存在】 + // StringUtils.hasText()检查字符串是否不为null且不为空字符串 + // 如果没有Token,说明可能是匿名访问或登录请求 + if (StringUtils.hasText(jwt)) { + // 【步骤2.5:检查Token是否在黑名单中】 + // 这是新增的安全检查,解决JWT无状态认证的logout问题 + // 如果token在黑名单中,说明用户已经logout,应该拒绝访问 + if (tokenBlacklistService.isBlacklisted(jwt)) { + log.warn("Token is blacklisted (user has logged out): {}", + jwt.substring(0, Math.min(20, jwt.length())) + "..."); + // 清除认证上下文,确保不会认证成功 + SecurityContextHolder.clearContext(); + // 继续过滤器链,让Spring Security返回401未授权 + filterChain.doFilter(request, response); + return; + } + + // 【步骤3:解析Token获取用户名】 + // 这一步会解析JWT的payload部分,提取用户名信息 + // 如果Token格式错误或签名无效,会抛出异常 + String username = jwtUtil.getUsernameFromToken(jwt); + + // 【步骤4:检查是否需要认证】 + // username != null: 确保成功从Token中提取到用户名 + // SecurityContextHolder.getContext().getAuthentication() == null: 确保当前请求还没有认证信息 + // 这样避免重复认证,提高性能 + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + // 【步骤5:加载用户详细信息】 + // 通过用户名从数据库加载完整的用户信息(包括权限、角色等) + // 这里使用了延迟加载的UserDetailsService,避免循环依赖 + UserDetails userDetails = getUserDetailsService().loadUserByUsername(username); + + // 【步骤6:验证Token的完整性】 + // 不仅要检查Token格式,还要验证: + // 1. Token是否过期 + // 2. 签名是否正确 + // 3. Token中的用户名是否与数据库中的一致 + if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { + // 【步骤7:创建Spring Security认证对象】 + // UsernamePasswordAuthenticationToken是Spring Security的标准认证对象 + // 三个参数的含义: + // 1. principal: 主体,通常是用户详情对象 + // 2. credentials: 凭据,JWT模式下不需要密码,设为null + // 3. authorities: 权限列表,从用户详情中获取 + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + userDetails, // 用户详情(包含用户名、权限等) + null, // 凭据(JWT认证不需要密码) + userDetails.getAuthorities() // 用户权限列表 + ); + + // 【步骤8:设置认证详情】 + // 添加额外的认证信息,如IP地址、Session ID等 + // 这些信息在安全审计和日志记录中很有用 + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + // 【步骤9:设置到Spring Security上下文】 + // 这是最关键的一步!将认证信息存储到SecurityContext中 + // 后续的Controller、Service等都可以通过SecurityContextHolder获取当前用户信息 + // 这就是Spring Security"记住"用户身份的机制 + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 记录成功的认证日志,便于调试和监控 + log.debug("JWT authentication successful for user: {}", username); + } else { + // Token验证失败,记录警告日志 + // 可能的原因:Token过期、签名错误、用户名不匹配等 + log.warn("JWT token validation failed for user: {}", username); + } + } + } + } catch (Exception e) { + // 【异常处理:安全优先原则】 + // 任何异常都不应该影响系统的安全性 + // 记录错误日志,便于排查问题 + log.error("Cannot set user authentication: {}", e.getMessage()); + // 清除认证上下文,确保不会使用错误或不完整的认证信息 + // 这是安全编程的重要原则:出错时选择更安全的状态 + SecurityContextHolder.clearContext(); + } + + // 继续过滤器链,让请求继续向下处理 + filterChain.doFilter(request, response); + } + + /** + * 从请求中提取JWT Token - HTTP标准认证头解析 + * + * 【HTTP Authorization头标准】 + * HTTP协议规定,认证信息应该放在Authorization请求头中 + * JWT认证的标准格式:Authorization: Bearer + * + * 【Bearer认证方案】 + * Bearer是OAuth 2.0规范中定义的认证方案 + * 意思是"持有者",表示谁持有这个Token,谁就有相应的权限 + * 格式:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + * + * 【为什么要单独提取?】 + * 1. 代码复用:多个地方可能需要提取Token + * 2. 职责分离:Token提取和Token验证是不同的职责 + * 3. 易于测试:可以单独测试Token提取逻辑 + * + * @param request HTTP请求对象,包含所有请求头信息 + * @return JWT Token字符串,如果不存在或格式错误则返回null + */ + private String getJwtFromRequest(HttpServletRequest request) { + // 从请求头中获取Authorization字段的值 + // 例如:"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + String bearerToken = request.getHeader("Authorization"); + // 使用JwtUtil工具类提取实际的Token部分(去掉"Bearer "前缀) + return jwtUtil.extractTokenFromHeader(bearerToken); + } + + /** + * 判断是否需要过滤 - 实现白名单机制 + * + * 【白名单机制的重要性】 + * 不是所有的接口都需要JWT认证,有些接口必须是公开的: + * 1. 登录接口:用户还没有Token,怎么能要求提供Token? + * 2. 注册接口:新用户注册时也没有Token + * 3. 文档接口:开发时需要查看API文档 + * + * 【OncePerRequestFilter的设计】 + * OncePerRequestFilter提供了shouldNotFilter方法,让我们可以灵活控制 + * 哪些请求需要过滤,哪些不需要。返回true表示跳过过滤。 + * + * 【安全考虑】 + * 白名单要谨慎设置,只有真正需要公开访问的接口才能加入白名单 + * 过多的白名单会降低系统安全性 + * + * 【路径匹配策略】 + * 使用startsWith进行前缀匹配,这样可以匹配一类接口 + * 例如:/user/login 可以匹配 /user/login、/user/login?username=xxx 等 + * + * @param request HTTP请求对象 + * @return boolean 是否不需要过滤,true表示跳过JWT验证 + * @throws ServletException Servlet异常 + */ + @Override + protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { + // 获取请求的URI路径 + String path = request.getRequestURI(); + + // 【业务接口白名单】 + // /user/login: 用户登录接口,用户通过用户名密码获取Token + // /user/register: 用户注册接口,新用户创建账号 + boolean isBusinessWhitelist = path.startsWith("/user/login") || + path.startsWith("/user/register"); + + // 【文档接口白名单】 + // 这些是Swagger API文档相关的接口,开发阶段需要公开访问 + // /doc.html: Knife4j文档首页 + // /swagger-ui: Swagger UI界面 + // /v3/api-docs: OpenAPI 3.0规范的JSON文档 + // /webjars: 前端资源文件(CSS、JS等) + boolean isDocWhitelist = path.startsWith("/doc.html") || + path.startsWith("/swagger-ui") || + path.startsWith("/v3/api-docs") || + path.startsWith("/webjars"); + + // 返回true表示不需要JWT验证,false表示需要验证 + return isBusinessWhitelist || isDocWhitelist; + } +} \ No newline at end of file diff --git a/user-service/user-service-common/pom.xml b/user-service/user-service-common/pom.xml index 20aa5fc..d1d701d 100644 --- a/user-service/user-service-common/pom.xml +++ b/user-service/user-service-common/pom.xml @@ -14,6 +14,24 @@ 3.2.4 + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + org.springframework.boot spring-boot-starter diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java index f655938..6bc9f28 100644 --- a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java +++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java @@ -1,78 +1,345 @@ package com.example.user.service.common; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import javax.crypto.SecretKey; import java.util.Date; -import java.util.HashMap; -import java.util.Map; - +/** + * JWT工具类 - JSON Web Token 核心处理工具 + * + * 【什么是JWT?】 + * JWT (JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。 + * 它是一种无状态的认证方式,服务器不需要保存用户的登录状态。 + * + * 【JWT的结构】 + * JWT由三部分组成,用点(.)分隔: + * 1. Header(头部):包含token类型和签名算法,如 {"typ":"JWT","alg":"HS512"} + * 2. Payload(载荷):包含声明信息,如用户名、过期时间等,如 {"sub":"user123","exp":1234567890} + * 3. Signature(签名):用于验证token的完整性,防止篡改 + * 格式:Header.Payload.Signature + * 例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c + * + * 【为什么使用JWT?】 + * 1. 无状态:服务器不需要存储session,适合分布式系统 + * 2. 跨域:可以在不同域名间传递认证信息 + * 3. 移动友好:适合移动应用的认证方式 + * 4. 性能好:避免了频繁的数据库查询 + * + * 【安全考虑】 + * 1. 密钥安全:签名密钥必须保密,泄露会导致token可被伪造 + * 2. HTTPS传输:token应通过HTTPS传输,防止被截获 + * 3. 过期时间:设置合理的过期时间,平衡安全性和用户体验 + * 4. 敏感信息:不要在payload中存储敏感信息,因为它只是Base64编码,不是加密 + * + * 【注解说明】 + * @Slf4j:Lombok注解,自动生成日志对象log,用于记录调试和错误信息 + * @Component:Spring注解,将该类注册为Spring容器管理的Bean,可以被其他类注入使用 + */ @Slf4j +@Component public class JwtUtil { + /** + * JWT签名密钥 - 用于保证token安全性的核心要素 + * + * 【作用说明】 + * 这个密钥用于对JWT进行签名和验证,确保token的完整性和真实性。 + * 任何人如果没有这个密钥,就无法伪造有效的JWT token。 + * + * 【配置方式】 + * @Value注解从application.properties或application.yml中读取jwt.secret配置 + * 如果配置文件中没有设置,则使用冒号后面的默认值 + * + * 【安全要求】 + * 1. 密钥长度:至少256位(32字节)才能满足HS256算法要求 + * 2. 密钥复杂度:应包含大小写字母、数字,避免使用简单密码 + * 3. 密钥保密:绝对不能泄露给客户端或第三方 + * 4. 生产环境:必须使用环境变量或加密配置文件存储 + * + * 【为什么这样设计?】 + * 使用@Value注解可以灵活配置密钥,不同环境(开发、测试、生产)可以使用不同的密钥 + */ + @Value("${jwt.secret:mySecretKeyForJwtTokenGenerationAndValidation123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}") + private static String secret; - private static final String SECRET_KEY = "123456"; - - - private static final long EXPIRATION_TIME = 5 * 60 * 1000; // 5分钟 - + /** + * JWT token过期时间(毫秒) - 控制token有效期的关键参数 + * + * 【时间设置说明】 + * 默认值86400000毫秒 = 24小时 = 1天 + * 计算方式:24小时 × 60分钟 × 60秒 × 1000毫秒 = 86400000毫秒 + * + * 【为什么需要过期时间?】 + * 1. 安全考虑:限制token的有效期,即使被盗用也会自动失效 + * 2. 减少风险:缩短攻击者可利用stolen token的时间窗口 + * 3. 强制重新认证:定期要求用户重新登录,提高安全性 + * + * 【时间设置策略】 + * - 高安全应用:1-2小时 + * - 一般应用:24小时 + * - 低风险应用:7天 + * - 移动应用:可以设置更长时间,配合refresh token使用 + * + * 【配置灵活性】 + * 通过@Value注解,可以在不同环境中设置不同的过期时间 + */ + @Value("${jwt.expiration:86400000}") + private static Long expiration; + /** + * 生成JWT token - 用户登录成功后创建身份凭证 + * + * 【方法作用】 + * 当用户登录成功后,调用此方法生成一个JWT token作为用户的身份凭证。 + * 用户后续的请求都需要携带这个token来证明自己的身份。 + * + * 【生成流程】 + * 1. 获取当前时间作为token的签发时间 + * 2. 计算token的过期时间(当前时间 + 配置的有效期) + * 3. 使用JJWT库的Builder模式构建token + * 4. 设置token的各个声明(Claims) + * 5. 使用密钥和算法对token进行签名 + * 6. 生成最终的token字符串 + * + * 【参数说明】 + * @param username 用户名,将作为token的主体(subject),用于标识token属于哪个用户 + * + * 【返回值说明】 + * @return String 生成的JWT token,格式为:Header.Payload.Signature + * + * 【使用场景】 + * - 用户登录成功后 + * - 需要为用户创建新的身份凭证时 + * - token刷新时(如果实现了refresh token机制) + */ + public static String generateToken(String username) { + // 获取当前时间 - 作为token的签发时间(iat - issued at) + Date now = new Date(); + // 计算过期时间 - 当前时间加上配置的有效期 + // 这样设计可以灵活控制token的生命周期 + Date expiryDate = new Date(now.getTime() + expiration); - public static String generateToken(Long userId, String username, Boolean isSuper) { - Map claims = new HashMap<>(); - claims.put("id", userId); - claims.put("name", username); - claims.put("is_super", isSuper); - + // 使用JJWT库的Builder模式构建JWT Token + // Builder模式的优点:代码清晰、易于理解、支持链式调用 return Jwts.builder() - .setClaims(claims) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) - .signWith(SignatureAlgorithm.HS256, SECRET_KEY) - .compact(); + .setSubject(username) // 设置主体(sub):标识token的所有者 + .setIssuedAt(now) // 设置签发时间(iat):token的创建时间 + .setExpiration(expiryDate) // 设置过期时间(exp):token的失效时间 + .signWith(getSigningKey(), SignatureAlgorithm.HS512) // 使用HS512算法和密钥进行签名 + .compact(); // 生成紧凑的URL安全字符串格式 } - - public static Claims parseToken(String token) { - return Jwts.parser() - .setSigningKey(SECRET_KEY) - .parseClaimsJws(token) - .getBody(); + /** + * 从token中获取用户名 - 解析JWT获取用户身份信息 + * + * 【方法作用】 + * 当收到客户端发送的JWT token时,需要从中提取用户名来识别用户身份。 + * 这是JWT认证流程中的关键步骤。 + * + * 【解析过程】 + * 1. 调用getClaimsFromToken()方法解析token + * 2. 从Claims中获取subject字段(用户名) + * 3. 返回用户名供后续业务逻辑使用 + * + * 【为什么这样设计?】 + * JWT的payload部分包含了用户信息,通过解析可以直接获取, + * 无需查询数据库,提高了性能。 + * + * @param token JWT token字符串 + * @return String 用户名,如果token格式错误或解析失败则可能抛出异常 + * + * 【注意事项】 + * - 此方法不验证token是否过期,只负责提取用户名 + * - 如果token被篡改,解析时会抛出签名验证异常 + */ + public String getUsernameFromToken(String token) { + // 解析token获取载荷(Claims) - Claims包含了token中的所有声明信息 + Claims claims = getClaimsFromToken(token); + // 从载荷中获取主体(subject),即用户名 + // subject是JWT标准中用于标识token所有者的字段 + return claims.getSubject(); } - - public static Long getUserIdFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("id", Long.class); + /** + * 验证token是否有效 - JWT认证的核心验证逻辑 + * + * 【验证目的】 + * 确保接收到的JWT token是合法、有效且未被篡改的。 + * 这是保护API安全的重要防线。 + * + * 【验证步骤】 + * 1. 从token中提取用户名 + * 2. 比较token中的用户名与期望的用户名是否一致 + * 3. 检查token是否已过期 + * 4. 隐式验证:getClaimsFromToken()会验证签名 + * + * 【为什么需要双重验证?】 + * - 用户名匹配:确保token属于正确的用户 + * - 过期检查:确保token仍在有效期内 + * - 签名验证:确保token未被篡改(在解析过程中自动完成) + * + * @param token JWT token字符串 + * @param username 期望的用户名,通常来自请求上下文 + * @return boolean true表示token有效,false表示无效 + * + * 【异常处理】 + * 使用try-catch捕获所有可能的异常(签名错误、格式错误等), + * 确保方法不会因异常而中断,统一返回false表示验证失败。 + */ + public boolean validateToken(String token, String username) { + try { + // 从token中获取用户名 - 这一步会验证token的签名 + String tokenUsername = getUsernameFromToken(token); + // 验证用户名是否匹配且token未过期 + // 两个条件都必须满足才认为token有效 + return (username.equals(tokenUsername) && !isTokenExpired(token)); + } catch (Exception e) { + // 捕获解析token时的异常(如签名错误、格式错误等) + // 记录错误日志便于调试,但不暴露具体错误信息给客户端 + log.error("Token validation failed: {}", e.getMessage()); + return false; // 任何异常都视为验证失败 + } } - - public static String getUsernameFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("name", String.class); + /** + * 检查token是否过期 + * + * @param token JWT token + * @return boolean token是否过期 + */ + public boolean isTokenExpired(String token) { + // 获取token的过期时间 + Date expiration = getExpirationDateFromToken(token); + // 比较过期时间是否在当前时间之前 + return expiration.before(new Date()); } - public static Boolean getIsSuperFromToken(String token) { - Claims claims = parseToken(token); - return claims.get("is_super", Boolean.class); + /** + * 从token中获取过期时间 + * + * @param token JWT token + * @return Date 过期时间 + */ + public Date getExpirationDateFromToken(String token) { + // 解析token获取载荷(Claims) + Claims claims = getClaimsFromToken(token); + // 从载荷中获取过期时间 + return claims.getExpiration(); } + /** + * 从token中解析Claims - JWT解析的核心方法 + * + * 【Claims概念】 + * Claims是JWT的载荷(Payload)部分,包含了关于用户和token的声明信息。 + * 标准声明包括:sub(主体)、exp(过期时间)、iat(签发时间)等。 + * + * 【解析流程】 + * 1. 创建JWT解析器(Parser) + * 2. 设置签名密钥用于验证token完整性 + * 3. 解析token字符串,验证签名 + * 4. 提取并返回Claims对象 + * + * 【安全机制】 + * 解析过程中会自动验证token的签名,如果token被篡改, + * 签名验证会失败并抛出异常,确保了token的安全性。 + * + * 【为什么设计为private?】 + * 这是一个内部工具方法,只供本类的其他方法使用, + * 不需要暴露给外部调用者,符合封装原则。 + * + * @param token JWT token字符串 + * @return Claims 载荷对象,包含token中的所有声明信息 + * @throws JwtException 如果token格式错误、签名无效或已过期 + */ + private Claims getClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) // 设置签名密钥用于验证token完整性 + .build() // 构建解析器 + .parseClaimsJws(token) // 解析JWT token并验证签名 + .getBody(); // 获取载荷部分(Claims) + } - public static boolean validateToken(String token) { - try { - parseToken(token); - return true; - } catch (Exception e) { - log.error("JWT令牌验证失败: {}", e.getMessage()); - return false; - } + /** + * 获取签名密钥 - 将配置的字符串密钥转换为加密算法所需的密钥对象 + * + * 【转换目的】 + * JJWT库需要SecretKey对象来进行HMAC签名算法, + * 而我们配置的是字符串,需要转换为合适的密钥格式。 + * + * 【HMAC算法说明】 + * HMAC (Hash-based Message Authentication Code) 是一种基于哈希的消息认证码算法。 + * 它结合了哈希函数和密钥,既能验证数据完整性,又能验证数据来源。 + * + * 【转换过程】 + * 1. 将字符串密钥转换为字节数组 + * 2. 使用Keys.hmacShaKeyFor()方法生成适合HMAC-SHA算法的密钥 + * 3. 返回SecretKey对象供签名和验证使用 + * + * 【安全考虑】 + * - 密钥长度必须足够(至少256位) + * - 密钥应该是随机生成的,不能是简单的字符串 + * - 密钥必须保密,不能泄露给客户端 + * + * @return SecretKey 适用于HMAC-SHA算法的签名密钥 + */ + private static SecretKey getSigningKey() { + // 将字符串密钥转换为字节数组 + // 使用UTF-8编码确保字符串到字节的转换一致性 + byte[] keyBytes = secret.getBytes(); + // 使用JJWT提供的工具方法生成HMAC-SHA密钥 + // 该方法会自动选择合适的HMAC算法(HS256、HS384、HS512) + return Keys.hmacShaKeyFor(keyBytes); } - public static boolean isTokenExpired(String token) { - Claims claims = parseToken(token); - return claims.getExpiration().before(new Date()); + /** + * 从请求头中提取token - 解析HTTP Authorization头获取JWT token + * + * 【HTTP Authorization头格式】 + * 根据RFC 6750 (OAuth 2.0 Bearer Token Usage)标准, + * JWT token应该放在HTTP请求头的Authorization字段中, + * 格式为: "Authorization: Bearer " + * + * 【为什么使用Bearer?】 + * Bearer是OAuth 2.0标准中定义的token类型,表示"持有者token", + * 意思是任何持有该token的人都可以使用它,无需额外的身份验证。 + * + * 【提取逻辑】 + * 1. 检查Authorization头是否存在 + * 2. 验证是否以"Bearer "开头(注意Bearer后有一个空格) + * 3. 提取"Bearer "后面的token部分 + * 4. 如果格式不正确,返回null + * + * 【使用场景】 + * 主要在JWT认证过滤器中使用,用于从HTTP请求中提取token, + * 然后进行后续的验证和用户身份识别。 + * + * 【安全考虑】 + * - 只接受标准的Bearer格式,拒绝其他格式 + * - 返回null而不是抛出异常,便于调用者处理 + * - 不对token内容进行验证,只负责提取 + * + * @param authHeader HTTP请求中的Authorization头的值 + * @return String JWT token字符串,如果格式不正确则返回null + * + * 【示例】 + * 输入: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 输出: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + */ + public String extractTokenFromHeader(String authHeader) { + // 检查Authorization头是否存在且以"Bearer "开头 + // 注意:"Bearer "后面有一个空格,这是标准格式要求 + if (authHeader != null && authHeader.startsWith("Bearer ")) { + // 提取Bearer后面的token部分 + // substring(7)是因为"Bearer "有7个字符(包括空格) + return authHeader.substring(7); + } + // 如果格式不正确,返回null,让调用者知道没有有效的token + return null; } } \ No newline at end of file diff --git a/user-service/user-service-domain/pom.xml b/user-service/user-service-domain/pom.xml index 502cbe0..47ee4c1 100644 --- a/user-service/user-service-domain/pom.xml +++ b/user-service/user-service-domain/pom.xml @@ -15,6 +15,17 @@ + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + + org.springframework.boot spring-boot-starter diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java index 8d9521b..4a221fa 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java @@ -21,17 +21,22 @@ public class User { private UserAge age; private Email email; private Password password; - private IsSuper isSuper; // 添加isSuper字段 + private Phone phone; + private RealName realName; + private Status status; + private Role role; + private CreateTime createTime; + private UpdateTime updateTime; + private LastLoginTime lastLoginTime; public User() { } - public User(UserId id, UserName name, UserAge age, Email email, Password password,IsSuper isSuper) { + public User(UserId id, UserName name, UserAge age, Email email, Password password) { this.id = id; this.name = name; this.age = age; this.email = email; this.password = password; - this.isSuper = isSuper; } public User( UserName name, UserAge age, Email email, Password password) { @@ -40,7 +45,7 @@ public class User { this.age = age; this.email = email; this.password = password; - this.isSuper = new IsSuper(false); + } public User(UserId userId, UserName userName, UserAge userAge, Email email) { @@ -48,7 +53,14 @@ public class User { this.name = name; this.age = age; this.email = email; - this.isSuper = new IsSuper(false); + + } + + public User(UserId userId, UserName userName, Email email, Password password) { + this.id = userId; + this.name = userName; + this.email = email; + this.password = password; } diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java index ebb036d..9bb30fb 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/config/PasswordConfig.java @@ -1,14 +1,32 @@ -//package com.example.user.service.domain.config; -// -//import org.springframework.context.annotation.Bean; -//import org.springframework.context.annotation.Configuration; -//import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -//import org.springframework.security.crypto.password.PasswordEncoder; -// -//@Configuration -//public class PasswordConfig { -// @Bean -// public PasswordEncoder passwordEncoder() { -// return new BCryptPasswordEncoder(); -// } -//} +package com.example.user.service.domain.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +/** + * 密码编码器配置类 + * 独立配置以避免循环依赖 + * + * 这个类专门用于配置密码编码器Bean,独立出来可以避免在复杂依赖关系中产生循环依赖问题 + */ +@Configuration +public class PasswordConfig { + + /** + * 密码编码器Bean + * 使用BCrypt算法对密码进行加密和验证 + * + * BCrypt是一种强大的密码哈希函数,具有以下特点: + * 1. 自动加盐(Salt) - 每次加密都会生成随机盐值,防止彩虹表攻击 + * 2. 可配置的计算复杂度 - 可以调整计算轮数来平衡安全性和性能 + * 3. 单向性 - 无法从哈希值反推出原始密码 + * + * @return PasswordEncoder 密码编码器实例 + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java new file mode 100644 index 0000000..d11074c --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/CreateTime.java @@ -0,0 +1,9 @@ +package com.example.user.service.domain.valueobject; + +import java.time.LocalDateTime; + +public record CreateTime(LocalDateTime createTime) { + public LocalDateTime getValue() { + return createTime; + } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java deleted file mode 100644 index f05ce45..0000000 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/IsSuper.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.example.user.service.domain.valueobject; - - -public record IsSuper(boolean value) { - public IsSuper { - - if (value && !isValidSuperUser()) { - throw new IllegalArgumentException("Invalid super user configuration"); - } - } - private boolean isValidSuperUser() { - - return true; - } - - public static IsSuper fromBoolean(boolean value) { - return new IsSuper(value); - } - -} \ No newline at end of file diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java new file mode 100644 index 0000000..ba06c34 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/LastLoginTime.java @@ -0,0 +1,7 @@ +package com.example.user.service.domain.valueobject; + +import java.time.LocalDateTime; + +public record LastLoginTime(LocalDateTime value) { + public LocalDateTime getValue() {return value;} +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java new file mode 100644 index 0000000..578177d --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Phone.java @@ -0,0 +1,5 @@ +package com.example.user.service.domain.valueobject; + +public record Phone(String phone) { + public String getValue() { return phone; } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java new file mode 100644 index 0000000..73fe39e --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/RealName.java @@ -0,0 +1,5 @@ +package com.example.user.service.domain.valueobject; + +public record RealName(String realName) { + public String getValue() { return realName; } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java new file mode 100644 index 0000000..4cfb617 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Role.java @@ -0,0 +1,5 @@ +package com.example.user.service.domain.valueobject; + +public record Role(String role) { + public String getValue() {return role;} +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java new file mode 100644 index 0000000..2dc838a --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/Status.java @@ -0,0 +1,7 @@ +package com.example.user.service.domain.valueobject; + +public record Status(Integer status) { + public Integer getValue() { + return status; + } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java new file mode 100644 index 0000000..8574d02 --- /dev/null +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/valueobject/UpdateTime.java @@ -0,0 +1,7 @@ +package com.example.user.service.domain.valueobject; + +import java.time.LocalDateTime; + +public record UpdateTime(LocalDateTime value) { + public LocalDateTime getValue() {return value;} +} -- Gitee From 138eae3d26adbca34bb5af4fb53d02f63347f117 Mon Sep 17 00:00:00 2001 From: little_jin <861165942@qq.com> Date: Wed, 17 Sep 2025 15:15:44 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86qa=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- qa-service/docker-compose.yml | 250 +++++++++++++++++ qa-service/nacos_config.sql | 179 ++++++++++++ qa-service/pom.xml | 57 +++- qa-service/qa-service-adapter/pom.xml | 39 +++ .../qa-service-adapter/qa-adapter-in/pom.xml | 38 +++ .../qa-adapter-in/qa-adapter-in-web/pom.xml | 67 +++++ .../in/web/controller/qaController.java | 74 +++++ .../in/web/dto/CreateQaRequestDTO.java | 6 + .../xmut/qa/adapter/in/web/dto/QaInfo.java | 50 ++++ .../qa/adapter/in/web/dto/QaResponseDTO.java | 14 + .../xmut/qa/adapter/in/web/dto/Result.java | 149 ++++++++++ .../in/web/dto/UpdateQaRequestDTO.java | 6 + .../web/exception/GlobalExceptionHandler.java | 259 ++++++++++++++++++ .../qa-service-adapter/qa-adapter-out/pom.xml | 38 +++ .../qa-adapter-out-persistence/pom.xml | 63 +++++ .../persistence/bridge/CreateQaBridge.java | 26 ++ .../persistence/bridge/DeleteQaBridge.java | 19 ++ .../persistence/bridge/GetQaListBridge.java | 32 +++ .../persistence/bridge/UpdateQaBridge.java | 24 ++ .../persistence/convertor/QaConvertor.java | 25 ++ .../out/persistence/entity/QaEntity.java | 29 ++ .../out/persistence/mapper/QaMapper.java | 15 + qa-service/qa-service-application/pom.xml | 57 ++++ .../application/command/CreateQaCommand.java | 11 + .../application/command/UpdateQaCommand.java | 9 + .../application/port/in/CreateQaUseCase.java | 8 + .../application/port/in/DeleteQaUseCase.java | 5 + .../application/port/in/GetQaByIdUseCase.java | 7 + .../application/port/in/GetQaListUseCase.java | 10 + .../application/port/in/UpdateQaUseCase.java | 10 + .../application/service/CreateQaService.java | 29 ++ .../application/service/DeleteQaService.java | 17 ++ .../application/service/GetQaByIdService.java | 18 ++ .../application/service/GetQaListService.java | 22 ++ .../application/service/UpdateQaService.java | 26 ++ qa-service/qa-service-bootstrap/pom.xml | 86 ++++++ .../QaServiceBootstrapApplication.java | 13 + .../src/main/resources/application.properties | 25 ++ qa-service/qa-service-common/pom.xml | 103 +++++++ .../xmut/qa/service/common/IdWorker.java | 199 ++++++++++++++ qa-service/qa-service-domain/pom.xml | 81 ++++++ .../example/xmut/qa/service/domain/Qa.java | 37 +++ .../qa/service/domain/port/CreateQaPort.java | 7 + .../qa/service/domain/port/DeleteQaPort.java | 5 + .../qa/service/domain/port/GetQaByIdPort.java | 8 + .../qa/service/domain/port/GetQaListPort.java | 11 + .../qa/service/domain/port/UpdateQaPort.java | 8 + .../qa/service/domain/valueobject/Answer.java | 7 + .../qa/service/domain/valueobject/QaId.java | 7 + .../service/domain/valueobject/Question.java | 7 + 50 files changed, 2281 insertions(+), 11 deletions(-) create mode 100644 qa-service/docker-compose.yml create mode 100644 qa-service/nacos_config.sql create mode 100644 qa-service/qa-service-adapter/pom.xml create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/pom.xml create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/pom.xml create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java create mode 100644 qa-service/qa-service-application/pom.xml create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java create mode 100644 qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java create mode 100644 qa-service/qa-service-bootstrap/pom.xml create mode 100644 qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java create mode 100644 qa-service/qa-service-bootstrap/src/main/resources/application.properties create mode 100644 qa-service/qa-service-common/pom.xml create mode 100644 qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java create mode 100644 qa-service/qa-service-domain/pom.xml create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java create mode 100644 qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java diff --git a/qa-service/docker-compose.yml b/qa-service/docker-compose.yml new file mode 100644 index 0000000..b92c130 --- /dev/null +++ b/qa-service/docker-compose.yml @@ -0,0 +1,250 @@ +networks: + app_net: + driver: bridge +services: + # MySQL 主库 + mysql: + image: mysql:8.4 + container_name: mysql + environment: + - MYSQL_ROOT_PASSWORD=root + - TZ=Asia/Shanghai + - MYSQL_CHARSET=utf8mb4 + - MYSQL_COLLATION=utf8mb4_general_ci + - MYSQL_ROOT_HOST=% + - MYSQL_SSL_MODE=REQUIRED + ports: + - "3306:3306" + privileged: true + volumes: + - ./mysql/conf.d:/etc/mysql/conf.d + - ./mysql/data:/var/lib/mysql + healthcheck: + test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app_net + restart: always + redis: + container_name: redis + image: redis:latest + ports: + - "6379:6379" + privileged: true + healthcheck: + test: [ "CMD", "redis-cli", "ping" ] + interval: 5s + timeout: 3s + retries: 5 + networks: + - app_net + restart: always + + rabbitmq: + container_name: rabbitmq + image: rabbitmq:4.1.2-management + ports: + - "5672:5672" + - "15672:15672" + privileged: true + healthcheck: + test: [ "CMD", "rabbitmqctl", "status" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + minio: + image: quay.io/minio/minio:latest + container_name: minio + restart: always + ports: + - "9000:9000" # S3 API 端口 + - "9001:9001" # Web 控制台端口 + environment: + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: admin123456 + volumes: + - ./minio-data:/data + command: server /data --console-address ":9001" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + # RocketMQ NameServer + rocketmq-namesrv: + image: apache/rocketmq:latest + container_name: rocketmq-namesrv + command: sh mqnamesrv + ports: + - "9876:9876" + environment: + JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9876" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + + # RocketMQ Broker + rocketmq-broker: + image: apache/rocketmq:latest + container_name: rocketmq-broker + command: sh mqbroker -c /home/rocketmq/rocketmq-5.3.3/conf/broker.conf + ports: + - "10909:10909" + - "10911:10911" + - "10912:10912" + environment: + JAVA_OPT_EXT: "-server -Xms128m -Xmx128m -Xmn128m" + NAMESRV_ADDR: "rocketmq-namesrv:9876" + volumes: + - ./rocketmq/conf/broker.conf:/home/rocketmq/rocketmq-5.3.3/conf/broker.conf + - ./rocketmq/logs:/home/rocketmq/logs + - ./rocketmq/store:/home/rocketmq/store + depends_on: + - rocketmq-namesrv + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:10911" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + + # Elasticsearch + elasticsearch: + image: elasticsearch:7.17.28 + container_name: elasticsearch + environment: + - discovery.type=single-node + - ES_JAVA_OPTS=-Xms512m -Xmx512m + - xpack.security.enabled=false + ports: + - "9200:9200" + - "9300:9300" + volumes: + - ./es/data:/usr/share/elasticsearch/data + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:9200" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + + # Nginx + nginx: + image: nginx:latest + container_name: nginx + ports: + - "80:80" + - "443:443" + volumes: + - ./nginx/conf.d:/etc/nginx/conf.d + - ./nginx/html:/usr/share/nginx/html + - ./nginx/logs:/var/log/nginx + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + nacos: + container_name: nacos + image: nacos/nacos-server:v3.0.2 + ports: + - "8848:8848" + - "8080:8080" + - "9848:9848" + - "9849:9849" + environment: + - TZ=Asia/Shanghai + - MODE=standalone + - PREFER_HOST_MODE=hostname + - SPRING_DATASOURCE_PLATFORM=mysql + - MYSQL_SERVICE_HOST=mysql + - MYSQL_SERVICE_DB_NAME=nacos_config + - MYSQL_SERVICE_PORT=3306 + - MYSQL_SERVICE_USER=root + - MYSQL_SERVICE_PASSWORD=root + - MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + - NACOS_AUTH_IDENTITY_KEY=2222 + - NACOS_AUTH_IDENTITY_VALUE=2xxx + - NACOS_AUTH_TOKEN=VGhpc0lzTXlDdXN0b21TZWNyZXRLZXkwMTIzNDU2Nzg= + volumes: + - ./nacos/logs:/home/nacos/logs + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8848/nacos" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + privileged: true + restart: always + # Seata + seata-server: + image: seataio/seata-server:1.6.0 + container_name: seata-server + ports: + - "7091:7091" + - "8091:8091" + networks: + - app_net + restart: always + + # Sentinel + sentinel: + image: bladex/sentinel-dashboard:latest + container_name: sentinel + ports: + - "8858:8858" + environment: + - JAVA_OPTS=-Dserver.port=8858 -Dcsp.sentinel.dashboard.server=localhost:8858 -Dproject.name=sentinel-dashboard + healthcheck: + test: [ "CMD", "curl", "-f", "http://localhost:8858" ] + interval: 30s + timeout: 10s + retries: 3 + networks: + - app_net + restart: always + leaf-server: + image: registry.cn-hangzhou.aliyuncs.com/itheima/meituan-leaf:1.0.1 + container_name: leaf-server + ports: + - "8090:8080" + environment: + - SPRING_PROFILES_ACTIVE=prod + volumes: + - ./leaf/application.properties:/leaf-server/config/application.properties + networks: + - app_net + + zookeeper: + image: bitnami/zookeeper:latest + container_name: zookeeper + ports: + - "2181:2181" + environment: + - ALLOW_ANONYMOUS_LOGIN=yes + networks: + - app_net + restart: always \ No newline at end of file diff --git a/qa-service/nacos_config.sql b/qa-service/nacos_config.sql new file mode 100644 index 0000000..433a90b --- /dev/null +++ b/qa-service/nacos_config.sql @@ -0,0 +1,179 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/******************************************/ +/* 表名称 = config_info */ +/******************************************/ +CREATE TABLE `config_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) DEFAULT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `c_desc` varchar(256) DEFAULT NULL COMMENT 'configuration description', + `c_use` varchar(64) DEFAULT NULL COMMENT 'configuration usage', + `effect` varchar(64) DEFAULT NULL COMMENT '配置生效的描述', + `type` varchar(64) DEFAULT NULL COMMENT '配置的类型', + `c_schema` text COMMENT '配置的模式', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info'; + +/******************************************/ +/* 表名称 = config_info since 2.5.0 */ +/******************************************/ +CREATE TABLE `config_info_gray` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `src_user` text COMMENT 'src_user', + `src_ip` varchar(100) DEFAULT NULL COMMENT 'src_ip', + `gmt_create` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_create', + `gmt_modified` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'gmt_modified', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `gray_name` varchar(128) NOT NULL COMMENT 'gray_name', + `gray_rule` text NOT NULL COMMENT 'gray_rule', + `encrypted_data_key` varchar(256) NOT NULL DEFAULT '' COMMENT 'encrypted_data_key', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_configinfogray_datagrouptenantgray` (`data_id`,`group_id`,`tenant_id`,`gray_name`), + KEY `idx_dataid_gmt_modified` (`data_id`,`gmt_modified`), + KEY `idx_gmt_modified` (`gmt_modified`) +) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='config_info_gray'; + +/******************************************/ +/* 表名称 = config_tags_relation */ +/******************************************/ +CREATE TABLE `config_tags_relation` ( + `id` bigint(20) NOT NULL COMMENT 'id', + `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', + `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', + `nid` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增长标识', + PRIMARY KEY (`nid`), + UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation'; + +/******************************************/ +/* 表名称 = group_capacity */ +/******************************************/ +CREATE TABLE `group_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_group_id` (`group_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表'; + +/******************************************/ +/* 表名称 = his_config_info */ +/******************************************/ +CREATE TABLE `his_config_info` ( + `id` bigint(20) unsigned NOT NULL COMMENT 'id', + `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'nid, 自增标识', + `data_id` varchar(255) NOT NULL COMMENT 'data_id', + `group_id` varchar(128) NOT NULL COMMENT 'group_id', + `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', + `content` longtext NOT NULL COMMENT 'content', + `md5` varchar(32) DEFAULT NULL COMMENT 'md5', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + `src_user` text COMMENT 'source user', + `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', + `op_type` char(10) DEFAULT NULL COMMENT 'operation type', + `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', + `encrypted_data_key` varchar(1024) NOT NULL DEFAULT '' COMMENT '密钥', + `publish_type` varchar(50) DEFAULT 'formal' COMMENT 'publish type gray or formal', + `gray_name` varchar(50) DEFAULT NULL COMMENT 'gray name', + `ext_info` longtext DEFAULT NULL COMMENT 'ext info', + PRIMARY KEY (`nid`), + KEY `idx_gmt_create` (`gmt_create`), + KEY `idx_gmt_modified` (`gmt_modified`), + KEY `idx_did` (`data_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造'; + + +/******************************************/ +/* 表名称 = tenant_capacity */ +/******************************************/ +CREATE TABLE `tenant_capacity` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', + `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', + `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', + `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', + `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', + `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', + `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', + `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表'; + + +CREATE TABLE `tenant_info` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', + `kp` varchar(128) NOT NULL COMMENT 'kp', + `tenant_id` varchar(128) default '' COMMENT 'tenant_id', + `tenant_name` varchar(128) default '' COMMENT 'tenant_name', + `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', + `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', + `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', + `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), + KEY `idx_tenant_id` (`tenant_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info'; + +CREATE TABLE `users` ( + `username` varchar(50) NOT NULL PRIMARY KEY COMMENT 'username', + `password` varchar(500) NOT NULL COMMENT 'password', + `enabled` boolean NOT NULL COMMENT 'enabled' +); + +CREATE TABLE `roles` ( + `username` varchar(50) NOT NULL COMMENT 'username', + `role` varchar(50) NOT NULL COMMENT 'role', + UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE +); + +CREATE TABLE `permissions` ( + `role` varchar(50) NOT NULL COMMENT 'role', + `resource` varchar(128) NOT NULL COMMENT 'resource', + `action` varchar(8) NOT NULL COMMENT 'action', + UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE +); + diff --git a/qa-service/pom.xml b/qa-service/pom.xml index 476ca31..09d8cea 100644 --- a/qa-service/pom.xml +++ b/qa-service/pom.xml @@ -11,22 +11,37 @@ 21 UTF-8 UTF-8 - 3.0.2 + 3.2.4 + 2023.0.1.0 + 2023.0.1 + 3.5.12 + 4.5.0 + 2.3.0 + + pom + + + qa-service-bootstrap + qa-service-adapter + qa-service-application + qa-service-domain + qa-service-common + + + - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-test - test - + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + org.springframework.boot spring-boot-dependencies @@ -34,6 +49,26 @@ pom import + + com.alibaba.cloud + spring-cloud-alibaba-dependencies + ${spring-cloud-alibaba.version} + pom + import + + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + @@ -54,7 +89,7 @@ spring-boot-maven-plugin ${spring-boot.version} - com.example.qa.service.QaServiceApplication + com.example.qa-service.qa-serviceApplication true diff --git a/qa-service/qa-service-adapter/pom.xml b/qa-service/qa-service-adapter/pom.xml new file mode 100644 index 0000000..0074d0c --- /dev/null +++ b/qa-service/qa-service-adapter/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + com.example + qa-service-adapter + 0.0.1-SNAPSHOT + qa-service-adapter + qa-service-adapter + + pom + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + qa-adapter-in + qa-adapter-out + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/pom.xml b/qa-service/qa-service-adapter/qa-adapter-in/pom.xml new file mode 100644 index 0000000..473ef75 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + com.example + qa-adapter-in + 0.0.1-SNAPSHOT + qa-adapter-in + qa-adapter-in + + pom + + + com.example + qa-service-adapter + 0.0.1-SNAPSHOT + + + + qa-adapter-in-web + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml new file mode 100644 index 0000000..973f362 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml @@ -0,0 +1,67 @@ + + + 4.0.0 + com.example + qa-adapter-in-web + 0.0.1-SNAPSHOT + qa-adapter-in-web + qa-adapter-in-web + + + com.example + qa-adapter-in + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + org.projectlombok + lombok + + + + com.example + qa-service-application + 0.0.1-SNAPSHOT + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + ${knife4j.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java new file mode 100644 index 0000000..08205e2 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java @@ -0,0 +1,74 @@ +package org.example.xmut.qa.adapter.in.web.controller; + + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.adapter.in.web.dto.CreateQaRequestDTO; +import org.example.xmut.qa.adapter.in.web.dto.QaResponseDTO; +import org.example.xmut.qa.adapter.in.web.dto.UpdateQaRequestDTO; +import org.example.xmut.qa.service.application.command.CreateQaCommand; +import org.example.xmut.qa.service.application.command.UpdateQaCommand; +import org.example.xmut.qa.service.application.port.in.*; +import org.example.xmut.qa.service.domain.Qa; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Slf4j +@RequestMapping("/qas") +@RestController +@RequiredArgsConstructor +public class qaController { + + private final GetQaListUseCase getQaListUseCase; + private final CreateQaUseCase createQaUseCase; + private final DeleteQaUseCase deleteQaUseCase; + private final UpdateQaUseCase updateQaUseCase; + private final GetQaByIdUseCase getQaByIdUseCase; + + @GetMapping("") + public List getQas() { + log.info("getQas"); + return getQaListUseCase.getQas(); + } + + @PostMapping() + public Qa createqa(@RequestBody CreateQaRequestDTO createqaRequestDTO){ + + CreateQaCommand command=CreateQaCommand.builder() + .question(createqaRequestDTO.question()) + .answer(createqaRequestDTO.answer()) + .build(); + + return createQaUseCase.createQa(command); + } + + + @DeleteMapping("{id}") + public String deleteQa(@PathVariable("id") Long id){ + deleteQaUseCase.deleteQa(id); + return "success"; + } + + @PutMapping("") + public Qa updateQa(@RequestBody UpdateQaRequestDTO updateQaRequestDTO){ + UpdateQaCommand command=UpdateQaCommand.builder() + .id(updateQaRequestDTO.id()) + .question(updateQaRequestDTO.question()) + .answer(updateQaRequestDTO.answer()) + .build(); + Qa qa = updateQaUseCase.updateQa(command); + return qa; + } + + + @GetMapping("{id}") + public QaResponseDTO getqaById(@PathVariable("id") Long id){ + Qa qa = getQaByIdUseCase.getQaById(id); + QaResponseDTO qaResponseDTO = new QaResponseDTO( + qa.getId().id(), + qa.getQuestion().question(), + qa.getAnswer().answer()); + return qaResponseDTO; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java new file mode 100644 index 0000000..dc81be0 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/CreateQaRequestDTO.java @@ -0,0 +1,6 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +public record CreateQaRequestDTO( + String question, + String answer) { +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java new file mode 100644 index 0000000..68cf323 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaInfo.java @@ -0,0 +1,50 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户信息DTO (Data Transfer Object) + * 用于封装用户的基本信息,在不同层之间传输 + * + * DTO是数据传输对象,主要用于在不同层之间传输数据 + * 这里专门用于传输用户的基本信息,不包含敏感信息如密码 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @Builder Lombok注解,提供Builder模式构建对象,使代码更清晰易读 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QaInfo { + + /** + * 用户ID + * + * 数据库中的主键,唯一标识一个用户 + * 使用Long类型可以支持更大的数据量 + */ + private Long id; + + /** + * 问题 + * + * 用户提出的问题 + */ + private String question; + + /** + * 答案 + * + * 系统回复的答案 + */ + private String answer; + +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java new file mode 100644 index 0000000..a7839ad --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/QaResponseDTO.java @@ -0,0 +1,14 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class QaResponseDTO { + private Long id; + private String question; + private String answer; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java new file mode 100644 index 0000000..24c5b97 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Result.java @@ -0,0 +1,149 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 通用响应结果类 + * 用于封装所有API接口的响应数据,统一返回格式 + * + * 这样做有以下好处: + * 1. 前端可以统一处理响应格式 + * 2. 便于统一错误处理 + * 3. 提高代码的可维护性 + * + * 使用泛型可以让这个类适用于任何类型的数据 + * + * @Data Lombok注解,自动生成getter、setter、toString等方法 + * @NoArgsConstructor Lombok注解,生成无参构造函数 + * @AllArgsConstructor Lombok注解,生成全参构造函数 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Result { + + /** + * 响应码 + * + * 用于表示请求处理的结果状态 + * 常见的响应码: + * 200 - 成功 + * 400 - 请求参数错误 + * 401 - 未授权 + * 403 - 禁止访问 + * 500 - 服务器内部错误 + */ + private Integer code; + + /** + * 响应消息 + * + * 用于描述请求处理的结果信息 + * 成功时可以是"操作成功" + * 失败时可以是具体的错误信息,如"用户名或密码错误" + */ + private String message; + + /** + * 响应数据 + * + * 用于携带具体的业务数据 + * 可以是任何类型,如用户信息、列表数据等 + * 使用泛型T使得这个字段可以适应不同类型的数据 + */ + private T data; + + /** + * 成功响应静态方法 + * + * 用于创建成功的响应结果 + * + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(T data) { + return new Result<>(200, "操作成功", data); + } + + /** + * 成功响应静态方法(无数据) + * + * 用于创建不携带数据的成功响应结果 + * + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success() { + return new Result<>(200, "操作成功", null); + } + + /** + * 成功响应静态方法(自定义消息) + * + * 用于创建携带自定义成功消息的响应结果 + * + * @param message 自定义的成功消息 + * @param data 响应数据 + * @return Result 成功的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result success(String message, T data) { + return new Result<>(200, message, data); + } + + /** + * 失败响应静态方法 + * + * 用于创建失败的响应结果,默认使用500状态码 + * + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(String message) { + return new Result<>(500, message, null); + } + + /** + * 失败响应静态方法(自定义状态码) + * + * 用于创建携带自定义状态码的失败响应结果 + * + * @param code 自定义状态码 + * @param message 错误消息 + * @return Result 失败的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result error(Integer code, String message) { + return new Result<>(code, message, null); + } + + /** + * 未授权响应静态方法 + * + * 用于创建401未授权的响应结果 + * + * @param message 错误消息 + * @return Result 未授权的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result unauthorized(String message) { + return new Result<>(401, message, null); + } + + /** + * 禁止访问响应静态方法 + * + * 用于创建403禁止访问的响应结果 + * + * @param message 错误消息 + * @return Result 禁止访问的响应结果 + * @param 泛型参数,表示数据的类型 + */ + public static Result forbidden(String message) { + return new Result<>(403, message, null); + } +} \ No newline at end of file diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java new file mode 100644 index 0000000..25875ef --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/UpdateQaRequestDTO.java @@ -0,0 +1,6 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +public record UpdateQaRequestDTO(long id, + String question, + String answer) { +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b0f990d --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/exception/GlobalExceptionHandler.java @@ -0,0 +1,259 @@ +package org.example.xmut.qa.adapter.in.web.exception; + +import org.example.xmut.qa.adapter.in.web.dto.Result; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.AuthenticationException; +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 java.util.stream.Collectors; + +/** + * 全局异常处理器 + * 统一处理应用中的各种异常,避免异常直接暴露给用户 + * + * @Slf4j Lombok注解,自动生成日志对象log,用于记录异常日志 + * @RestControllerAdvice 组合注解,相当于@ControllerAdvice + @ResponseBody + * 用于定义全局异常处理器,可以捕获控制器层抛出的异常 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理认证异常 + * + * 当用户认证失败时(如Token无效、过期等)会抛出AuthenticationException + * + * @ExceptionHandler 注解指定该方法处理哪种异常 + * @ResponseStatus 注解指定返回的HTTP状态码 + * + * @param e 认证异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AuthenticationException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleAuthenticationException(AuthenticationException e) { + log.error("认证异常: {}", e.getMessage()); + return Result.unauthorized("认证失败: " + e.getMessage()); + } + + /** + * 处理凭据错误异常 + * + * 当用户名或密码错误时会抛出BadCredentialsException + * 这是AuthenticationException的一个子类 + * + * @param e 凭据错误异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BadCredentialsException.class) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public Result handleBadCredentialsException(BadCredentialsException e) { + log.error("凭据错误: {}", e.getMessage()); + return Result.unauthorized("用户名或密码错误"); + } + + /** + * 处理访问拒绝异常 + * + * 当已认证用户尝试访问没有权限的资源时会抛出AccessDeniedException + * + * @param e 访问拒绝异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public Result handleAccessDeniedException(AccessDeniedException e) { + log.error("访问拒绝: {}", e.getMessage()); + return Result.forbidden("访问被拒绝,权限不足"); + } + + /** + * 处理参数校验异常 - @Valid注解 + * + * 当使用@Valid注解验证请求参数失败时会抛出MethodArgumentNotValidException + * 例如LoginRequest中的用户名或密码不符合验证规则 + * + * @param e 参数校验异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getBindingResult().getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数校验失败: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理参数绑定异常 + * + * 当请求参数绑定到对象时发生错误会抛出BindException + * + * @param e 参数绑定异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BindException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBindException(BindException e) { + // 收集所有字段的错误信息并拼接成字符串 + String errorMessage = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + log.error("参数绑定失败: {}", errorMessage); + return Result.error("参数绑定失败: " + errorMessage); + } + + /** + * 处理约束违反异常 + * + * 当使用Bean Validation API验证失败时会抛出ConstraintViolationException + * + * @param e 约束违反异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(ConstraintViolationException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleConstraintViolationException(ConstraintViolationException e) { + // 收集所有约束违反的错误信息并拼接成字符串 + String errorMessage = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + log.error("约束违反: {}", errorMessage); + return Result.error("参数校验失败: " + errorMessage); + } + + /** + * 处理非法参数异常 + * + * 当传递给方法的参数不合法时会抛出IllegalArgumentException + * + * @param e 非法参数异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(IllegalArgumentException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleIllegalArgumentException(IllegalArgumentException e) { + log.error("非法参数: {}", e.getMessage()); + return Result.error("参数错误: " + e.getMessage()); + } + + /** + * 处理空指针异常 + * + * 当尝试访问空对象的属性或方法时会抛出NullPointerException + * + * @param e 空指针异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(NullPointerException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleNullPointerException(NullPointerException e) { + log.error("空指针异常", e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 处理运行时异常 + * + * 当发生未预期的运行时错误时会抛出RuntimeException + * 这是很多异常的父类 + * + * @param e 运行时异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(RuntimeException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleRuntimeException(RuntimeException e) { + log.error("运行时异常: {}", e.getMessage(), e); + return Result.error("系统异常: " + e.getMessage()); + } + + /** + * 处理其他所有异常 + * + * 作为兜底的异常处理方法,处理所有未被上面方法处理的异常 + * + * @param e 异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public Result handleException(Exception e) { + log.error("未知异常: {}", e.getMessage(), e); + return Result.error("系统内部错误,请联系管理员"); + } + + /** + * 自定义业务异常类 + * + * 用于处理应用程序中特定的业务异常 + * 继承自RuntimeException,是一个受检异常 + */ + public static class BusinessException extends RuntimeException { + /** + * 异常码 + * + * 用于标识异常的类型,便于前端进行不同的处理 + */ + private final int code; + + /** + * 构造函数 - 只有消息 + * + * @param message 异常消息 + */ + public BusinessException(String message) { + super(message); + this.code = 500; + } + + /** + * 构造函数 - 有码和消息 + * + * @param code 异常码 + * @param message 异常消息 + */ + public BusinessException(int code, String message) { + super(message); + this.code = code; + } + + /** + * 获取异常码 + * + * @return int 异常码 + */ + public int getCode() { + return code; + } + } + + /** + * 处理自定义业务异常 + * + * 处理应用程序中抛出的BusinessException + * + * @param e 业务异常对象 + * @return Result 统一响应结果 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public Result handleBusinessException(BusinessException e) { + log.error("业务异常: {}", e.getMessage()); + return Result.error(e.getCode(), e.getMessage()); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/pom.xml b/qa-service/qa-service-adapter/qa-adapter-out/pom.xml new file mode 100644 index 0000000..dfc017b --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + com.example + qa-adapter-out + 0.0.1-SNAPSHOT + qa-adapter-out + qa-adapter-out + + pom + + + com.example + qa-service-adapter + 0.0.1-SNAPSHOT + + + + qa-adapter-out-persistence + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml new file mode 100644 index 0000000..0e36162 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + com.example + qa-adapter-out-persistence + 0.0.1-SNAPSHOT + qa-adapter-out-persistence + qa-adapter-out-persistence + + com.example + qa-adapter-out + 0.0.1-SNAPSHOT + + + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-domain + 0.0.1-SNAPSHOT + + + + com.baomidou + mybatis-plus-spring-boot3-starter + + + + com.mysql + mysql-connector-j + runtime + + + com.example + qa-service-application + 0.0.1-SNAPSHOT + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java new file mode 100644 index 0000000..c8304c8 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/CreateQaBridge.java @@ -0,0 +1,26 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.domain.port.CreateQaPort; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class CreateQaBridge implements CreateQaPort { + @Resource + private QaMapper qaMapper; + + @Override + public Qa createQa(Qa qa) { + QaEntity qaEntity = QaConvertor.toEntity(qa); + int result = qaMapper.insert(qaEntity); + //result 指受影响行数 + log.info("result:{}",result); + return qa; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java new file mode 100644 index 0000000..1fc5ef7 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/DeleteQaBridge.java @@ -0,0 +1,19 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.domain.port.DeleteQaPort; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class DeleteQaBridge implements DeleteQaPort { + @Resource + private QaMapper qaMapper; + @Override + public void deleteQa(Long id) { + int result = qaMapper.deleteById(id); + log.info("result:{}",result); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java new file mode 100644 index 0000000..66cf65f --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaListBridge.java @@ -0,0 +1,32 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaListPort; +import jakarta.annotation.Resource; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class GetQaListBridge implements GetQaListPort { + + @Resource + private QaMapper qaMapper; + + @Override + public List getQas() { + List entities = qaMapper.selectList(null); + + ArrayList list = new ArrayList<>(); + + entities.forEach(qaEntity -> { + Qa qa = QaConvertor.toDomain(qaEntity); + list.add(qa); + }); + return list; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java new file mode 100644 index 0000000..f848b6b --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/UpdateQaBridge.java @@ -0,0 +1,24 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.UpdateQaPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class UpdateQaBridge implements UpdateQaPort { + @Resource + private QaMapper qaMapper; + + + @Override + public Qa updateQa(Qa qa) { + int result = qaMapper.updateById(QaConvertor.toEntity(qa)); + log.info("result:{}",result); + return qa; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java new file mode 100644 index 0000000..6edfa60 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/convertor/QaConvertor.java @@ -0,0 +1,25 @@ +package org.example.xmut.qa.adapter.out.persistence.convertor; + +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.valueobject.*; + +public class QaConvertor { + + public static Qa toDomain(QaEntity qaEntity) { + return new Qa( + new QaId(qaEntity.getId()), + new Question(qaEntity.getQuestion()), + new Answer(qaEntity.getAnswer()) + ); + } + + public static QaEntity toEntity(Qa qa) { + return new QaEntity( + qa.getId().id(), + qa.getQuestion().question(), + qa.getAnswer().answer() + ); + } +} + diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java new file mode 100644 index 0000000..2953d0e --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/entity/QaEntity.java @@ -0,0 +1,29 @@ +package org.example.xmut.qa.adapter.out.persistence.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("sys_user") +public class QaEntity { + @TableId(type = IdType.AUTO) + private Long id; + + private String question; + + private String answer; + + public QaEntity(long id, String question, String answer) { + this.id = id; + this.question = question; + this.answer = answer; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java new file mode 100644 index 0000000..7f40a22 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/mapper/QaMapper.java @@ -0,0 +1,15 @@ +package org.example.xmut.qa.adapter.out.persistence.mapper; + + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + + +public interface QaMapper extends BaseMapper{ + + @Select("SELECT * FROM sys_qa WHERE id = #{id}") + QaEntity selectById(@Param("id") Long id); +} diff --git a/qa-service/qa-service-application/pom.xml b/qa-service/qa-service-application/pom.xml new file mode 100644 index 0000000..8d9eb8b --- /dev/null +++ b/qa-service/qa-service-application/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + com.example + qa-service-application + 0.0.1-SNAPSHOT + qa-service-application + qa-service-application + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-domain + 0.0.1-SNAPSHOT + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java new file mode 100644 index 0000000..9588148 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/CreateQaCommand.java @@ -0,0 +1,11 @@ +package org.example.xmut.qa.service.application.command; + +import lombok.Builder; + +@Builder +public record CreateQaCommand( + Long id, + String question, + String answer +) { +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java new file mode 100644 index 0000000..fa5f5ba --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/command/UpdateQaCommand.java @@ -0,0 +1,9 @@ +package org.example.xmut.qa.service.application.command; + +import lombok.Builder; + +@Builder +public record UpdateQaCommand(Long id, + String question, + String answer) { +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java new file mode 100644 index 0000000..c884e62 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/CreateQaUseCase.java @@ -0,0 +1,8 @@ +package org.example.xmut.qa.service.application.port.in; + +import org.example.xmut.qa.service.application.command.CreateQaCommand; +import org.example.xmut.qa.service.domain.Qa; + +public interface CreateQaUseCase { + Qa createQa(CreateQaCommand createQaCommand); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java new file mode 100644 index 0000000..cd49ae6 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/DeleteQaUseCase.java @@ -0,0 +1,5 @@ +package org.example.xmut.qa.service.application.port.in; + +public interface DeleteQaUseCase { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java new file mode 100644 index 0000000..51df281 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaByIdUseCase.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.application.port.in; + +import org.example.xmut.qa.service.domain.Qa; + +public interface GetQaByIdUseCase { + Qa getQaById(Long id); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java new file mode 100644 index 0000000..78bf93c --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/GetQaListUseCase.java @@ -0,0 +1,10 @@ +package org.example.xmut.qa.service.application.port.in; + +import org.example.xmut.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListUseCase { + + List getQas(); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java new file mode 100644 index 0000000..16a7d4e --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/port/in/UpdateQaUseCase.java @@ -0,0 +1,10 @@ +package org.example.xmut.qa.service.application.port.in; + + +import org.example.xmut.qa.service.application.command.UpdateQaCommand; +import org.example.xmut.qa.service.domain.Qa; + +public interface UpdateQaUseCase { + + Qa updateQa(UpdateQaCommand command); +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java new file mode 100644 index 0000000..d7a60cc --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/CreateQaService.java @@ -0,0 +1,29 @@ +package org.example.xmut.qa.service.application.service; + +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.application.command.CreateQaCommand; +import org.example.xmut.qa.service.application.port.in.CreateQaUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.CreateQaPort; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class CreateQaService implements CreateQaUseCase { + @Resource + private CreateQaPort createQaPort; + @Override + public Qa createQa(CreateQaCommand createQaCommand) { + //command -> domain + Qa qa=new Qa( + new Question(createQaCommand.question()), new Question(createQaCommand.question()), + new Answer(createQaCommand.answer()) + ); + log.info("qa:{}",qa); + return createQaPort.createQa(qa); + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java new file mode 100644 index 0000000..6db5748 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/DeleteQaService.java @@ -0,0 +1,17 @@ +package org.example.xmut.qa.service.application.service; + + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.port.in.DeleteQaUseCase; +import org.example.xmut.qa.service.domain.port.DeleteQaPort; +import org.springframework.stereotype.Service; + +@Service +public class DeleteQaService implements DeleteQaUseCase { + @Resource + private DeleteQaPort deleteQaPort; + @Override + public void deleteQa(Long id) { + deleteQaPort.deleteQa(id); + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java new file mode 100644 index 0000000..72cb81c --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaByIdService.java @@ -0,0 +1,18 @@ +package org.example.xmut.qa.service.application.service; + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.port.in.GetQaByIdUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaByIdPort; +import org.springframework.stereotype.Service; + +@Service +public class GetQaByIdService implements GetQaByIdUseCase { + + @Resource + private GetQaByIdPort getQaByIdPort; + @Override + public Qa getQaById(Long id) { + return getQaByIdPort.getQaById(id); + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java new file mode 100644 index 0000000..4b49c49 --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/GetQaListService.java @@ -0,0 +1,22 @@ +package org.example.xmut.qa.service.application.service; + + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.port.in.GetQaListUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaListPort; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GetQaListService implements GetQaListUseCase { + + @Resource + GetQaListPort getQaListPort; + @Override + public List getQas() { + List qa = Qa.getQas(getQaListPort); + return qa; + } +} diff --git a/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java new file mode 100644 index 0000000..1e5448e --- /dev/null +++ b/qa-service/qa-service-application/src/main/java/org/example/xmut/qa/service/application/service/UpdateQaService.java @@ -0,0 +1,26 @@ +package org.example.xmut.qa.service.application.service; + +import jakarta.annotation.Resource; +import org.example.xmut.qa.service.application.command.UpdateQaCommand; +import org.example.xmut.qa.service.application.port.in.UpdateQaUseCase; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.UpdateQaPort; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.stereotype.Service; + +@Service +public class UpdateQaService implements UpdateQaUseCase { + @Resource + private UpdateQaPort updateQaPort; + + @Override + public Qa updateQa(UpdateQaCommand command) { + Qa qa = new Qa( + new QaId(command.id()), + new Question(command.question()), + new Answer(command.answer())); + return updateQaPort.updateQa(qa); + } +} diff --git a/qa-service/qa-service-bootstrap/pom.xml b/qa-service/qa-service-bootstrap/pom.xml new file mode 100644 index 0000000..0ddd8f7 --- /dev/null +++ b/qa-service/qa-service-bootstrap/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + com.example + qa-service-bootstrap + 0.0.1-SNAPSHOT + qa-service-bootstrap + qa-service-bootstrap + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.example + qa-adapter-in-web + 0.0.1-SNAPSHOT + + + + com.example + qa-adapter-out-persistence + 0.0.1-SNAPSHOT + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + com.example.qa.service.bootstrap.qaServiceBootstrapApplication + false + + + + repackage + + repackage + + + + + + + + diff --git a/qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java b/qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java new file mode 100644 index 0000000..4b1d931 --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/java/org/example/xmut/qa/service/bootstrap/QaServiceBootstrapApplication.java @@ -0,0 +1,13 @@ +package org.example.xmut.qa.service.bootstrap; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class QaServiceBootstrapApplication { + + public static void main(String[] args) { + SpringApplication.run(QaServiceBootstrapApplication.class, args); + } + +} diff --git a/qa-service/qa-service-bootstrap/src/main/resources/application.properties b/qa-service/qa-service-bootstrap/src/main/resources/application.properties new file mode 100644 index 0000000..9ca871a --- /dev/null +++ b/qa-service/qa-service-bootstrap/src/main/resources/application.properties @@ -0,0 +1,25 @@ +server.port=28080 + +spring.application.name=qa-service + + + +# Nacos认证信息 +spring.cloud.nacos.discovery.username=nacos +spring.cloud.nacos.discovery.password=nacos +# Nacos 服务发现与注册配置,其中子属性 server-addr 指定 Nacos 服务器主机和端口 +spring.cloud.nacos.discovery.server-addr=192.168.168.128:8848 +# 注册到 nacos 的指定 namespace,默认为 public +spring.cloud.nacos.discovery.namespace=public + +# Nacos帮助文档: https://nacos.io/zh-cn/docs/concepts.html +# Nacos认证信息 +spring.cloud.nacos.config.username=nacos +spring.cloud.nacos.config.password=nacos +spring.cloud.nacos.config.contextPath=/nacos +# 设置配置中心服务端地址 +spring.cloud.nacos.config.server-addr=192.168.168.128:8848 +# Nacos 配置中心的namespace。需要注意,如果使用 public 的 namcespace ,请不要填写这个值,直接留空即可 +# spring.cloud.nacos.config.namespace= +spring.config.import=nacos:${spring.application.name}.properties?refresh=true + diff --git a/qa-service/qa-service-common/pom.xml b/qa-service/qa-service-common/pom.xml new file mode 100644 index 0000000..5a51d3c --- /dev/null +++ b/qa-service/qa-service-common/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + com.example + qa-service-common + 0.0.1-SNAPSHOT + qa-service-common + qa-service-common + + 21 + UTF-8 + UTF-8 + 3.2.4 + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + org.springframework.boot + spring-boot-starter + + + io.jsonwebtoken + jjwt + 0.9.1 + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + com.sun.xml.bind + jaxb-core + 2.3.0.1 + + + com.sun.xml.bind + jaxb-impl + 2.3.3 + + + javax.activation + activation + 1.1.1 + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + diff --git a/qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java b/qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java new file mode 100644 index 0000000..6035356 --- /dev/null +++ b/qa-service/qa-service-common/src/main/java/org/example/xmut/qa/service/common/IdWorker.java @@ -0,0 +1,199 @@ +package org.example.xmut.qa.service.common; + +import java.lang.management.ManagementFactory; +import java.net.InetAddress; +import java.net.NetworkInterface; + +/** + * @author wuyunbin + *

名称:IdWorker.java

+ *

描述:分布式自增长ID

+ *
+ *     Twitter的 Snowflake JAVA实现方案
+ * 
+ * 核心代码为其IdWorker这个类实现,其原理结构如下,我分别用一个0表示一位,用—分割开部分的作用: + * 1||0---0000000000 0000000000 0000000000 0000000000 0 --- 00000 ---00000 ---000000000000 + * 在上面的字符串中,第一位为未使用(实际上也可作为long的符号位),接下来的41位为毫秒级时间, + * 然后5位datacenter标识位,5位机器ID(并不算标识符,实际是为线程标识), + * 然后12位该毫秒内的当前毫秒内的计数,加起来刚好64位,为一个Long型。 + * 这样的好处是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由datacenter和机器ID作区分), + * 并且效率较高,经测试,snowflake每秒能够产生26万ID左右,完全满足需要。 + *

+ * 64位ID (42(毫秒)+5(机器ID)+5(业务编码)+12(重复累加)) + * @author Polim + */ +public class IdWorker { + /** + * 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动) + */ + private final static long TWEPOCH = 1288834974657L; + + /** + * 机器标识位数 + */ + private final static long WORKER_ID_BITS = 5L; + + /** + * 数据中心标识位数 + */ + private final static long DATA_CENTER_ID_BITS = 5L; + + /** + * 机器ID最大值 + */ + private final static long MAX_WORKER_ID = -1L ^ (-1L << WORKER_ID_BITS); + + /** + * 数据中心ID最大值 + */ + private final static long MAX_DATACENTER_ID = -1L ^ (-1L << DATA_CENTER_ID_BITS); + + /** + * 毫秒内自增位 + */ + private final static long SEQUENCE_BITS = 12L; + + /** + * 机器ID偏左移12位 + */ + private final static long WORKER_ID_SHIFT = SEQUENCE_BITS; + + /** + * 数据中心ID左移17位 + */ + private final static long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; + + /** + * 时间毫秒左移22位 + */ + private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; + + private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); + + /** + * 上次生产id时间戳 + */ + private static long lastTimestamp = -1L; + + /** + * 0,并发控制 + */ + private long sequence = 0L; + + private final long workerId; + + /** + * 数据标识id部分 + */ + private final long datacenterId; + + public IdWorker() { + this.datacenterId = getDatacenterId(); + this.workerId = getMaxWorkerId(datacenterId); + } + + /** + * @param workerId 工作机器ID + * @param datacenterId 序列号 + */ + public IdWorker(long workerId, long datacenterId) { + if (workerId > MAX_WORKER_ID || workerId < 0) { + throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", MAX_WORKER_ID)); + } + if (datacenterId > MAX_DATACENTER_ID || datacenterId < 0) { + throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", MAX_DATACENTER_ID)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + } + + /** + * 获取下一个ID + * + * @return + */ + public synchronized long nextId() { + long timestamp = timeGen(); + if (timestamp < lastTimestamp) { + throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + + if (lastTimestamp == timestamp) { + // 当前毫秒内,则+1 + sequence = (sequence + 1) & SEQUENCE_MASK; + if (sequence == 0) { + // 当前毫秒内计数满了,则等待下一秒 + timestamp = tilNextMillis(lastTimestamp); + } + } else { + sequence = 0L; + } + lastTimestamp = timestamp; + // ID偏移组合生成最终的ID,并返回ID + + return ((timestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT) + | (datacenterId << DATACENTER_ID_SHIFT) + | (workerId << WORKER_ID_SHIFT) | sequence; + } + + private long tilNextMillis(final long lastTimestamp) { + long timestamp = this.timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = this.timeGen(); + } + return timestamp; + } + + private long timeGen() { + return System.currentTimeMillis(); + } + + /** + *

+ * 获取 MAX_WORKER_ID + *

+ */ + protected static long getMaxWorkerId(long datacenterId) { + StringBuilder mpid = new StringBuilder(); + mpid.append(datacenterId); + String name = ManagementFactory.getRuntimeMXBean().getName(); + if (!name.isEmpty()) { + /* + * GET jvmPid + */ + mpid.append(name.split("@")[0]); + } + /* + * MAC + PID 的 hashcode 获取16个低位 + */ + return (mpid.toString().hashCode() & 0xffff) % (IdWorker.MAX_WORKER_ID + 1); + } + + /** + *

+ * 数据标识id部分 + *

+ */ + protected static long getDatacenterId() { + long id = 0L; + try { + InetAddress ip = InetAddress.getLocalHost(); + NetworkInterface network = NetworkInterface.getByInetAddress(ip); + if (network == null) { + id = 1L; + } else { + byte[] mac = network.getHardwareAddress(); + id = ((0x000000FF & (long) mac[mac.length - 1]) + | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6; + id = id % (IdWorker.MAX_DATACENTER_ID + 1); + } + } catch (Exception e) { + System.out.println(" getDatacenterId: " + e.getMessage()); + } + return id; + } + + + + +} diff --git a/qa-service/qa-service-domain/pom.xml b/qa-service/qa-service-domain/pom.xml new file mode 100644 index 0000000..19c5de5 --- /dev/null +++ b/qa-service/qa-service-domain/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + com.example + qa-service-domain + 0.0.1-SNAPSHOT + qa-service-domain + qa-service-domain + + + com.example + qa-service + 0.0.1-SNAPSHOT + + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security + spring-security-test + test + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.projectlombok + lombok + provided + + + + com.example + qa-service-common + 0.0.1-SNAPSHOT + + + org.springframework.security + spring-security-crypto + + + + + + + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 21 + 21 + UTF-8 + + + + + + + diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java new file mode 100644 index 0000000..8bbcf5d --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java @@ -0,0 +1,37 @@ +package org.example.xmut.qa.service.domain; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.service.domain.port.GetQaListPort; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; + +import java.util.List; + +@Slf4j +@Setter +@Getter +@ToString +public class Qa { + private QaId id; + private Question question; + private Answer answer; + + public Qa(QaId qaId, Question question, Answer answer) { + this.id = qaId; + this.question=question; + this.answer=answer; + } + + public Qa(Question question, Question question1, Answer answer) { + this.question=question; + this.answer=answer; + } + + public static List getQas(GetQaListPort getQaListPort) { + return getQaListPort.getQas(); + } +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java new file mode 100644 index 0000000..8ddc319 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/CreateQaPort.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.port; + +import org.example.xmut.qa.service.domain.Qa; + +public interface CreateQaPort { + Qa createQa(Qa qa); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java new file mode 100644 index 0000000..4b457c9 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/DeleteQaPort.java @@ -0,0 +1,5 @@ +package org.example.xmut.qa.service.domain.port; + +public interface DeleteQaPort { + void deleteQa(Long id); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java new file mode 100644 index 0000000..e09144e --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaByIdPort.java @@ -0,0 +1,8 @@ +package org.example.xmut.qa.service.domain.port; + + +import org.example.xmut.qa.service.domain.Qa; + +public interface GetQaByIdPort { + Qa getQaById(Long id); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java new file mode 100644 index 0000000..e3417e4 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/GetQaListPort.java @@ -0,0 +1,11 @@ +package org.example.xmut.qa.service.domain.port; + + + +import org.example.xmut.qa.service.domain.Qa; + +import java.util.List; + +public interface GetQaListPort { + List getQas(); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java new file mode 100644 index 0000000..dffe38b --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/port/UpdateQaPort.java @@ -0,0 +1,8 @@ +package org.example.xmut.qa.service.domain.port; + + +import org.example.xmut.qa.service.domain.Qa; + +public interface UpdateQaPort { + Qa updateQa(Qa qa); +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java new file mode 100644 index 0000000..3e54ff7 --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Answer.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.valueobject; + +public record Answer(String answer) { + public String getValue() { + return answer; + } +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java new file mode 100644 index 0000000..7ecddca --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/QaId.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.valueobject; + +public record QaId(long id) { + public long getValue() { + return id; + } +} diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java new file mode 100644 index 0000000..9dd7f5d --- /dev/null +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/valueobject/Question.java @@ -0,0 +1,7 @@ +package org.example.xmut.qa.service.domain.valueobject; + +public record Question(String question) { + public String getValue() { + return question; + } +} -- Gitee From c882010ca57ed828031193e5fa62ffba3de8a04e Mon Sep 17 00:00:00 2001 From: little_jin <861165942@qq.com> Date: Wed, 17 Sep 2025 15:22:12 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E8=8C=83=E9=91=AB=E5=9B=BD=20=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E7=9A=84=E6=9B=B4=E6=94=B9=EF=BC=9A=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86=E6=9E=84=E9=80=A0=E6=96=B9=E6=B3=95=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E4=BA=86=E8=B5=8B=E5=80=BC=E5=8F=98=E9=87=8F=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/bridge/GetQaByIdBridge.java | 22 +++++++++++++++++++ .../com/example/user/service/domain/User.java | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java diff --git a/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java new file mode 100644 index 0000000..be11514 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-out/qa-adapter-out-persistence/src/main/java/org/example/xmut/qa/adapter/out/persistence/bridge/GetQaByIdBridge.java @@ -0,0 +1,22 @@ +package org.example.xmut.qa.adapter.out.persistence.bridge; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.port.GetQaByIdPort; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class GetQaByIdBridge implements GetQaByIdPort { + @Resource + private QaMapper qaMapper; + @Override + public Qa getQaById(Long id) { + QaEntity qaEntity = qaMapper.selectById(id); + return QaConvertor.toDomain(qaEntity); + } +} diff --git a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java index 4a221fa..7bf3f1a 100644 --- a/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java +++ b/user-service/user-service-domain/src/main/java/com/example/user/service/domain/User.java @@ -49,9 +49,9 @@ public class User { } public User(UserId userId, UserName userName, UserAge userAge, Email email) { - this.id = id; - this.name = name; - this.age = age; + this.id = userId; + this.name = userName; + this.age = userAge; this.email = email; } -- Gitee From fc3ce0df0c851884bf3dc40453f211cbdbf30b8b Mon Sep 17 00:00:00 2001 From: little_jin <861165942@qq.com> Date: Wed, 17 Sep 2025 15:47:35 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E8=BF=87=E7=A8=8B=E4=B8=AD=E9=81=87=E5=88=B0=E7=9A=84?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/UserLoginService.java | 3 +- .../example/user/service/common/JwtUtil.java | 299 +++--------------- 2 files changed, 41 insertions(+), 261 deletions(-) diff --git a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java index 31b5a6e..03883a8 100644 --- a/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java +++ b/user-service/user-service-application/src/main/java/com/example/user/service/application/service/UserLoginService.java @@ -29,7 +29,8 @@ public class UserLoginService implements UserLoginUseCase { throw new RuntimeException("密码错误"); } // 签发token - String token = JwtUtil.generateToken( + JwtUtil jwtUtil = new JwtUtil(); + String token = jwtUtil.generateToken( user.getName().username() ); log.info("生成的JWT令牌: {}", token); diff --git a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java index 6bc9f28..0137dd9 100644 --- a/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java +++ b/user-service/user-service-common/src/main/java/com/example/user/service/common/JwtUtil.java @@ -10,336 +10,115 @@ import javax.crypto.SecretKey; import java.util.Date; /** - * JWT工具类 - JSON Web Token 核心处理工具 - * - * 【什么是JWT?】 - * JWT (JSON Web Token) 是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。 - * 它是一种无状态的认证方式,服务器不需要保存用户的登录状态。 - * - * 【JWT的结构】 - * JWT由三部分组成,用点(.)分隔: - * 1. Header(头部):包含token类型和签名算法,如 {"typ":"JWT","alg":"HS512"} - * 2. Payload(载荷):包含声明信息,如用户名、过期时间等,如 {"sub":"user123","exp":1234567890} - * 3. Signature(签名):用于验证token的完整性,防止篡改 - * 格式:Header.Payload.Signature - * 例如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c - * - * 【为什么使用JWT?】 - * 1. 无状态:服务器不需要存储session,适合分布式系统 - * 2. 跨域:可以在不同域名间传递认证信息 - * 3. 移动友好:适合移动应用的认证方式 - * 4. 性能好:避免了频繁的数据库查询 - * - * 【安全考虑】 - * 1. 密钥安全:签名密钥必须保密,泄露会导致token可被伪造 - * 2. HTTPS传输:token应通过HTTPS传输,防止被截获 - * 3. 过期时间:设置合理的过期时间,平衡安全性和用户体验 - * 4. 敏感信息:不要在payload中存储敏感信息,因为它只是Base64编码,不是加密 - * - * 【注解说明】 - * @Slf4j:Lombok注解,自动生成日志对象log,用于记录调试和错误信息 - * @Component:Spring注解,将该类注册为Spring容器管理的Bean,可以被其他类注入使用 + * JWT工具类 + * 用于生成、验证和解析JWT token */ @Slf4j @Component public class JwtUtil { - /** - * JWT签名密钥 - 用于保证token安全性的核心要素 - * - * 【作用说明】 - * 这个密钥用于对JWT进行签名和验证,确保token的完整性和真实性。 - * 任何人如果没有这个密钥,就无法伪造有效的JWT token。 - * - * 【配置方式】 - * @Value注解从application.properties或application.yml中读取jwt.secret配置 - * 如果配置文件中没有设置,则使用冒号后面的默认值 - * - * 【安全要求】 - * 1. 密钥长度:至少256位(32字节)才能满足HS256算法要求 - * 2. 密钥复杂度:应包含大小写字母、数字,避免使用简单密码 - * 3. 密钥保密:绝对不能泄露给客户端或第三方 - * 4. 生产环境:必须使用环境变量或加密配置文件存储 - * - * 【为什么这样设计?】 - * 使用@Value注解可以灵活配置密钥,不同环境(开发、测试、生产)可以使用不同的密钥 - */ + // JWT密钥,实际项目中应该从配置文件读取 @Value("${jwt.secret:mySecretKeyForJwtTokenGenerationAndValidation123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789}") - private static String secret; + private String secret; - /** - * JWT token过期时间(毫秒) - 控制token有效期的关键参数 - * - * 【时间设置说明】 - * 默认值86400000毫秒 = 24小时 = 1天 - * 计算方式:24小时 × 60分钟 × 60秒 × 1000毫秒 = 86400000毫秒 - * - * 【为什么需要过期时间?】 - * 1. 安全考虑:限制token的有效期,即使被盗用也会自动失效 - * 2. 减少风险:缩短攻击者可利用stolen token的时间窗口 - * 3. 强制重新认证:定期要求用户重新登录,提高安全性 - * - * 【时间设置策略】 - * - 高安全应用:1-2小时 - * - 一般应用:24小时 - * - 低风险应用:7天 - * - 移动应用:可以设置更长时间,配合refresh token使用 - * - * 【配置灵活性】 - * 通过@Value注解,可以在不同环境中设置不同的过期时间 - */ - @Value("${jwt.expiration:86400000}") - private static Long expiration; + // token过期时间(毫秒) + @Value("${jwt.expiration:86400000}") // 默认24小时 + private Long expiration; /** - * 生成JWT token - 用户登录成功后创建身份凭证 - * - * 【方法作用】 - * 当用户登录成功后,调用此方法生成一个JWT token作为用户的身份凭证。 - * 用户后续的请求都需要携带这个token来证明自己的身份。 - * - * 【生成流程】 - * 1. 获取当前时间作为token的签发时间 - * 2. 计算token的过期时间(当前时间 + 配置的有效期) - * 3. 使用JJWT库的Builder模式构建token - * 4. 设置token的各个声明(Claims) - * 5. 使用密钥和算法对token进行签名 - * 6. 生成最终的token字符串 - * - * 【参数说明】 - * @param username 用户名,将作为token的主体(subject),用于标识token属于哪个用户 - * - * 【返回值说明】 - * @return String 生成的JWT token,格式为:Header.Payload.Signature - * - * 【使用场景】 - * - 用户登录成功后 - * - 需要为用户创建新的身份凭证时 - * - token刷新时(如果实现了refresh token机制) + * 生成JWT token + * @param username 用户名 + * @return JWT token */ - public static String generateToken(String username) { - // 获取当前时间 - 作为token的签发时间(iat - issued at) + public String generateToken(String username) { Date now = new Date(); - // 计算过期时间 - 当前时间加上配置的有效期 - // 这样设计可以灵活控制token的生命周期 Date expiryDate = new Date(now.getTime() + expiration); - // 使用JJWT库的Builder模式构建JWT Token - // Builder模式的优点:代码清晰、易于理解、支持链式调用 return Jwts.builder() - .setSubject(username) // 设置主体(sub):标识token的所有者 - .setIssuedAt(now) // 设置签发时间(iat):token的创建时间 - .setExpiration(expiryDate) // 设置过期时间(exp):token的失效时间 - .signWith(getSigningKey(), SignatureAlgorithm.HS512) // 使用HS512算法和密钥进行签名 - .compact(); // 生成紧凑的URL安全字符串格式 + .setSubject(username) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(getSigningKey(), SignatureAlgorithm.HS512) + .compact(); } /** - * 从token中获取用户名 - 解析JWT获取用户身份信息 - * - * 【方法作用】 - * 当收到客户端发送的JWT token时,需要从中提取用户名来识别用户身份。 - * 这是JWT认证流程中的关键步骤。 - * - * 【解析过程】 - * 1. 调用getClaimsFromToken()方法解析token - * 2. 从Claims中获取subject字段(用户名) - * 3. 返回用户名供后续业务逻辑使用 - * - * 【为什么这样设计?】 - * JWT的payload部分包含了用户信息,通过解析可以直接获取, - * 无需查询数据库,提高了性能。 - * - * @param token JWT token字符串 - * @return String 用户名,如果token格式错误或解析失败则可能抛出异常 - * - * 【注意事项】 - * - 此方法不验证token是否过期,只负责提取用户名 - * - 如果token被篡改,解析时会抛出签名验证异常 + * 从token中获取用户名 + * @param token JWT token + * @return 用户名 */ public String getUsernameFromToken(String token) { - // 解析token获取载荷(Claims) - Claims包含了token中的所有声明信息 Claims claims = getClaimsFromToken(token); - // 从载荷中获取主体(subject),即用户名 - // subject是JWT标准中用于标识token所有者的字段 return claims.getSubject(); } /** - * 验证token是否有效 - JWT认证的核心验证逻辑 - * - * 【验证目的】 - * 确保接收到的JWT token是合法、有效且未被篡改的。 - * 这是保护API安全的重要防线。 - * - * 【验证步骤】 - * 1. 从token中提取用户名 - * 2. 比较token中的用户名与期望的用户名是否一致 - * 3. 检查token是否已过期 - * 4. 隐式验证:getClaimsFromToken()会验证签名 - * - * 【为什么需要双重验证?】 - * - 用户名匹配:确保token属于正确的用户 - * - 过期检查:确保token仍在有效期内 - * - 签名验证:确保token未被篡改(在解析过程中自动完成) - * - * @param token JWT token字符串 - * @param username 期望的用户名,通常来自请求上下文 - * @return boolean true表示token有效,false表示无效 - * - * 【异常处理】 - * 使用try-catch捕获所有可能的异常(签名错误、格式错误等), - * 确保方法不会因异常而中断,统一返回false表示验证失败。 + * 验证token是否有效 + * @param token JWT token + * @param username 用户名 + * @return 是否有效 */ public boolean validateToken(String token, String username) { try { - // 从token中获取用户名 - 这一步会验证token的签名 String tokenUsername = getUsernameFromToken(token); - // 验证用户名是否匹配且token未过期 - // 两个条件都必须满足才认为token有效 return (username.equals(tokenUsername) && !isTokenExpired(token)); } catch (Exception e) { - // 捕获解析token时的异常(如签名错误、格式错误等) - // 记录错误日志便于调试,但不暴露具体错误信息给客户端 log.error("Token validation failed: {}", e.getMessage()); - return false; // 任何异常都视为验证失败 + return false; } } /** * 检查token是否过期 - * * @param token JWT token - * @return boolean token是否过期 + * @return 是否过期 */ public boolean isTokenExpired(String token) { - // 获取token的过期时间 Date expiration = getExpirationDateFromToken(token); - // 比较过期时间是否在当前时间之前 return expiration.before(new Date()); } /** * 从token中获取过期时间 - * * @param token JWT token - * @return Date 过期时间 + * @return 过期时间 */ public Date getExpirationDateFromToken(String token) { - // 解析token获取载荷(Claims) Claims claims = getClaimsFromToken(token); - // 从载荷中获取过期时间 return claims.getExpiration(); } /** - * 从token中解析Claims - JWT解析的核心方法 - * - * 【Claims概念】 - * Claims是JWT的载荷(Payload)部分,包含了关于用户和token的声明信息。 - * 标准声明包括:sub(主体)、exp(过期时间)、iat(签发时间)等。 - * - * 【解析流程】 - * 1. 创建JWT解析器(Parser) - * 2. 设置签名密钥用于验证token完整性 - * 3. 解析token字符串,验证签名 - * 4. 提取并返回Claims对象 - * - * 【安全机制】 - * 解析过程中会自动验证token的签名,如果token被篡改, - * 签名验证会失败并抛出异常,确保了token的安全性。 - * - * 【为什么设计为private?】 - * 这是一个内部工具方法,只供本类的其他方法使用, - * 不需要暴露给外部调用者,符合封装原则。 - * - * @param token JWT token字符串 - * @return Claims 载荷对象,包含token中的所有声明信息 - * @throws JwtException 如果token格式错误、签名无效或已过期 + * 从token中解析Claims + * @param token JWT token + * @return Claims */ private Claims getClaimsFromToken(String token) { return Jwts.parserBuilder() - .setSigningKey(getSigningKey()) // 设置签名密钥用于验证token完整性 - .build() // 构建解析器 - .parseClaimsJws(token) // 解析JWT token并验证签名 - .getBody(); // 获取载荷部分(Claims) + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); } /** - * 获取签名密钥 - 将配置的字符串密钥转换为加密算法所需的密钥对象 - * - * 【转换目的】 - * JJWT库需要SecretKey对象来进行HMAC签名算法, - * 而我们配置的是字符串,需要转换为合适的密钥格式。 - * - * 【HMAC算法说明】 - * HMAC (Hash-based Message Authentication Code) 是一种基于哈希的消息认证码算法。 - * 它结合了哈希函数和密钥,既能验证数据完整性,又能验证数据来源。 - * - * 【转换过程】 - * 1. 将字符串密钥转换为字节数组 - * 2. 使用Keys.hmacShaKeyFor()方法生成适合HMAC-SHA算法的密钥 - * 3. 返回SecretKey对象供签名和验证使用 - * - * 【安全考虑】 - * - 密钥长度必须足够(至少256位) - * - 密钥应该是随机生成的,不能是简单的字符串 - * - 密钥必须保密,不能泄露给客户端 - * - * @return SecretKey 适用于HMAC-SHA算法的签名密钥 + * 获取签名密钥 + * @return 签名密钥 */ - private static SecretKey getSigningKey() { - // 将字符串密钥转换为字节数组 - // 使用UTF-8编码确保字符串到字节的转换一致性 + private SecretKey getSigningKey() { byte[] keyBytes = secret.getBytes(); - // 使用JJWT提供的工具方法生成HMAC-SHA密钥 - // 该方法会自动选择合适的HMAC算法(HS256、HS384、HS512) return Keys.hmacShaKeyFor(keyBytes); } /** - * 从请求头中提取token - 解析HTTP Authorization头获取JWT token - * - * 【HTTP Authorization头格式】 - * 根据RFC 6750 (OAuth 2.0 Bearer Token Usage)标准, - * JWT token应该放在HTTP请求头的Authorization字段中, - * 格式为: "Authorization: Bearer " - * - * 【为什么使用Bearer?】 - * Bearer是OAuth 2.0标准中定义的token类型,表示"持有者token", - * 意思是任何持有该token的人都可以使用它,无需额外的身份验证。 - * - * 【提取逻辑】 - * 1. 检查Authorization头是否存在 - * 2. 验证是否以"Bearer "开头(注意Bearer后有一个空格) - * 3. 提取"Bearer "后面的token部分 - * 4. 如果格式不正确,返回null - * - * 【使用场景】 - * 主要在JWT认证过滤器中使用,用于从HTTP请求中提取token, - * 然后进行后续的验证和用户身份识别。 - * - * 【安全考虑】 - * - 只接受标准的Bearer格式,拒绝其他格式 - * - 返回null而不是抛出异常,便于调用者处理 - * - 不对token内容进行验证,只负责提取 - * - * @param authHeader HTTP请求中的Authorization头的值 - * @return String JWT token字符串,如果格式不正确则返回null - * - * 【示例】 - * 输入: "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - * 输出: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * 从请求头中提取token + * @param authHeader Authorization头 + * @return JWT token */ public String extractTokenFromHeader(String authHeader) { - // 检查Authorization头是否存在且以"Bearer "开头 - // 注意:"Bearer "后面有一个空格,这是标准格式要求 if (authHeader != null && authHeader.startsWith("Bearer ")) { - // 提取Bearer后面的token部分 - // substring(7)是因为"Bearer "有7个字符(包括空格) return authHeader.substring(7); } - // 如果格式不正确,返回null,让调用者知道没有有效的token return null; } } \ No newline at end of file -- Gitee From 74219dde223c85bd2a925955a40ec4096d789051 Mon Sep 17 00:00:00 2001 From: Jun <2783122898@qq.com> Date: Wed, 17 Sep 2025 15:47:38 +0800 Subject: [PATCH 5/8] =?UTF-8?q?RabbitMQ=E6=96=B0=E5=A2=9Epom=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20maven=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- user-service/pom.xml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/user-service/pom.xml b/user-service/pom.xml index c69afc8..360e9a2 100644 --- a/user-service/pom.xml +++ b/user-service/pom.xml @@ -69,6 +69,26 @@ knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version}
+ + + + com.alibaba.fastjson2 + fastjson2 + 2.0.58 + + + + + org.springframework.boot + spring-boot-starter-amqp + + + + org.springframework.amqp + spring-rabbit-test + test + +
-- Gitee From aac76f05468c0cf284e5e7b1817739ec992baa38 Mon Sep 17 00:00:00 2001 From: Jun <2783122898@qq.com> Date: Wed, 17 Sep 2025 15:51:22 +0800 Subject: [PATCH 6/8] RabbitConfig --- .../qa-service-config/RabbitConfig.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 qa-service/qa-service-config/RabbitConfig.java diff --git a/qa-service/qa-service-config/RabbitConfig.java b/qa-service/qa-service-config/RabbitConfig.java new file mode 100644 index 0000000..a5e17d4 --- /dev/null +++ b/qa-service/qa-service-config/RabbitConfig.java @@ -0,0 +1,56 @@ +package com.example.mq.demo.config; + +import com.example.mq.demo.domain.Student; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.DefaultJackson2JavaTypeMapper; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class RabbitConfig { + //让springboot启动时自动创建队列 + + @Bean + public Queue queue() { + return new Queue("test"); + } + + @Bean + public Queue queue2() { + return new Queue("test2"); + } + + @Bean + public Queue queue3() { + return new Queue("test3"); + } + + @Bean + public MessageConverter messageConverter() { + Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); + // 配置类型映射器以支持 Student 类的反序列化 + DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper(); + Map> idClassMapping = new HashMap<>(); + // 添加 Student 类的映射 如果有更多的类需要转换,则继续添加 + idClassMapping.put("com.example.mq.demo.domain.Student", Student.class); + typeMapper.setIdClassMapping(idClassMapping); + typeMapper.setTrustedPackages("com.example.mq.demo.domain"); + converter.setJavaTypeMapper(typeMapper); + + return converter; + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + template.setMessageConverter(messageConverter()); + return template; + } +} \ No newline at end of file -- Gitee From 0a2d4ced7a1ecdbad93326603df39bd08200bc78 Mon Sep 17 00:00:00 2001 From: Jun <2783122898@qq.com> Date: Wed, 17 Sep 2025 17:06:11 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=88=E4=BD=BF=E7=94=A8MQ=E5=BC=82=E6=AD=A5=E6=8A=8A?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E5=86=99=E5=85=A5=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E4=B8=AD=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../qa-adapter-in/qa-adapter-in-web/pom.xml | 23 ++++++++ .../in/web/controller/qaController.java | 26 ++++++++- .../adapter/in/web/listener/QaListener.java | 23 ++++++++ .../qa-service-config/RabbitConfig.java | 56 ------------------- .../example/xmut/qa/service/domain/Qa.java | 4 ++ user-service/pom.xml | 18 ------ 6 files changed, 73 insertions(+), 77 deletions(-) create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java delete mode 100644 qa-service/qa-service-config/RabbitConfig.java diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml index 973f362..2d991cc 100644 --- a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/pom.xml @@ -35,6 +35,23 @@ lombok
+ + org.springframework.boot + spring-boot-starter-amqp + + + + org.springframework.amqp + spring-rabbit-test + test + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.58 + + com.example qa-service-application @@ -46,6 +63,12 @@ knife4j-openapi3-jakarta-spring-boot-starter ${knife4j.version} + + com.example + qa-adapter-out-persistence + 0.0.1-SNAPSHOT + compile +
diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java index 08205e2..32c5cc4 100644 --- a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java @@ -1,15 +1,18 @@ package org.example.xmut.qa.adapter.in.web.controller; +import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.example.xmut.qa.adapter.in.web.dto.CreateQaRequestDTO; -import org.example.xmut.qa.adapter.in.web.dto.QaResponseDTO; -import org.example.xmut.qa.adapter.in.web.dto.UpdateQaRequestDTO; +import org.example.xmut.qa.adapter.in.web.dto.*; import org.example.xmut.qa.service.application.command.CreateQaCommand; import org.example.xmut.qa.service.application.command.UpdateQaCommand; import org.example.xmut.qa.service.application.port.in.*; import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -26,6 +29,9 @@ public class qaController { private final UpdateQaUseCase updateQaUseCase; private final GetQaByIdUseCase getQaByIdUseCase; + @Resource + private RabbitTemplate rabbitTemplate; + @GetMapping("") public List getQas() { log.info("getQas"); @@ -71,4 +77,18 @@ public class qaController { qa.getAnswer().answer()); return qaResponseDTO; } + + + //新增mq添加到数据库的方法 + @GetMapping("sendObject") + public String sendObject() { + //把消息交给mq处理 + Qa qa = new Qa(); + qa.setId(new QaId(1L)); + qa.setQuestion(new Question("question")); + qa.setAnswer(new Answer("answer")); + rabbitTemplate.convertAndSend("qaTest",qa); + return "success"; + } + } diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java new file mode 100644 index 0000000..ebba911 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/listener/QaListener.java @@ -0,0 +1,23 @@ +package org.example.xmut.qa.adapter.in.web.listener; + +import jakarta.annotation.Resource; + +import org.example.xmut.qa.adapter.out.persistence.convertor.QaConvertor; +import org.example.xmut.qa.adapter.out.persistence.entity.QaEntity; +import org.example.xmut.qa.adapter.out.persistence.mapper.QaMapper; +import org.example.xmut.qa.service.domain.Qa; +import org.springframework.amqp.rabbit.annotation.RabbitHandler; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +@RabbitListener(queues = "qaTest") +public class QaListener { + @Resource + private QaMapper qaMapper; + @RabbitHandler + public void handler(Qa qa){ + QaEntity qaEntity = QaConvertor.toEntity(qa); + qaMapper.insert(qaEntity); + } +} diff --git a/qa-service/qa-service-config/RabbitConfig.java b/qa-service/qa-service-config/RabbitConfig.java deleted file mode 100644 index a5e17d4..0000000 --- a/qa-service/qa-service-config/RabbitConfig.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.example.mq.demo.config; - -import com.example.mq.demo.domain.Student; -import org.springframework.amqp.core.Queue; -import org.springframework.amqp.rabbit.connection.ConnectionFactory; -import org.springframework.amqp.rabbit.core.RabbitTemplate; -import org.springframework.amqp.support.converter.DefaultJackson2JavaTypeMapper; -import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; -import org.springframework.amqp.support.converter.MessageConverter; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import java.util.HashMap; -import java.util.Map; - -@Configuration -public class RabbitConfig { - //让springboot启动时自动创建队列 - - @Bean - public Queue queue() { - return new Queue("test"); - } - - @Bean - public Queue queue2() { - return new Queue("test2"); - } - - @Bean - public Queue queue3() { - return new Queue("test3"); - } - - @Bean - public MessageConverter messageConverter() { - Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); - // 配置类型映射器以支持 Student 类的反序列化 - DefaultJackson2JavaTypeMapper typeMapper = new DefaultJackson2JavaTypeMapper(); - Map> idClassMapping = new HashMap<>(); - // 添加 Student 类的映射 如果有更多的类需要转换,则继续添加 - idClassMapping.put("com.example.mq.demo.domain.Student", Student.class); - typeMapper.setIdClassMapping(idClassMapping); - typeMapper.setTrustedPackages("com.example.mq.demo.domain"); - converter.setJavaTypeMapper(typeMapper); - - return converter; - } - - @Bean - public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { - RabbitTemplate template = new RabbitTemplate(connectionFactory); - template.setMessageConverter(messageConverter()); - return template; - } -} \ No newline at end of file diff --git a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java index 8bbcf5d..5d31b57 100644 --- a/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java +++ b/qa-service/qa-service-domain/src/main/java/org/example/xmut/qa/service/domain/Qa.java @@ -31,6 +31,10 @@ public class Qa { this.answer=answer; } + public Qa() { + + } + public static List getQas(GetQaListPort getQaListPort) { return getQaListPort.getQas(); } diff --git a/user-service/pom.xml b/user-service/pom.xml index 360e9a2..931dcb0 100644 --- a/user-service/pom.xml +++ b/user-service/pom.xml @@ -71,24 +71,6 @@ - - com.alibaba.fastjson2 - fastjson2 - 2.0.58 - - - - - org.springframework.boot - spring-boot-starter-amqp - - - - org.springframework.amqp - spring-rabbit-test - test - - -- Gitee From 09be4e877ba29406d279186d83ac8bf4c9d40004 Mon Sep 17 00:00:00 2001 From: little_jin <861165942@qq.com> Date: Wed, 17 Sep 2025 21:03:24 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E5=90=91KIMI?= =?UTF-8?q?=E5=8F=91=E9=80=81=E8=AF=B7=E6=B1=82=EF=BC=8C=E5=B9=B6=E4=BD=BF?= =?UTF-8?q?=E7=94=A8RabbitMQ=E5=B0=86KIMI=E7=9A=84=E5=9B=9E=E7=AD=94?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E4=B8=8E=E7=94=A8=E6=88=B7=E6=8F=90=E9=97=AE?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=E5=AD=98=E5=85=A5=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E4=B8=AD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/config/MoonShotConfig.java | 14 +++ .../qa/adapter/in/web/config/RestConfig.java | 13 +++ .../in/web/controller/kimiController.java | 104 ++++++++++++++++++ .../in/web/controller/qaController.java | 24 ++-- .../in/web/dto/ChatCompletionRequestDTO.java | 12 ++ .../in/web/dto/ChatCompletionResponseDTO.java | 14 +++ .../xmut/qa/adapter/in/web/dto/Choice.java | 10 ++ .../xmut/qa/adapter/in/web/dto/Message.java | 9 ++ 8 files changed, 188 insertions(+), 12 deletions(-) create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java create mode 100644 qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java new file mode 100644 index 0000000..e474022 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/MoonShotConfig.java @@ -0,0 +1,14 @@ +package org.example.xmut.qa.adapter.in.web.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "moonshot") +public class MoonShotConfig { + private String url; + private String key; + private String model; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java new file mode 100644 index 0000000..425e041 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/config/RestConfig.java @@ -0,0 +1,13 @@ +package org.example.xmut.qa.adapter.in.web.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestConfig { + @Bean + public RestTemplate getRestTemplate(){ + return new RestTemplate(); + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java new file mode 100644 index 0000000..44a6ed7 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/kimiController.java @@ -0,0 +1,104 @@ +package org.example.xmut.qa.adapter.in.web.controller; + + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.example.xmut.qa.adapter.in.web.config.MoonShotConfig; +import org.example.xmut.qa.adapter.in.web.dto.ChatCompletionRequestDTO; +import org.example.xmut.qa.adapter.in.web.dto.ChatCompletionResponseDTO; +import org.example.xmut.qa.adapter.in.web.dto.Choice; +import org.example.xmut.qa.adapter.in.web.dto.Message; +import org.example.xmut.qa.service.common.IdWorker; +import org.example.xmut.qa.service.domain.Qa; +import org.example.xmut.qa.service.domain.valueobject.Answer; +import org.example.xmut.qa.service.domain.valueobject.QaId; +import org.example.xmut.qa.service.domain.valueobject.Question; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Tag(name="示例控制器") +@RestController +@RequestMapping("kimi") +public class kimiController { + @Resource + private RestTemplate restTemplate; + + @Resource + private MoonShotConfig moonShotConfig; + + @Resource + private RabbitTemplate rabbitTemplate; + + // 创建IdWorker实例用于生成唯一ID + private static final IdWorker idWorker = new IdWorker(); + + @Operation(summary = "kimi示例") + @GetMapping("demo") + public List demo(@RequestParam("question") String question) { + log.info("demo"); + + //创建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + moonShotConfig.getKey()); + //创建请求体 + ChatCompletionRequestDTO request = new ChatCompletionRequestDTO(); + request.setModel(moonShotConfig.getModel()); + request.setTemperature(0.6); + //创建消息列表 + Message systemMessage = new Message(); + systemMessage.setRole("system"); + systemMessage.setContent("你是 Kimi,由 Moonshot AI 提供的人工智能助手,你更擅长中文和英文的对话。你会为用户提供安全,有帮助,准确的回答。同时,你会拒绝一切涉及恐怖主义,种族歧视,黄色暴力等问题的回答。Moonshot AI 为专有名词,不可翻译成其他语言。"); + + Message userMessage=new Message(); + userMessage.setRole("user"); + userMessage.setContent(question); + + request.setMessages(Arrays.asList( + systemMessage,userMessage + )); + + //创建Http实体 + HttpEntity response= restTemplate.exchange( + moonShotConfig.getUrl(), + HttpMethod.POST, + new HttpEntity<>(request, headers), + ChatCompletionResponseDTO.class + ); + + log.info("response:{}",response); + + List choices = response.getBody().getChoices(); + + for (Choice choice : choices) { + //把消息交给mq处理 + Qa qa = new Qa(); + // 使用IdWorker生成唯一ID + qa.setId(new QaId(idWorker.nextId())); + // 用户的原始问题 + qa.setQuestion(new Question(question)); + // Kimi的回答内容 + qa.setAnswer(new Answer(choice.getMessage().getContent())); + rabbitTemplate.convertAndSend("qaTest",qa); + } + + return choices; + } +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java index 32c5cc4..3e65761 100644 --- a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/controller/qaController.java @@ -39,7 +39,7 @@ public class qaController { } @PostMapping() - public Qa createqa(@RequestBody CreateQaRequestDTO createqaRequestDTO){ + public Qa createQa(@RequestBody CreateQaRequestDTO createqaRequestDTO){ CreateQaCommand command=CreateQaCommand.builder() .question(createqaRequestDTO.question()) @@ -79,16 +79,16 @@ public class qaController { } - //新增mq添加到数据库的方法 - @GetMapping("sendObject") - public String sendObject() { - //把消息交给mq处理 - Qa qa = new Qa(); - qa.setId(new QaId(1L)); - qa.setQuestion(new Question("question")); - qa.setAnswer(new Answer("answer")); - rabbitTemplate.convertAndSend("qaTest",qa); - return "success"; - } +// //新增mq添加到数据库的方法 +// @GetMapping("sendObject") +// public String sendObject() { +// //把消息交给mq处理 +// Qa qa = new Qa(); +// qa.setId(new QaId(1L)); +// qa.setQuestion(new Question("question")); +// qa.setAnswer(new Answer("answer")); +// rabbitTemplate.convertAndSend("qaTest",qa); +// return "success"; +// } } diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java new file mode 100644 index 0000000..a2185a9 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionRequestDTO.java @@ -0,0 +1,12 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChatCompletionRequestDTO { + private String model; + private List messages; + private double temperature; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java new file mode 100644 index 0000000..50c6bde --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/ChatCompletionResponseDTO.java @@ -0,0 +1,14 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +import java.util.List; + +@Data +public class ChatCompletionResponseDTO { + private String id; + private String object; + private long created; + private String model; + private List choices; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java new file mode 100644 index 0000000..586ca17 --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Choice.java @@ -0,0 +1,10 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +@Data +public class Choice { + private int index; + private Message message; + private String finish_reason; +} diff --git a/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java new file mode 100644 index 0000000..ffd0d8c --- /dev/null +++ b/qa-service/qa-service-adapter/qa-adapter-in/qa-adapter-in-web/src/main/java/org/example/xmut/qa/adapter/in/web/dto/Message.java @@ -0,0 +1,9 @@ +package org.example.xmut.qa.adapter.in.web.dto; + +import lombok.Data; + +@Data +public class Message { + private String role; + private String content; +} -- Gitee