16 changed files with 462 additions and 21 deletions
-
51opai-api/src/main/java/com/bw/opai/auth/controller/OpaiAuthController.java
-
14opai-api/src/main/java/com/bw/opai/auth/dto/LoginRequest.java
-
18opai-api/src/main/java/com/bw/opai/auth/dto/UserDTO.java
-
21opai-api/src/main/java/com/bw/opai/auth/entity/SysUser.java
-
51opai-api/src/main/java/com/bw/opai/auth/interceptor/AuthInterceptor.java
-
21opai-api/src/main/java/com/bw/opai/auth/service/IRemoteAuthService.java
-
92opai-api/src/main/java/com/bw/opai/auth/service/impl/RemoteAuthServiceImpl.java
-
40opai-api/src/main/java/com/bw/opai/auth/utils/UserContext.java
-
8opai-api/src/main/java/com/bw/opai/common/Res.java
-
3opai-api/src/main/java/com/bw/opai/common/ResponseCode.java
-
14opai-api/src/main/java/com/bw/opai/config/CorsConfig.java
-
19opai-api/src/main/java/com/bw/opai/config/RestConfig.java
-
26opai-api/src/main/java/com/bw/opai/config/WebMvcConfig.java
-
25opai-api/src/main/java/com/bw/opai/exception/BizException.java
-
46opai-api/src/main/java/com/bw/opai/exception/GlobalExceptionHandler.java
-
2opai-api/src/main/resources/bootstrap.yml
@ -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<String> 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<String> register(@RequestBody SysUser user) { |
|||
return remoteAuthService.remoteRegister(user); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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(); |
|||
} |
|||
} |
|||
@ -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<String> remoteLogin(String username, String password); |
|||
/*** 注册中转 ***/ |
|||
Res<String> remoteRegister(SysUser user); |
|||
/*** 核心鉴权:拿着 token 去 Auth-Server 换 UserDTO ***/ |
|||
UserDTO verifyToken(String token); |
|||
} |
|||
@ -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<String> 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<String> 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; |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
package com.bw.opai.auth.utils; |
|||
|
|||
import com.bw.opai.auth.dto.UserDTO; |
|||
|
|||
/** |
|||
* 严谨程序员必备:用户上下文容器 |
|||
* 作用:在当前线程中存储已登录的用户信息 |
|||
*/ |
|||
public class UserContext { |
|||
private static final ThreadLocal<UserDTO> 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(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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/**"); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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("后端接口调用失败"); |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue