This commit is contained in:
parent
9d790b6999
commit
d6c1f74acf
|
|
@ -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中体现其多系统能力。
|
||||||
|
|
||||||
|
---
|
||||||
|
如需扩展更多业务场景或权限模型,可在此架构基础上灵活调整。
|
||||||
|
|
@ -13,6 +13,13 @@ import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
@Component
|
@Component
|
||||||
public class JwtPermissionFilter implements GlobalFilter, Ordered {
|
public class JwtPermissionFilter implements GlobalFilter, Ordered {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这边能获取到Token里面的值
|
||||||
|
* @param exchange
|
||||||
|
* @param chain
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
|
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ http {
|
||||||
}
|
}
|
||||||
|
|
||||||
# /api 路径转发到网关
|
# /api 路径转发到网关
|
||||||
location /api/ {
|
location /api {
|
||||||
rewrite ^/api/(.*)$ /b/$1 break;
|
rewrite ^/api/(.*)$ /b/$1 break;
|
||||||
proxy_pass http://localhost:8080;
|
proxy_pass http://localhost:8080;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ import org.springframework.context.annotation.Bean;
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public FilterRegistrationBean<ForcePromptLoginFilter> forcePromptLoginFilterRegistration(ForcePromptLoginFilter filter) {
|
public FilterRegistrationBean<ForcePromptLoginFilter> forcePromptLoginFilterRegistration(ForcePromptLoginFilter filter) {
|
||||||
FilterRegistrationBean<ForcePromptLoginFilter> registration = new FilterRegistrationBean<>();
|
FilterRegistrationBean<ForcePromptLoginFilter> registration = new FilterRegistrationBean<>();
|
||||||
|
|
@ -68,6 +69,7 @@ public class SecurityConfig {
|
||||||
registration.setOrder(-101); // 顺序要比Spring Security的Filter更靠前
|
registration.setOrder(-101); // 顺序要比Spring Security的Filter更靠前
|
||||||
return registration;
|
return registration;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@Lazy
|
@Lazy
|
||||||
AuthenticationProvider tenantAwareAuthenticationProvider;
|
AuthenticationProvider tenantAwareAuthenticationProvider;
|
||||||
|
|
@ -77,8 +79,6 @@ public class SecurityConfig {
|
||||||
super(loginFormUrl);
|
super(loginFormUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException exception) {
|
protected String determineUrlToUseForThisRequest(HttpServletRequest request, HttpServletResponse response, org.springframework.security.core.AuthenticationException exception) {
|
||||||
String loginUrl = super.determineUrlToUseForThisRequest(request, response, exception);
|
String loginUrl = super.determineUrlToUseForThisRequest(request, response, exception);
|
||||||
|
|
@ -163,6 +163,7 @@ public class SecurityConfig {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 注册客户端(内存存储)
|
// 注册客户端(内存存储)
|
||||||
|
// 改造点:这个地方要改为读数据库
|
||||||
@Bean
|
@Bean
|
||||||
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
|
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
|
||||||
// a.sun.com 前端应用的 client
|
// a.sun.com 前端应用的 client
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import java.nio.file.Paths;
|
||||||
@RestController
|
@RestController
|
||||||
public class LoginController {
|
public class LoginController {
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
public String login(HttpServletRequest request, @RequestParam(value = "client_id", required = false) String clientId) throws IOException {
|
public String login(HttpServletRequest request, @RequestParam(value = "client_id", required = false) String clientId) throws IOException {
|
||||||
|
|
|
||||||
|
|
@ -27,18 +27,28 @@ public class DbService {
|
||||||
clients.add(new Client(2L, "b-client","b-secret","https://b.local.com/callback"));
|
clients.add(new Client(2L, "b-client","b-secret","https://b.local.com/callback"));
|
||||||
|
|
||||||
// 租户-客户端关系
|
// 租户-客户端关系
|
||||||
// 租户t1 可以登录 a b
|
// 租户t1 可以登录 a
|
||||||
// 租户t2 可以登录 a
|
// 租户t2 可以登录 b
|
||||||
|
// 租户t3 可以登录 a b
|
||||||
tenantClients.add(new TenantClient(1L, 1L,1L));
|
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(1L,1L,"u1","u1",true,false));
|
||||||
users.add(new User(2L,1L,"u2","u2",false,true));
|
users.add(new User(2L,1L,"u2","u2",false,true));
|
||||||
users.add(new User(3L,2L,"u3","u3",false,false));
|
users.add(new User(3L,1L,"u3","u3",false,false));
|
||||||
users.add(new User(4L,2L,"u2","u2",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
|
// t1 u2 可以登录 a
|
||||||
|
|
@ -47,11 +57,18 @@ public class DbService {
|
||||||
userClientAuthorities.add(new UserClientAuthorities(2L,3L,1L));
|
userClientAuthorities.add(new UserClientAuthorities(2L,3L,1L));
|
||||||
userClientAuthorities.add(new UserClientAuthorities(3L,4L,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 u1 可以登录 a b 租户用户
|
||||||
// t1 u2 可以登录 a
|
// t1 u2 可以登录 a 普通用户
|
||||||
// t2 u3 可以登录 a
|
// t2 u3 可以登录 a 普通用户
|
||||||
// t2 u2 可以登录 a
|
// t2 u2 可以登录 a 普通用户
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ import java.io.IOException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 其实没用到
|
||||||
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class ForcePromptLoginFilter implements Filter {
|
public class ForcePromptLoginFilter implements Filter {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,14 +47,18 @@ public class TenantAwareAuthenticationProvider implements AuthenticationProvider
|
||||||
System.out.println("租户代码: " + tenantCode);
|
System.out.println("租户代码: " + tenantCode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 这边判断用户是否有权限
|
* 这边判断用户是否有权限 改造点 (下面这四个值必须要有,方便网关做权限管理)
|
||||||
|
* client_id
|
||||||
|
* tenant_code 租户code
|
||||||
|
* clientIds 用户可以访问的回掉地址
|
||||||
|
* isLongToken 是否是长期TOKEN
|
||||||
*/
|
*/
|
||||||
UserDetailsInfo userDetails = userDetailsService.loadUserByUsername(username,clientId,tenantCode);
|
UserDetailsInfo userDetails = userDetailsService.loadUserByUsername(username,clientId,tenantCode);
|
||||||
|
|
||||||
if (userDetails != null && passwordEncoder.matches(password, userDetails.getUserDetails().getPassword())) {
|
if (userDetails != null && passwordEncoder.matches(password, userDetails.getUserDetails().getPassword())) {
|
||||||
System.out.println("用户认证成功");
|
System.out.println("用户认证成功");
|
||||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails, password, userDetails.getUserDetails().getAuthorities());
|
userDetails.getUserDetails(), password, userDetails.getUserDetails().getAuthorities());
|
||||||
|
|
||||||
Map<String, Object> details = new HashMap<>();
|
Map<String, Object> details = new HashMap<>();
|
||||||
details.put("client_id", clientId);
|
details.put("client_id", clientId);
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,14 @@ public class CustomUserDetailsService implements UserDetailsService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 这个地方,UserInfo对象的定义不能变
|
||||||
|
* UserDetailsInfo 的 authorities 这个字段被用来放 validClient了,也就是用户可以访问哪些系统
|
||||||
|
*/
|
||||||
DbService.UserInfo userInfo = dbService.getUser(clientId,tenantCode,username);
|
DbService.UserInfo userInfo = dbService.getUser(clientId,tenantCode,username);
|
||||||
|
|
||||||
if(Objects.nonNull(userInfo)) {
|
if(Objects.nonNull(userInfo)) {
|
||||||
String[] authorities = userInfo.validClient.toArray(new String[0]);
|
String[] authorities = userInfo.validClient.toArray(new String[0]);
|
||||||
|
|
||||||
UserDetailsInfo userDetailsInfo = new UserDetailsInfo();
|
UserDetailsInfo userDetailsInfo = new UserDetailsInfo();
|
||||||
userDetailsInfo.userDetails = org.springframework.security.core.userdetails.User.builder()
|
userDetailsInfo.userDetails = org.springframework.security.core.userdetails.User.builder()
|
||||||
.username(userInfo.userName)
|
.username(userInfo.userName)
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,12 @@ public class CustomTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingC
|
||||||
@Override
|
@Override
|
||||||
public void customize(JwtEncodingContext context) {
|
public void customize(JwtEncodingContext context) {
|
||||||
// 只对access_token生效
|
// 只对access_token生效
|
||||||
|
//2
|
||||||
if ("access_token".equals(context.getTokenType().getValue())) {
|
if ("access_token".equals(context.getTokenType().getValue())) {
|
||||||
String username = context.getPrincipal().getName();
|
String username = context.getPrincipal().getName();
|
||||||
context.getClaims().claim("username", username);
|
context.getClaims().claim("username", username);
|
||||||
|
|
||||||
// 取details
|
//取 Details
|
||||||
Object detailsObj = context.getPrincipal().getDetails();
|
Object detailsObj = context.getPrincipal().getDetails();
|
||||||
if (detailsObj instanceof Map details) {
|
if (detailsObj instanceof Map details) {
|
||||||
Object clientId = details.get("client_id");
|
Object clientId = details.get("client_id");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue