This commit is contained in:
parent
9d790b6999
commit
364d92971d
|
|
@ -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中体现其多系统能力。
|
||||
|
||||
---
|
||||
如需扩展更多业务场景或权限模型,可在此架构基础上灵活调整。
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 普通用户
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import java.io.IOException;
|
|||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 其实没用到
|
||||
*/
|
||||
@Component
|
||||
public class ForcePromptLoginFilter implements Filter {
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue