This commit is contained in:
孙小云 2025-07-25 09:31:15 +08:00
parent 9d790b6999
commit 364d92971d
9 changed files with 123 additions and 19 deletions

74
de.md Normal file
View File

@ -0,0 +1,74 @@
# 系统交互图与设计说明
## 1. 交互时序图
```mermaid
sequenceDiagram
participant 用户
participant 前端A
participant 前端B
participant 网关
participant OIDC
participant 资源服务
用户->>前端A: 访问 a.local.com
前端A->>用户: 检查本地token
alt token无效或无token
前端A->>OIDC: 跳转/oauth2/authorize (client_id=a-client)
OIDC->>用户: 登录页/授权页
用户->>OIDC: 输入账号、密码、租户
OIDC->>前端A: 跳转回/callback?code=xxx
前端A->>OIDC: 用code换token
OIDC->>前端A: 返回access_token (含租户、clientId、用户名等)
end
前端A->>网关: 携带token访问API
网关->>OIDC: 校验token
网关->>资源服务: 转发请求
资源服务->>网关: 返回数据
网关->>前端A: 返回数据
用户->>前端B: 访问 b.local.com
前端B->>用户: 检查本地token
alt token无效或无token
前端B->>OIDC: 跳转/oauth2/authorize (client_id=b-client)
OIDC->>用户: 登录页/授权页
用户->>OIDC: 输入账号、密码、租户
OIDC->>前端B: 跳转回/callback?code=xxx
前端B->>OIDC: 用code换token
OIDC->>前端B: 返回access_token (含租户、clientId、用户名等)
end
前端B->>网关: 携带token访问API
网关->>OIDC: 校验token
网关->>资源服务: 转发请求
资源服务->>网关: 返回数据
网关->>前端B: 返回数据
```
## 2. 设计说明
### 多租户与多系统
- 支持多租户如tenant-a、tenant-b每个租户可绑定多个client如a-client、b-client
- 用户登录时需输入租户信息系统根据租户和client校验用户权限。
### OIDC认证与Token定制
- OIDC服务端支持标准OAuth2/OIDC认证流程。
- 登录成功后生成的JWT token中包含用户名、clientId、租户代码等自定义字段。
- 支持根据用户类型动态定制token有效期。
### 网关权限控制
- 网关自动校验JWT token的合法性。
- 网关通过解析token中的租户、clientId、用户类型等字段实现细粒度的权限控制。
- 可根据访问的host如a.local.com、b.local.com和token内容做多系统隔离和授权。
### 前后端交互
- 前端负责本地token管理token失效或无token时自动跳转OIDC认证。
- 前端携带token访问网关API网关校验并转发到后端资源服务。
- 支持SSO和强制登录通过prompt=login参数或后端策略控制
### 典型场景
- 用户A登录a.local.com获取a-client的token只能访问A系统资源。
- 用户B登录b.local.com获取b-client的token只能访问B系统资源。
- 超级管理员可配置为多租户多client权限token中体现其多系统能力。
---
如需扩展更多业务场景或权限模型,可在此架构基础上灵活调整。

View File

@ -90,7 +90,7 @@ http {
}
# /api 路径转发到网关
location /api/ {
location /api {
rewrite ^/api/(.*)$ /b/$1 break;
proxy_pass http://localhost:8080;
proxy_set_header Host $host;

View File

@ -60,6 +60,7 @@ import org.springframework.context.annotation.Bean;
public class SecurityConfig {
@Bean
public FilterRegistrationBean<ForcePromptLoginFilter> forcePromptLoginFilterRegistration(ForcePromptLoginFilter filter) {
FilterRegistrationBean<ForcePromptLoginFilter> registration = new FilterRegistrationBean<>();
@ -68,6 +69,7 @@ public class SecurityConfig {
registration.setOrder(-101); // 顺序要比Spring Security的Filter更靠前
return registration;
}
@Autowired
@Lazy
AuthenticationProvider tenantAwareAuthenticationProvider;
@ -77,8 +79,6 @@ public class SecurityConfig {
super(loginFormUrl);
}
@Override
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException exception) {
String loginUrl = super.determineUrlToUseForThisRequest(request, response, exception);
@ -163,6 +163,7 @@ public class SecurityConfig {
// }
// 注册客户端内存存储
// 改造点:这个地方要改为读数据库
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
// a.sun.com 前端应用的 client

View File

@ -14,6 +14,7 @@ import java.nio.file.Paths;
@RestController
public class LoginController {
@GetMapping("/login")
@ResponseBody
public String login(HttpServletRequest request, @RequestParam(value = "client_id", required = false) String clientId) throws IOException {

View File

@ -27,18 +27,28 @@ public class DbService {
clients.add(new Client(2L, "b-client","b-secret","https://b.local.com/callback"));
// 租户-客户端关系
// 租户t1 可以登录 a b
// 租户t2 可以登录 a
// 租户t1 可以登录 a
// 租户t2 可以登录 b
// 租户t3 可以登录 a b
tenantClients.add(new TenantClient(1L, 1L,1L));
tenantClients.add(new TenantClient(2L, 1L,2L));
tenantClients.add(new TenantClient(3L, 2L,1L));
tenantClients.add(new TenantClient(2L, 2L,1L));
tenantClients.add(new TenantClient(3L, 2L,2L));
// 为租户添加用户
// t1 u1 可登录 a b
// t1 u1 可登录 a
// t2 u2 可登录 b
// t3 u3 可登录 a b
users.add(new User(1L,1L,"u1","u1",true,false));
users.add(new User(2L,1L,"u2","u2",false,true));
users.add(new User(3L,2L,"u3","u3",false,false));
users.add(new User(4L,2L,"u2","u2",false,false));
users.add(new User(3L,1L,"u3","u3",false,false));
users.add(new User(4L,1L,"u4","u4",false,false));
users.add(new User(5L,2L,"u1","u1",false,false));
users.add(new User(6L,2L,"u2","u2",false,true));
users.add(new User(7L,2L,"u3","u3",true,false));
users.add(new User(8L,2L,"u4","u4",false,false));
// 添加系统的普通用户
// t1 u2 可以登录 a
@ -47,11 +57,18 @@ public class DbService {
userClientAuthorities.add(new UserClientAuthorities(2L,3L,1L));
userClientAuthorities.add(new UserClientAuthorities(3L,4L,1L));
userClientAuthorities.add(new UserClientAuthorities(4L,5L,2L));
userClientAuthorities.add(new UserClientAuthorities(5L,6L,1L));
userClientAuthorities.add(new UserClientAuthorities(6L,8L,2L));
userClientAuthorities.add(new UserClientAuthorities(7L,8L,1L));
// 总结
// t1 u1 可以登录 a b
// t1 u2 可以登录 a
// t2 u3 可以登录 a
// t2 u2 可以登录 a
// t1 u1 可以登录 a b 租户用户
// t1 u2 可以登录 a 普通用户
// t2 u3 可以登录 a 普通用户
// t2 u2 可以登录 a 普通用户
}

View File

@ -11,6 +11,9 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* 其实没用到
*/
@Component
public class ForcePromptLoginFilter implements Filter {

View File

@ -47,14 +47,18 @@ public class TenantAwareAuthenticationProvider implements AuthenticationProvider
System.out.println("租户代码: " + tenantCode);
/**
* 这边判断用户是否有权限
* 这边判断用户是否有权限 改造点 下面这四个值必须要有,方便网关做权限管理
* client_id
* tenant_code 租户code
* clientIds 用户可以访问的回掉地址
* isLongToken 是否是长期TOKEN
*/
UserDetailsInfo userDetails = userDetailsService.loadUserByUsername(username,clientId,tenantCode);
if (userDetails != null && passwordEncoder.matches(password, userDetails.getUserDetails().getPassword())) {
System.out.println("用户认证成功");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
userDetails, password, userDetails.getUserDetails().getAuthorities());
userDetails.getUserDetails(), password, userDetails.getUserDetails().getAuthorities());
Map<String, Object> details = new HashMap<>();
details.put("client_id", clientId);

View File

@ -42,11 +42,14 @@ public class CustomUserDetailsService implements UserDetailsService {
return null;
}
/**
* 这个地方,UserInfo对象的定义不能变
* UserDetailsInfo authorities 这个字段被用来放 validClient了,也就是用户可以访问哪些系统
*/
DbService.UserInfo userInfo = dbService.getUser(clientId,tenantCode,username);
if(Objects.nonNull(userInfo)) {
String[] authorities = userInfo.validClient.toArray(new String[0]);
UserDetailsInfo userDetailsInfo = new UserDetailsInfo();
userDetailsInfo.userDetails = org.springframework.security.core.userdetails.User.builder()
.username(userInfo.userName)

View File

@ -17,11 +17,12 @@ public class CustomTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingC
@Override
public void customize(JwtEncodingContext context) {
// 只对access_token生效
//2
if ("access_token".equals(context.getTokenType().getValue())) {
String username = context.getPrincipal().getName();
context.getClaims().claim("username", username);
// 取details
// Details
Object detailsObj = context.getPrincipal().getDetails();
if (detailsObj instanceof Map details) {
Object clientId = details.get("client_id");