diff --git a/de.md b/de.md new file mode 100644 index 0000000..dc0cbfd --- /dev/null +++ b/de.md @@ -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中体现其多系统能力。 + +--- +如需扩展更多业务场景或权限模型,可在此架构基础上灵活调整。 \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf index d85cab9..9217ac0 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -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; diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java index e78ed6d..2b98558 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java @@ -60,6 +60,7 @@ import org.springframework.context.annotation.Bean; public class SecurityConfig { + @Bean public FilterRegistrationBean forcePromptLoginFilterRegistration(ForcePromptLoginFilter filter) { FilterRegistrationBean 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 diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/LoginController.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/LoginController.java index 96d23f2..4ef1146 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/LoginController.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/LoginController.java @@ -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 { diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/db/DbService.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/db/DbService.java index 3cdfcde..3a139e4 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/db/DbService.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/db/DbService.java @@ -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 普通用户 } diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/filter/ForcePromptLoginFilter.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/filter/ForcePromptLoginFilter.java index 4955a0a..df85493 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/filter/ForcePromptLoginFilter.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/filter/ForcePromptLoginFilter.java @@ -11,6 +11,9 @@ import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +/** + * 其实没用到 + */ @Component public class ForcePromptLoginFilter implements Filter { diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/provider/TenantAwareAuthenticationProvider.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/provider/TenantAwareAuthenticationProvider.java index c2f9163..7a3d66d 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/provider/TenantAwareAuthenticationProvider.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/provider/TenantAwareAuthenticationProvider.java @@ -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 details = new HashMap<>(); details.put("client_id", clientId); diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/service/CustomUserDetailsService.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/service/CustomUserDetailsService.java index f64764a..70e2811 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/service/CustomUserDetailsService.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/service/CustomUserDetailsService.java @@ -42,11 +42,14 @@ public class CustomUserDetailsService implements UserDetailsService { return null; } - DbService.UserInfo userInfo = dbService.getUser(clientId,tenantCode,username); + /** + * 这个地方,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) diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/token/CustomTokenCustomizer.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/token/CustomTokenCustomizer.java index 4b04762..48ffdf9 100644 --- a/oidc/src/main/java/com/tuoheng/oauth/oidc/token/CustomTokenCustomizer.java +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/token/CustomTokenCustomizer.java @@ -17,11 +17,12 @@ public class CustomTokenCustomizer implements OAuth2TokenCustomizer