refactor: 拆分出认证服务

This commit is contained in:
2025-11-20 22:43:05 +08:00
parent f93ec43915
commit c03876e29e
41 changed files with 662 additions and 261 deletions

1
.idea/encodings.xml generated
View File

@@ -3,6 +3,7 @@
<component name="Encoding"> <component name="Encoding">
<file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-ai-service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-client/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/aioj-backend-common/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/aioj-backend-common/src/main/java" charset="UTF-8" />

5
.idea/misc.xml generated
View File

@@ -7,6 +7,11 @@
<option value="$PROJECT_DIR$/pom.xml" /> <option value="$PROJECT_DIR$/pom.xml" />
</list> </list>
</option> </option>
<option name="ignoredFiles">
<set>
<option value="$PROJECT_DIR$/aioj-backend-model/pom.xml" />
</set>
</option>
</component> </component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK"> <component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="zulu-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" /> <output url="file://$PROJECT_DIR$/out" />

100
aioj-backend-auth/pom.xml Normal file
View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.meowrain</groupId>
<artifactId>ai-oj</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>aioj-backend-auth</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- spring cloud发现服务-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- OAuth2 Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<!--
引用通用模块
-->
<dependency>
<groupId>cn.meowrain</groupId>
<artifactId>aioj-backend-common</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!--引入openfeign-->
<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<version>4.3.0</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,13 @@
package cn.meowrain.aioj.backend.auth;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients(basePackages = "cn.meowrain.aioj.backend.auth.clients")
@SpringBootApplication
public class AIOJAuthApplication {
public static void main(String[] args) {
SpringApplication.run(AIOJAuthApplication.class, args);
}
}

View File

@@ -0,0 +1,13 @@
package cn.meowrain.aioj.backend.auth.clients;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.framework.web.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "user-service", path = "/api/v1/user")
public interface UserClient {
@GetMapping("/inner/get-by-username")
Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount);
}

View File

@@ -0,0 +1,20 @@
package cn.meowrain.aioj.backend.auth.common.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public enum ChainMarkEnums {
/**
* 用户登录请求验证
*/
USER_LOGIN_REQ_PARAM_VERIFY("USER_LOGIN_REQ_PARAM_VERIFY");
@Getter
private final String markName;
@Override
public String toString() {
return markName;
}
}

View File

@@ -1,4 +1,4 @@
package cn.meowrain.aioj.backend.userservice.config; package cn.meowrain.aioj.backend.auth.config;
import lombok.Data; import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;

View File

@@ -1,4 +1,4 @@
package cn.meowrain.aioj.backend.userservice.config; package cn.meowrain.aioj.backend.auth.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -10,18 +10,10 @@ import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import cn.meowrain.aioj.backend.userservice.security.JwtAuthenticationFilter;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
public class SecurityConfiguration { public class SecurityConfiguration {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfiguration(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
@@ -30,7 +22,7 @@ public class SecurityConfiguration {
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers( .requestMatchers(
"/auth/**", "/v1/auth/**",
"/doc.html", "/doc.html",
"/swagger-ui/**", "/swagger-ui/**",
"/swagger-resources/**", "/swagger-resources/**",
@@ -39,8 +31,7 @@ public class SecurityConfiguration {
"/favicon.ico" "/favicon.ico"
) )
.permitAll() .permitAll()
.anyRequest().authenticated()) .anyRequest().authenticated());
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build(); return http.build();
} }

View File

@@ -0,0 +1,38 @@
package cn.meowrain.aioj.backend.auth.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
@EnableKnife4j
public class SwaggerConfiguration implements ApplicationRunner {
@Value("${server.port:8080}")
private String serverPort;
@Value("${server.servlet.context-path:}")
private String contextPath;
@Bean
public OpenAPI customerOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("AIOJ-renz微服务✨")
.description("用户认证功能")
.version("v1.0.0")
.contact(new Contact().name("meowrain").email("meowrain@126.com"))
.license(new License().name("MeowRain").url("https://meowrain.cn")));
}
@Override
public void run(ApplicationArguments args) throws Exception {
log.info("✨API Document: http://127.0.0.1:{}{}/doc.html", serverPort, contextPath);
}
}

View File

@@ -0,0 +1,35 @@
package cn.meowrain.aioj.backend.auth.controller;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.framework.web.Results;
import cn.meowrain.aioj.backend.framework.web.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/v1/auth")
public class AuthController {
private final AuthService authService;
public AuthController(AuthService authService) {
this.authService = authService;
}
@PostMapping("/login")
public Result<UserLoginResponseDTO> login(@RequestBody UserLoginRequestDTO userLoginRequest) {
UserLoginResponseDTO userLoginResponse = authService.userLogin(userLoginRequest);
return Results.success(userLoginResponse);
}
@PostMapping("/auth")
public Result<String> auth(@RequestBody UserLoginRequestDTO userLoginRequest) {
UserLoginResponseDTO userLoginResponseDTO = authService.userLogin(userLoginRequest);
return Results.success(userLoginResponseDTO.getToken());
}
}

View File

@@ -1,19 +1,19 @@
package cn.meowrain.aioj.backend.userservice.dto.chains; package cn.meowrain.aioj.backend.auth.dto.chains;
import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler; import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHandler;
import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.exception.ClientException; import cn.meowrain.aioj.backend.framework.exception.ClientException;
import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@Slf4j @Slf4j
public class UserLoginRequestParamVerifyChain implements AbstractChianHandler<UserLoginRequest> { public class UserLoginRequestParamVerifyChain implements AbstractChianHandler<UserLoginRequestDTO> {
@Override @Override
public void handle(UserLoginRequest requestParam) { public void handle(UserLoginRequestDTO requestParam) {
if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) { if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) {
throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR); throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR);
} }

View File

@@ -1,9 +1,9 @@
package cn.meowrain.aioj.backend.userservice.dto.chains.context; package cn.meowrain.aioj.backend.auth.dto.chains.context;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.framework.designpattern.chains.CommonChainContext; import cn.meowrain.aioj.backend.framework.designpattern.chains.CommonChainContext;
import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class UserLoginRequestParamVerifyContext extends CommonChainContext<UserLoginRequest> { public class UserLoginRequestParamVerifyContext extends CommonChainContext<UserLoginRequestDTO> {
} }

View File

@@ -0,0 +1,9 @@
package cn.meowrain.aioj.backend.auth.dto.req;
import lombok.Data;
@Data
public class UserLoginRequestDTO {
private String userAccount;
private String userPassword;
}

View File

@@ -0,0 +1,68 @@
package cn.meowrain.aioj.backend.auth.dto.resp;
import lombok.Data;
import java.util.Date;
/**
* 用户认证响应体
*/
@Data
public class UserAuthRespDTO {
/**
* id
*/
private Long id;
/**
* 用户账号
*/
private String userAccount;
/**
* 用户密码
*/
private String userPassword;
/**
* 开放平台id
*/
private String unionId;
/**
* 公众号openId
*/
private String mpOpenId;
/**
* 用户昵称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户简介
*/
private String userProfile;
/**
* 用户角色user/admin/ban
*/
private String userRole;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -0,0 +1,73 @@
package cn.meowrain.aioj.backend.auth.dto.resp;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class UserLoginResponseDTO implements Serializable {
/**
* id
*/
private Long id;
/**
* 用户账号
*/
private String userAccount;
/**
* 开放平台id
*/
private String unionId;
/**
* 公众号openId
*/
private String mpOpenId;
/**
* 用户昵称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户简介
*/
private String userProfile;
/**
* 用户角色user/admin/ban
*/
private String userRole;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 是否删除
*/
private Integer isDelete;
/**
* JWT令牌登录成功返回
*/
private String token;
private static final long serialVersionUID = 1L;
}

View File

@@ -0,0 +1,14 @@
package cn.meowrain.aioj.backend.auth.service;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
public interface AuthService {
/**
* 用户登录
* @param request {@link UserLoginRequestDTO}
* @return {@link UserLoginResponseDTO}
*/
UserLoginResponseDTO userLogin(UserLoginRequestDTO request);
}

View File

@@ -0,0 +1,58 @@
package cn.meowrain.aioj.backend.auth.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.crypto.digest.BCrypt;
import cn.meowrain.aioj.backend.auth.clients.UserClient;
import cn.meowrain.aioj.backend.auth.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.auth.dto.chains.context.UserLoginRequestParamVerifyContext;
import cn.meowrain.aioj.backend.auth.dto.req.UserLoginRequestDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.auth.dto.resp.UserLoginResponseDTO;
import cn.meowrain.aioj.backend.auth.service.AuthService;
import cn.meowrain.aioj.backend.auth.utils.JwtUtil;
import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.exception.ServiceException;
import cn.meowrain.aioj.backend.framework.web.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthServiceImpl implements AuthService {
private final JwtUtil jwtUtil;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
private final UserClient userClient;
@Override
public UserLoginResponseDTO userLogin(UserLoginRequestDTO requestParam) {
// 1.校验
userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(),
requestParam);
// 如果调用user-service失败那么就说明是系统内部错误
Result<UserAuthRespDTO> userResp = userClient.getUserByUserName(requestParam.getUserAccount());
if (userResp.isFail()) {
log.error("调用user-service返回失败{}", userResp.getMessage());
throw new ServiceException(ErrorCode.SYSTEM_ERROR);
}
UserAuthRespDTO user = userResp.getData();
if (ObjectUtil.isNull(user)
|| !BCrypt.checkpw(requestParam.getUserPassword(), user.getUserPassword())) {
throw new ServiceException("用户不存在或者密码错误", ErrorCode.NOT_LOGIN_ERROR);
}
// 生成 JWT
String token = jwtUtil.generateToken(user);
UserLoginResponseDTO resp = new UserLoginResponseDTO();
resp.setId(user.getId());
resp.setUserAccount(user.getUserAccount());
resp.setUserAvatar(user.getUserAvatar());
resp.setUserProfile(user.getUserProfile());
resp.setUserRole(user.getUserRole());
resp.setCreateTime(user.getCreateTime());
resp.setUpdateTime(user.getUpdateTime());
resp.setToken(token);
return resp;
}
}

View File

@@ -1,7 +1,7 @@
package cn.meowrain.aioj.backend.userservice.utils; package cn.meowrain.aioj.backend.auth.utils;
import cn.meowrain.aioj.backend.userservice.config.JwtPropertiesConfiguration; import cn.meowrain.aioj.backend.auth.config.JwtPropertiesConfiguration;
import cn.meowrain.aioj.backend.userservice.dao.entity.User; import cn.meowrain.aioj.backend.auth.dto.resp.UserAuthRespDTO;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
@@ -25,7 +25,7 @@ public class JwtUtil {
return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes()); return Keys.hmacShaKeyFor(jwtConfig.getSecret().getBytes());
} }
public String generateToken(User user) { public String generateToken(UserAuthRespDTO user) {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
Map<String, Object> claims = new HashMap<>(); Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId()); claims.put("userId", user.getId());

View File

@@ -0,0 +1,9 @@
spring:
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: 10.0.0.10:8848
username: nacos
password: nacos

View File

@@ -0,0 +1,37 @@
spring:
application:
name: auth-service
profiles:
active: @env@
devtools:
livereload:
enabled: true
server:
port: 10011
servlet:
context-path: /api
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
default-flat-param-object: true
swagger-ui:
path: /swagger-ui.html
tags-sorter: alpha
operations-sorter: alpha
group-configs:
- group: 'default'
paths-to-match: '/**'
packages-to-scan: cn.meowrain.aioj.backend.userservice.controller
knife4j:
basic:
enable: true
setting:
language: zh_cn
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
jwt:
secret: "12345678901234567890123456789012" # 至少32字节
expire: 86400000 # 24小时单位毫秒

View File

@@ -41,5 +41,14 @@
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-extension</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -0,0 +1,14 @@
spring:
data:
redis:
host: 10.0.0.10
port: 6379
password: 123456
cloud:
nacos:
discovery:
enabled: true
register-enabled: true
server-addr: 10.0.0.10:8848
username: nacos
password: nacos

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.meowrain</groupId>
<artifactId>ai-oj</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>aioj-backend-model</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@@ -32,17 +32,6 @@
<artifactId>aioj-backend-common</artifactId> <artifactId>aioj-backend-common</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2 Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
@@ -81,23 +70,5 @@
<groupId>com.alibaba.cloud</groupId> <groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency> </dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
<scope>runtime</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -1,6 +1,5 @@
package cn.meowrain.aioj.backend.userservice.config; package cn.meowrain.aioj.backend.userservice.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans; import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;

View File

@@ -2,35 +2,31 @@ package cn.meowrain.aioj.backend.userservice.controller;
import cn.meowrain.aioj.backend.framework.web.Result; import cn.meowrain.aioj.backend.framework.web.Result;
import cn.meowrain.aioj.backend.framework.web.Results; import cn.meowrain.aioj.backend.framework.web.Results;
import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserLoginResponse;
import cn.meowrain.aioj.backend.userservice.service.UserService; import cn.meowrain.aioj.backend.userservice.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController() @RestController()
@RequestMapping("/auth") @RequestMapping("/v1/user")
public class UserController { public class UserController {
private final UserService userService; private final UserService userService;
@PostMapping("/register") @PostMapping("/register")
public Result<Long> register(@RequestBody UserRegisterRequest userRegisterRequest) { public Result<Long> register(@RequestBody UserRegisterRequestDTO userRegisterRequest) {
Long l = userService.userRegister(userRegisterRequest); Long l = userService.userRegister(userRegisterRequest);
return Results.success(l); return Results.success(l);
} }
@PostMapping("/login") @GetMapping("/inner/get-by-username")
public Result<UserLoginResponse> login(@RequestBody UserLoginRequest userLoginRequest) { public Result<UserAuthRespDTO> getUserByUserName(@RequestParam("userAccount") String userAccount) {
UserLoginResponse userLoginResponse = userService.userLogin(userLoginRequest); UserAuthRespDTO userAuthDTO = userService.findAuthInfoByUserAccount(userAccount);
return Results.success(userLoginResponse); return Results.success(userAuthDTO);
} }
} }

View File

@@ -4,16 +4,16 @@ import cn.meowrain.aioj.backend.framework.designpattern.chains.AbstractChianHand
import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.exception.ClientException; import cn.meowrain.aioj.backend.framework.exception.ClientException;
import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@Slf4j @Slf4j
public class UserRegisterRequestParamVerifyChain implements AbstractChianHandler<UserRegisterRequest> { public class UserRegisterRequestParamVerifyChain implements AbstractChianHandler<UserRegisterRequestDTO> {
@Override @Override
public void handle(UserRegisterRequest requestParam) { public void handle(UserRegisterRequestDTO requestParam) {
// 校验参数里面用户名和密码是不是空的 // 校验参数里面用户名和密码是不是空的
if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) { if (StringUtils.isAnyBlank(requestParam.getUserAccount(), requestParam.getUserPassword())) {
throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR); throw new ClientException("参数为空", ErrorCode.PARAMS_ERROR);

View File

@@ -1,9 +1,9 @@
package cn.meowrain.aioj.backend.userservice.dto.chains.context; package cn.meowrain.aioj.backend.userservice.dto.chains.context;
import cn.meowrain.aioj.backend.framework.designpattern.chains.CommonChainContext; import cn.meowrain.aioj.backend.framework.designpattern.chains.CommonChainContext;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class UserRegisterRequestParamVerifyContext extends CommonChainContext<UserRegisterRequest> { public class UserRegisterRequestParamVerifyContext extends CommonChainContext<UserRegisterRequestDTO> {
} }

View File

@@ -4,7 +4,7 @@ import jakarta.servlet.http.HttpServletRequest;
import lombok.Data; import lombok.Data;
@Data @Data
public class UserLoginRequest { public class UserLoginRequestDTO {
private String userAccount; private String userAccount;
private String userPassword; private String userPassword;
} }

View File

@@ -4,7 +4,7 @@ import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@Data @Data
public class UserRegisterRequest { public class UserRegisterRequestDTO {
private String userAccount; private String userAccount;
private String userPassword; private String userPassword;
private String checkPassword; private String checkPassword;

View File

@@ -0,0 +1,69 @@
package cn.meowrain.aioj.backend.userservice.dto.resp;
import lombok.Data;
import java.util.Date;
/**
* 用户认证响应体
*/
@Data
public class UserAuthRespDTO {
/**
* id
*/
private Long id;
/**
* 用户账号
*/
private String userAccount;
/**
* 用户密码
*/
private String userPassword;
/**
* 开放平台id
*/
private String unionId;
/**
* 公众号openId
*/
private String mpOpenId;
/**
* 用户昵称
*/
private String userName;
/**
* 用户头像
*/
private String userAvatar;
/**
* 用户简介
*/
private String userProfile;
/**
* 用户角色user/admin/ban
*/
private String userRole;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
}

View File

@@ -7,7 +7,7 @@ import java.io.Serializable;
import java.util.Date; import java.util.Date;
@Data @Data
public class UserLoginResponse implements Serializable { public class UserLoginResponseDTO implements Serializable {
/** /**
* id * id
*/ */

View File

@@ -8,6 +8,6 @@ import java.io.Serializable;
* 用户注册成功响应对象 * 用户注册成功响应对象
*/ */
@Data @Data
public class UserRegisterResponse implements Serializable { public class UserRegisterResponseDTO implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
} }

View File

@@ -1,35 +0,0 @@
package cn.meowrain.aioj.backend.userservice.security;
import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.security.core.GrantedAuthority;
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.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserMapper userMapper;
public CustomUserDetailsService(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.selectOne(Wrappers.lambdaQuery(User.class).eq(User::getUserAccount, username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
Collection<? extends GrantedAuthority> authorities = Collections
.singletonList(new SimpleGrantedAuthority(user.getUserRole()));
return new org.springframework.security.core.userdetails.User(user.getUserAccount(), user.getUserPassword(),
authorities);
}
}

View File

@@ -1,76 +0,0 @@
package cn.meowrain.aioj.backend.userservice.security;
import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper;
import cn.meowrain.aioj.backend.userservice.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
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;
import java.util.Collections;
import java.util.List;
/**
* JWT请求过滤器解析 Authorization 头中的 Bearer token 并设置认证上下文
*/
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final UserMapper userMapper;
public JwtAuthenticationFilter(JwtUtil jwtUtil, UserMapper userMapper) {
this.jwtUtil = jwtUtil;
this.userMapper = userMapper;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (!StringUtils.hasText(header) || !header.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = header.substring(7);
if (!jwtUtil.isTokenValid(token)) {
filterChain.doFilter(request, response);
return;
}
try {
Claims claims = jwtUtil.parseClaims(token);
String account = claims.getSubject();
Long userId = claims.get("userId", Long.class);
// 避免重复设置
if (StringUtils.hasText(account) && SecurityContextHolder.getContext().getAuthentication() == null) {
// 可选:从数据库再查一次,保证用户未被删除/封禁
User user = userMapper.selectById(userId);
if (user == null || !StringUtils.hasText(user.getUserRole())) {
filterChain.doFilter(request, response);
return;
}
List<SimpleGrantedAuthority> authorities = Collections
.singletonList(new SimpleGrantedAuthority(user.getUserRole()));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(account,
null, authorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception ignored) {
// 解析失败直接继续,不抛出,保持无状态
}
filterChain.doFilter(request, response);
}
}

View File

@@ -1,23 +1,25 @@
package cn.meowrain.aioj.backend.userservice.service; package cn.meowrain.aioj.backend.userservice.service;
import cn.meowrain.aioj.backend.userservice.dao.entity.User; import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserLoginResponse; import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
public interface UserService extends IService<User> { public interface UserService extends IService<User> {
/** /**
* 用户注册 * 用户注册
* @param request {@link UserRegisterRequest} * @param request {@link cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO}
* @return {@link Long} * @return {@link Long}
*/ */
Long userRegister(UserRegisterRequest request); Long userRegister(UserRegisterRequestDTO request);
/** /**
* 用户登录 * 根据用户账号查找用户认证信息
* @param request {@link UserLoginRequest} * @param userAccount
* @return {@link UserLoginResponse} * @return
*/ */
UserLoginResponse userLogin(UserLoginRequest request); UserAuthRespDTO findAuthInfoByUserAccount(String userAccount);
} }

View File

@@ -1,46 +1,48 @@
package cn.meowrain.aioj.backend.userservice.service.impl; package cn.meowrain.aioj.backend.userservice.service.impl;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.crypto.digest.BCrypt;
import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode; import cn.meowrain.aioj.backend.framework.errorcode.ErrorCode;
import cn.meowrain.aioj.backend.framework.exception.ClientException;
import cn.meowrain.aioj.backend.framework.exception.ServiceException; import cn.meowrain.aioj.backend.framework.exception.ServiceException;
import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums; import cn.meowrain.aioj.backend.userservice.common.enums.ChainMarkEnums;
import cn.meowrain.aioj.backend.userservice.dao.entity.User; import cn.meowrain.aioj.backend.userservice.dao.entity.User;
import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper; import cn.meowrain.aioj.backend.userservice.dao.mapper.UserMapper;
import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserLoginRequestParamVerifyContext;
import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserRegisterRequestParamVerifyContext; import cn.meowrain.aioj.backend.userservice.dto.chains.context.UserRegisterRequestParamVerifyContext;
import cn.meowrain.aioj.backend.userservice.dto.req.UserLoginRequest;
import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequest; import cn.meowrain.aioj.backend.userservice.dto.req.UserRegisterRequestDTO;
import cn.meowrain.aioj.backend.userservice.dto.resp.UserLoginResponse; import cn.meowrain.aioj.backend.userservice.dto.resp.UserAuthRespDTO;
import cn.meowrain.aioj.backend.userservice.service.UserService; import cn.meowrain.aioj.backend.userservice.service.UserService;
import cn.meowrain.aioj.backend.userservice.utils.JwtUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Date; import java.util.Date;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext; private final UserRegisterRequestParamVerifyContext userRegisterRequestParamVerifyContext;
private final UserLoginRequestParamVerifyContext userLoginRequestParamVerifyContext;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
@Override @Override
public Long userRegister(UserRegisterRequest request) { public Long userRegister(UserRegisterRequestDTO request) {
UserAuthRespDTO authInfoByUserAccount = findAuthInfoByUserAccount(request.getUserAccount());
if(authInfoByUserAccount!=null){
throw new ClientException("重复创建用户");
}
log.info("进行用户注册");
userRegisterRequestParamVerifyContext.handler(ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(), userRegisterRequestParamVerifyContext.handler(ChainMarkEnums.USER_REGISTER_REQ_PARAM_VERIFY.getMarkName(),
request); request);
// 使用 BCrypt 加密密码 // 使用 BCrypt 加密密码
Date now = new Date(); Date now = new Date();
String encryptPassword = passwordEncoder.encode(request.getUserPassword()); String salt = BCrypt.gensalt();
String encryptPassword = BCrypt.hashpw(request.getUserPassword(),salt);
User user = new User().setUserAccount(request.getUserAccount()).setUserPassword(encryptPassword) User user = new User().setUserAccount(request.getUserAccount()).setUserPassword(encryptPassword)
.setUserRole("user").setCreateTime(now).setUpdateTime(now); .setUserRole("user").setCreateTime(now).setUpdateTime(now);
try { try {
// 需要修改表,使得用户名是唯一的 // 需要修改表,使得用户名是唯一的
this.save(user); this.save(user);
@@ -52,28 +54,15 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
return user.getId(); return user.getId();
} }
@Override @Override
public UserLoginResponse userLogin(UserLoginRequest requestParam) { public UserAuthRespDTO findAuthInfoByUserAccount(String userAccount) {
// 1.校验 User one = this.lambdaQuery().eq(User::getUserAccount, userAccount).one();
userLoginRequestParamVerifyContext.handler(ChainMarkEnums.USER_LOGIN_REQ_PARAM_VERIFY.getMarkName(), UserAuthRespDTO userAuthDTO = new UserAuthRespDTO();
requestParam); if(one!=null){
User user = this.baseMapper.selectOne(Wrappers.lambdaQuery(User.class) BeanUtils.copyProperties(one, userAuthDTO);
.eq(User::getUserAccount, requestParam.getUserAccount())); return userAuthDTO;
if (ObjectUtil.isNull(user)
|| !passwordEncoder.matches(requestParam.getUserPassword(), user.getUserPassword())) {
throw new ServiceException("用户不存在或者密码错误", ErrorCode.NOT_LOGIN_ERROR);
} }
// 生成 JWT return null;
String token = jwtUtil.generateToken(user);
UserLoginResponse resp = new UserLoginResponse();
resp.setId(user.getId());
resp.setUserAccount(user.getUserAccount());
resp.setUserAvatar(user.getUserAvatar());
resp.setUserProfile(user.getUserProfile());
resp.setUserRole(user.getUserRole());
resp.setCreateTime(user.getCreateTime());
resp.setUpdateTime(user.getUpdateTime());
resp.setToken(token);
return resp;
} }
} }

View File

@@ -5,6 +5,8 @@ spring:
active: @env@ active: @env@
server: server:
port: 10010 port: 10010
servlet:
context-path: /api
springdoc: springdoc:
api-docs: api-docs:
enabled: true enabled: true
@@ -27,6 +29,3 @@ mybatis-plus:
configuration: configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml mapper-locations: classpath*:/mapper/**/*.xml
jwt:
secret: "12345678901234567890123456789012" # 至少32字节
expire: 86400000 # 24小时单位毫秒

View File

@@ -20,10 +20,10 @@
<module>aioj-backend-gateway</module> <module>aioj-backend-gateway</module>
<module>aioj-backend-judge-service</module> <module>aioj-backend-judge-service</module>
<module>aioj-backend-user-service</module> <module>aioj-backend-user-service</module>
<module>aioj-backend-model</module>
<module>aioj-backend-question-service</module> <module>aioj-backend-question-service</module>
<module>aioj-backend-client</module> <module>aioj-backend-client</module>
<module>aioj-backend-ai-service</module> <module>aioj-backend-ai-service</module>
<module>aioj-backend-auth</module>
</modules> </modules>
<properties> <properties>
<maven.compiler.source>17</maven.compiler.source> <maven.compiler.source>17</maven.compiler.source>