diff --git a/opai-api/src/main/java/com/bw/opai/auth/controller/OpaiAuthController.java b/opai-api/src/main/java/com/bw/opai/auth/controller/OpaiAuthController.java new file mode 100644 index 0000000..3c4bcc4 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/controller/OpaiAuthController.java @@ -0,0 +1,51 @@ +package com.bw.opai.auth.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.bw.opai.auth.dto.LoginRequest; +import com.bw.opai.auth.entity.SysUser; +import com.bw.opai.auth.service.IRemoteAuthService; +import com.bw.opai.common.Res; + + +/** + * opai 系统认证转发中心 + * @author jian.mao + * @date 2026年1月28日 + * @description + */ +@RestController +@RequestMapping("/auth") +public class OpaiAuthController { + + @Autowired + private IRemoteAuthService remoteAuthService; + + /** + * opai 登录接口 + * 逻辑:透传给远端认证中心 + */ + @PostMapping("/login") + public Res login(@RequestBody LoginRequest loginReq) { + // 1. 基本参数校验 + if (loginReq == null || loginReq.getUsername() == null) { + return Res.fail("用户名不能为空"); + } + + // 2. 调用我们在 Service 里的转发逻辑 + return remoteAuthService.remoteLogin(loginReq.getUsername(), loginReq.getPassword()); + } + + /** + * opai 注册接口 + * 逻辑:透传给远端认证中心开通账号 + */ + @PostMapping("/register") + public Res register(@RequestBody SysUser user) { + return remoteAuthService.remoteRegister(user); + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/dto/LoginRequest.java b/opai-api/src/main/java/com/bw/opai/auth/dto/LoginRequest.java new file mode 100644 index 0000000..2b578e4 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/dto/LoginRequest.java @@ -0,0 +1,14 @@ +package com.bw.opai.auth.dto; + +import lombok.Data; + +@Data +public class LoginRequest { + private String username; + private String password; + + /** + * 关键:前端传过来当前是哪个系统 + */ + private String appKey; +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/dto/UserDTO.java b/opai-api/src/main/java/com/bw/opai/auth/dto/UserDTO.java new file mode 100644 index 0000000..06c251c --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/dto/UserDTO.java @@ -0,0 +1,18 @@ +package com.bw.opai.auth.dto; + +import java.io.Serializable; + +import lombok.Data; + +@Data +public class UserDTO implements Serializable { + private static final long serialVersionUID = 1L; + /** + * // 用户ID + */ + private Long userId; + /** + * // 用户名 + */ + private String username; +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/entity/SysUser.java b/opai-api/src/main/java/com/bw/opai/auth/entity/SysUser.java new file mode 100644 index 0000000..95e4f39 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/entity/SysUser.java @@ -0,0 +1,21 @@ +package com.bw.opai.auth.entity; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@TableName("sys_user") +public class SysUser { + @TableId(type = IdType.AUTO) + private Long id; + + private String username; + + private String password; + + private String email; + + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/interceptor/AuthInterceptor.java b/opai-api/src/main/java/com/bw/opai/auth/interceptor/AuthInterceptor.java new file mode 100644 index 0000000..8478317 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/interceptor/AuthInterceptor.java @@ -0,0 +1,51 @@ +package com.bw.opai.auth.interceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import com.bw.opai.auth.dto.UserDTO; +import com.bw.opai.auth.service.IRemoteAuthService; +import com.bw.opai.auth.utils.UserContext; + +/** + * 登录拦截 + * @author jian.mao + * @date 2026年1月28日 + * @description + */ +@Component +public class AuthInterceptor implements HandlerInterceptor { + + @Autowired + private IRemoteAuthService remoteAuthService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 1. 拿到 Token (逻辑参考) + String token = request.getHeader("Authorization"); + if (token == null) { + throw new RuntimeException("缺失凭证"); // 会被全局异常捕获 + } + + // 2. 跨系统校验 + UserDTO user = remoteAuthService.verifyToken(token); + if (user == null) { + throw new RuntimeException("凭证无效"); + } + + // 3. 存入上下文,解决 UserContext resolved 报错 + UserContext.setUser(user); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + // 4. 规范:用完必须清理 + UserContext.remove(); + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/service/IRemoteAuthService.java b/opai-api/src/main/java/com/bw/opai/auth/service/IRemoteAuthService.java new file mode 100644 index 0000000..b73cd78 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/service/IRemoteAuthService.java @@ -0,0 +1,21 @@ +package com.bw.opai.auth.service; + +import com.bw.opai.auth.dto.UserDTO; +import com.bw.opai.auth.entity.SysUser; +import com.bw.opai.common.Res; + +/** + * 用户管理service接口 + * @author jian.mao + * @date 2026年1月28日 + * @description + */ +public interface IRemoteAuthService { + + /*** 登录中转 ***/ + Res remoteLogin(String username, String password); + /*** 注册中转 ***/ + Res remoteRegister(SysUser user); + /*** 核心鉴权:拿着 token 去 Auth-Server 换 UserDTO ***/ + UserDTO verifyToken(String token); +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/service/impl/RemoteAuthServiceImpl.java b/opai-api/src/main/java/com/bw/opai/auth/service/impl/RemoteAuthServiceImpl.java new file mode 100644 index 0000000..23f528f --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/service/impl/RemoteAuthServiceImpl.java @@ -0,0 +1,92 @@ +package com.bw.opai.auth.service.impl; + + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.bw.opai.auth.dto.LoginRequest; +import com.bw.opai.auth.dto.UserDTO; +import com.bw.opai.auth.entity.SysUser; +import com.bw.opai.auth.service.IRemoteAuthService; +import com.bw.opai.common.Res; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Service +public class RemoteAuthServiceImpl implements IRemoteAuthService { + + private static final Logger log = LoggerFactory.getLogger(RemoteAuthServiceImpl.class); + + @Autowired + private RestTemplate restTemplate; + + /** + * 显式引入 ObjectMapper 处理 JSON 转换,确保 UserDTO 转换不出错 + */ + @Autowired + private ObjectMapper objectMapper; + + /** + * 从 Nacos 读取认证中心地址,如 http://192.168.1.100:8080 + */ + @Value("${auth-server.url}") + private String authUrl; + + /** + * opai 系统在认证中心备案的 appKey + */ + @Value("${opai.app-key}") + private String appKey; + + @Override + public Res remoteLogin(String username, String password) { + String url = authUrl + "/auth/login"; + + LoginRequest loginReq = new LoginRequest(); + loginReq.setUsername(username); + loginReq.setPassword(password); + loginReq.setAppKey(appKey); + + try { + // 调用认证中心 AuthController.login + return restTemplate.postForObject(url, loginReq, Res.class); + } catch (Exception e) { + log.error("远程登录调用失败: {}", e.getMessage()); + return Res.unAuth("认证中心连接异常"); + } + } + + @Override + public Res remoteRegister(SysUser user) { + String url = authUrl + "/auth/register"; + try { + return restTemplate.postForObject(url, user, Res.class); + } catch (Exception e) { + log.error("远程注册调用失败: {}", e.getMessage()); + return Res.fail("认证中心连接异常"); + } + } + + @Override + public UserDTO verifyToken(String token) { + // 与 AuthController.verify 接口对应: @GetMapping("/verify") + String url = authUrl + "/auth/verify?token=" + token + "&appKey=" + appKey; + + try { + // 发起 GET 请求 + Res result = restTemplate.getForObject(url, Res.class); + + if (result != null && result.getResCode() == 0 && result.getData() != null) { + // 规范点:跨系统数据转换,通过 ObjectMapper 将 LinkedHashMap 转换为真正的 UserDTO + return objectMapper.convertValue(result.getData(), UserDTO.class); + } + } catch (Exception e) { + log.error("Token 远程校验异常: {}", e.getMessage()); + // 校验失败返回 null,拦截器会根据此结果决定是否放行 + } + return null; + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/auth/utils/UserContext.java b/opai-api/src/main/java/com/bw/opai/auth/utils/UserContext.java new file mode 100644 index 0000000..bb7e495 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/auth/utils/UserContext.java @@ -0,0 +1,40 @@ +package com.bw.opai.auth.utils; + +import com.bw.opai.auth.dto.UserDTO; + +/** + * 严谨程序员必备:用户上下文容器 + * 作用:在当前线程中存储已登录的用户信息 + */ +public class UserContext { + private static final ThreadLocal USER_HOLDER = new ThreadLocal<>(); + + /** + * 存入当前登录用户 + */ + public static void setUser(UserDTO user) { + USER_HOLDER.set(user); + } + + /** + * 获取当前登录用户 + */ + public static UserDTO getUser() { + return USER_HOLDER.get(); + } + + /** + * 获取当前用户ID + */ + public static Long getUserId() { + UserDTO user = USER_HOLDER.get(); + return user != null ? user.getUserId() : null; + } + + /** + * 规范:请求结束必须清理,防止内存泄漏 + */ + public static void remove() { + USER_HOLDER.remove(); + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/common/Res.java b/opai-api/src/main/java/com/bw/opai/common/Res.java index 675a20b..bf7e203 100644 --- a/opai-api/src/main/java/com/bw/opai/common/Res.java +++ b/opai-api/src/main/java/com/bw/opai/common/Res.java @@ -28,21 +28,22 @@ public class Res { this.data = data; } - /** - * 分页结果封装 - * @param page MyBatis-Plus 分页对象 - * @param 实体类型 - * @return Res 包含 total/page/size/records - */ - public static Res> page(Page page) { - Map pageData = new HashMap<>(); - pageData.put("total", page.getTotal()); - pageData.put("page", page.getCurrent()); - pageData.put("size", page.getSize()); - pageData.put("records", page.getRecords()); - return Res.ok(pageData); - } - + /** + * 分页结果封装 + * + * @param page MyBatis-Plus 分页对象 + * @param 实体类型 + * @return Res 包含 total/page/size/records + */ + public static Res> page(Page page) { + Map pageData = new HashMap<>(); + pageData.put("total", page.getTotal()); + pageData.put("page", page.getCurrent()); + pageData.put("size", page.getSize()); + pageData.put("records", page.getRecords()); + return Res.ok(pageData); + } + public static Res ok(T data) { return new Res(ResponseCode.SUCCESS.code(), ResponseCode.SUCCESS.message(), data); } @@ -50,10 +51,17 @@ public class Res { public static Res fail(String msg) { return new Res(ResponseCode.FAIL.code(), msg, null); } + public static Res checkError(T error) { return new Res(ResponseCode.FAIL.code(), ResponseCode.CHECKERROR.message(), error); } + + + public static Res unAuth(String msg) { + return new Res(ResponseCode.AUTHRROR.code(), msg, null); + } + // getter & setter public int getResCode() { return resCode; diff --git a/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java b/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java index e87eaf7..270c60f 100644 --- a/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java +++ b/opai-api/src/main/java/com/bw/opai/common/ResponseCode.java @@ -10,7 +10,8 @@ public enum ResponseCode { SUCCESS(0, "success"), FAIL(-1, "fail"), - CHECKERROR(400, "paramsError"); + CHECKERROR(400, "paramsError"), + AUTHRROR(401, "loginError"); private final int code; private final String message; diff --git a/opai-api/src/main/java/com/bw/opai/config/CorsConfig.java b/opai-api/src/main/java/com/bw/opai/config/CorsConfig.java index cc140dc..3f9df2e 100644 --- a/opai-api/src/main/java/com/bw/opai/config/CorsConfig.java +++ b/opai-api/src/main/java/com/bw/opai/config/CorsConfig.java @@ -1,5 +1,6 @@ package com.bw.opai.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -7,12 +8,19 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class CorsConfig implements WebMvcConfigurer { + // 从 Nacos 读取,如果没配,默认放行 localhost:8080 + @Value("${opai.cors.allowed-origins}") + private String[] allowedOrigins; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") - .allowedOrigins("*") - .allowedMethods("*") + // 1. 【核心】直接使用注入的数组 + .allowedOrigins(allowedOrigins) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") - .allowCredentials(false); + // 2. 【核心】必须设为 true,否则 AUTH_TOKEN Cookie 存不下来 + .allowCredentials(true) + .maxAge(3600); } -} +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/config/RestConfig.java b/opai-api/src/main/java/com/bw/opai/config/RestConfig.java new file mode 100644 index 0000000..fe46333 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/config/RestConfig.java @@ -0,0 +1,19 @@ +package com.bw.opai.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestConfig { + @Bean + public RestTemplate restTemplate() { + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + // 建立连接超时 + factory.setConnectTimeout(3000); + // 响应超时 + factory.setReadTimeout(5000); + return new RestTemplate(factory); + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/config/WebMvcConfig.java b/opai-api/src/main/java/com/bw/opai/config/WebMvcConfig.java new file mode 100644 index 0000000..0aaff03 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/config/WebMvcConfig.java @@ -0,0 +1,26 @@ +package com.bw.opai.config; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.bw.opai.auth.interceptor.AuthInterceptor; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private AuthInterceptor authInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(authInterceptor) + // 拦截所有业务接口 + .addPathPatterns("/apps/**") + // 放行登录注册接口(否则就死循环了) + .excludePathPatterns("/auth/**") + .excludePathPatterns("/static/**"); + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/exception/BizException.java b/opai-api/src/main/java/com/bw/opai/exception/BizException.java new file mode 100644 index 0000000..afdbec7 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/exception/BizException.java @@ -0,0 +1,25 @@ +package com.bw.opai.exception; + +/** + * 自定义业务异常 + */ +public class BizException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private int code; + private String msg; + + public BizException(int code, String msg) { + super(msg); + this.code = code; + this.msg = msg; + } + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } +} \ No newline at end of file diff --git a/opai-api/src/main/java/com/bw/opai/exception/GlobalExceptionHandler.java b/opai-api/src/main/java/com/bw/opai/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..5713462 --- /dev/null +++ b/opai-api/src/main/java/com/bw/opai/exception/GlobalExceptionHandler.java @@ -0,0 +1,46 @@ +package com.bw.opai.exception; + +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import com.bw.opai.common.Res; + +import lombok.extern.slf4j.Slf4j; + +/** + * 全局异常处理 + * @author jian.mao + * @date 2026年1月28日 + * @description + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + /** + * 处理自定义业务异常 (比如拦截器抛出的 401) + */ + @ExceptionHandler(BizException.class) + public Res handleBizException(BizException e) { + log.error("业务异常: {}", e.getMsg()); + return Res.fail(e.getMsg()); + } + + /** + * 处理未知的运行时异常 + */ + @ExceptionHandler(RuntimeException.class) + public Res handleRuntimeException(RuntimeException e) { + log.error("运行时异常: ", e); + return Res.fail(e.getMessage()); + } + + /** + * 处理所有其他未明确捕获的异常 + */ + @ExceptionHandler(Exception.class) + public Res handleException(Exception e) { + log.error("系统未知异常: ", e); + return Res.fail("后端接口调用失败"); + } +} \ No newline at end of file diff --git a/opai-api/src/main/resources/bootstrap.yml b/opai-api/src/main/resources/bootstrap.yml index 9df65ef..a1b079f 100644 --- a/opai-api/src/main/resources/bootstrap.yml +++ b/opai-api/src/main/resources/bootstrap.yml @@ -48,5 +48,5 @@ logging: root: info com.alibaba.nacos.client.config.impl: WARN file: - path: ../logs + path: ./logs \ No newline at end of file