diff --git a/README.md b/README.md new file mode 100644 index 0000000..ee52dba --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +# OAuth2/OIDC 统一认证系统 + +这是一个基于Spring Boot的多服务架构,使用OAuth2和OIDC实现统一认证。 + +## 系统架构 + +``` +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ 前端应用 │ │ 网关服务 │ │ 资源服务 │ +│ (a.sun.com) │───▶│ (8080) │───▶│ (8081) │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ +│ OIDC服务器 │ │ JWT验证 │ │ 受保护资源 │ +│ (9000) │ │ Token转发 │ │ API端点 │ +└─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +## 服务组件 + +### 1. OIDC授权服务器 (oidc/) +- **端口**: 9000 +- **功能**: OAuth2/OIDC授权服务器 +- **客户端配置**: + - 客户端ID: `a-client` + - 客户端密钥: `a-secret` + - 重定向URI: `http://a.sun.com/callback` + - 授权类型: `authorization_code`, `refresh_token` + - 作用域: `openid`, `read` + +### 2. 网关服务 (gateway/) +- **端口**: 8080 +- **功能**: API网关,JWT验证,Token转发 +- **特性**: + - JWT Token验证 + - 路由转发到后端服务 + - Token Relay功能 + +### 3. 资源服务 (resourceservice/) +- **端口**: 8081 +- **功能**: 后端API服务 +- **端点**: `/api/resource` (GET) + +### 4. 前端应用 (resourceservicehtml/) +- **域名**: a.sun.com +- **功能**: 用户界面,OIDC登录流程 +- **页面**: + - `index.html`: 主页面,包含登录和API调用功能 + - `callback.html`: OIDC回调处理页面 + - `test.html`: 配置测试页面 + +## 认证流程 + +### 1. 用户登录流程 +1. 用户访问 `http://a.sun.com` +2. 前端检查本地Token +3. 如果没有Token,重定向到OIDC登录页面 +4. 用户在OIDC服务器登录 +5. OIDC服务器重定向回 `http://a.sun.com/callback` +6. 前端用授权码交换Token +7. Token保存到本地存储 + +### 2. API调用流程 +1. 前端使用Token调用 `/api/resource` +2. 请求经过nginx代理到网关 +3. 网关验证JWT Token +4. 网关转发请求到资源服务 +5. 资源服务返回数据 + +## 配置说明 + +### OIDC服务器配置 +- 使用内存存储用户和客户端 +- 默认用户: `user/password` +- RSA密钥对用于JWT签名 + +### 网关配置 +- WebFlux架构 +- JWT Token验证 +- 路由配置指向资源服务 + +### 前端配置 +- OIDC客户端配置 +- Token本地存储 +- 状态参数防CSRF攻击 + +## 启动顺序 + +1. **启动OIDC服务器**: + ```bash + cd oidc + mvn spring-boot:run + ``` + +2. **启动网关服务**: + ```bash + cd gateway + mvn spring-boot:run + ``` + +3. **启动资源服务**: + ```bash + cd resourceservice + mvn spring-boot:run + ``` + +4. **配置nginx**: + ```bash + sudo cp resourceservicehtml/nginx.conf /opt/homebrew/etc/nginx/nginx.conf + sudo nginx -s reload + ``` + +5. **配置hosts**: + ``` + 127.0.0.1 a.sun.com + 127.0.0.1 oidc.sun.com + ``` + +## 测试 + +### 1. 访问前端 +- 主页: `http://a.sun.com` +- 测试页面: `http://a.sun.com/test.html` + +### 2. 测试OIDC端点 +- JWKS: `http://localhost:9000/oauth2/jwks` +- 授权端点: `http://localhost:9000/oauth2/authorize` +- Token端点: `http://localhost:9000/oauth2/token` + +### 3. 测试API +- 资源API: `http://a.sun.com/api/resource` + +## 安全特性 + +- JWT Token验证 +- 状态参数防CSRF攻击 +- HTTPS重定向支持 +- Token过期处理 +- 刷新Token机制 + +## 故障排除 + +### 常见问题 + +1. **OIDC发现端点无法访问** + - 检查OIDC服务器是否启动 + - 验证安全配置 + +2. **Token验证失败** + - 检查Token是否过期 + - 验证JWT签名 + +3. **nginx权限问题** + - 确保nginx用户有读取权限 + - 检查目录权限设置 + +4. **CORS问题** + - 检查前端域名配置 + - 验证重定向URI设置 + +## 开发说明 + +### 添加新客户端 +在 `oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java` 中添加新的 `RegisteredClient`。 + +### 添加新API端点 +在 `resourceservice` 中添加新的Controller和端点。 + +### 修改前端配置 +在 `resourceservicehtml/index.html` 中修改 `oidcConfig` 对象。 + +## 技术栈 + +- **后端**: Spring Boot 3.x, Spring Security, Spring Cloud Gateway +- **前端**: HTML5, JavaScript, CSS3 +- **认证**: OAuth2, OpenID Connect, JWT +- **代理**: nginx +- **构建**: Maven + +## 版本信息 + +- Spring Boot: 3.5.3 +- Spring Security: 6.5.1 +- Spring Cloud Gateway: 4.1.1 +- Java: 17 + + +mkcert -uninstall && mkcert -install && mkcert -key-file ssl/private.key -cert-file ssl/certificate.crt "*.local.com" local.com \ No newline at end of file diff --git a/gateway/.idea/workspace.xml b/gateway/.idea/workspace.xml index 27d6df4..0c4a32d 100644 --- a/gateway/.idea/workspace.xml +++ b/gateway/.idea/workspace.xml @@ -5,7 +5,17 @@ + + + + + + + + + + diff --git a/gateway/target/classes/com/tuoheng/gateway/GatewayApplication.class b/gateway/target/classes/com/tuoheng/gateway/GatewayApplication.class index 0e4fc1f..7bac476 100644 Binary files a/gateway/target/classes/com/tuoheng/gateway/GatewayApplication.class and b/gateway/target/classes/com/tuoheng/gateway/GatewayApplication.class differ diff --git a/gateway/target/classes/com/tuoheng/gateway/config/SecurityConfig.class b/gateway/target/classes/com/tuoheng/gateway/config/SecurityConfig.class index e26ff99..6a0a8e2 100644 Binary files a/gateway/target/classes/com/tuoheng/gateway/config/SecurityConfig.class and b/gateway/target/classes/com/tuoheng/gateway/config/SecurityConfig.class differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..6852f7a --- /dev/null +++ b/index.html @@ -0,0 +1,27 @@ + + + + + OAuth2 登录 + + + +
+

OAuth2 登录

+
+ + + +
+
+
+ + \ No newline at end of file diff --git a/install-key.pem b/install-key.pem new file mode 100644 index 0000000..d83c48b --- /dev/null +++ b/install-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/FXUfK+ry8hUJ +H3mYhlDukXW05kLTRjyMd8G7s8Qt3M9B1e92rxHNKcU45+lSDTF8bb1JF6DzCXmo +Clon/6H8YvHzD3RyypmqCRi17qQwch5Jtprtb8lCJVcBClDUFjoeTtPXpJMXdH5P +xjLR6Aa3Z4HpBpIKMsvp9ZaAG0XQDTUo8urcIeV16iRsRfqLRufwQ4gVlVxuOJVb +mHmKyBJZja+iBVlsiVBggMQJq2a2HQE4Lk2lv+7uWqbJ2MrL8d8aOkJIt+YtqlxV +mv2vLoLw2hAd4emnF2ijFEYJtNzepJ5X0nltzAHQd+uW44sTjWlMOopqKpqD1+R9 +ZNsMsdupAgMBAAECggEAGVETenTMJTITvWixKJcrI+Cb0sLrOajFnurC/UZ9CIKH +5zYcCwJ4/lC5c6euTxO2acD0YjnCNlEcEDqG5WPGJ3VIjyaODCNxpoicAIbEtDJ6 +dtO9xRWZea0O0PF38hGb06YoBRsl7eaeUZ114D+4nBYXrTMUqEtAnxfNv91dK4pJ +byV1Wp+bHgXzIH4kM3dPjOsDwAUIgtmUFrSXdsG+OzlTZxUiCShAWFXe9W5KiLR0 +fZJkCu2qhLg7QQvgrGmp4PRov97dJqbh0XXCyDLHmVbjqrkCD8AcdBqtRI45B2TA +STbNcq6nlXf2VOsVjHnDoIEDwpDDeCDesDfnSVPzgQKBgQDE75cWIjH7/DXBJ0rN +x81v2MwEZw3zLdq5GNENPitbHRgPZ8jvEsABg5hJButucjyxOPopdYQg+c6QhkMG +qumig3k9aOVonVOrF3+iXGoMWjp3yZypS15hJzx1M7WneY1ctE3RQ2tIwOk8xKPB +BXSOsbAlVIlUfRqHO9h0m2JTeQKBgQD4ZI3oXAkKAYfGlZRY+FJ+As/p9PhQovv6 +nq/F1BwF9NvLjihzt71pYVbEcZkkUA2QHxQEMKerVLpRIhaschmnCIyqh8w1wfyj +QfO5fi3e3amtkXyRU1CnXPh4ywtWmpvZtmoDqol13Zjjzjose2GGt4hpZsBggVHQ +6jPgrAENsQKBgHMYi7aV2ZyptEjky+UkZr59eA8Co7aCEBipllQlB3XCtTMbtuVy +keDQpgnYD3SHM01oPVxJoCUdmkoBDd8xuEYQjKUFTz4q5KFTpHahiCEcApvLqtGO +iORC6CSfSgVNFv8dKXWp72OfyzCGxCWlKI/U7VuD4pcMXpq2sTTFM1wRAoGBANZW +rr2a7ZHc0DTkTiaX4VcrRg40fTHX8mfJFxQ2fBgHusJj4TQ5kRCmFiFdhTB4g7uh +lbwn4AdQDZaFO9uCefBQyFE+7VBWHJMkDhQ6dYqi7BACQuOEaUyCRUa2rwoEUAgG +CGUxe3xhw9SP2FMaBIYjSWrqZ4bfEKKd9jYhNqeBAoGBAK9adFsDFQtT2O4u25hx +Yv7m9ZG5NbvhuQuEK7skdDb+HZHK/1JIV8WG16zTe897I7IC+OYezKLA1fbBnna0 +rARwY/NZL1/CA5SmPGgK5Vy9IJbyJ50tQqFUGaF0SF/5jAuzCLa82d3XWUOlUgWH +zkITyIa3HhT//7eFdH3sa+Hc +-----END PRIVATE KEY----- diff --git a/install.pem b/install.pem new file mode 100644 index 0000000..3879475 --- /dev/null +++ b/install.pem @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEWTCCAsGgAwIBAgIQBKpJXSm53rsT3EorgGHTXjANBgkqhkiG9w0BAQsFADCB +kzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMTQwMgYDVQQLDCtzdW5w +ZW5nQHN1bnBlbmdkZU1hY0Jvb2stUHJvLmxvY2FsICjlrZnpuY8pMTswOQYDVQQD +DDJta2NlcnQgc3VucGVuZ0BzdW5wZW5nZGVNYWNCb29rLVByby5sb2NhbCAo5a2Z +6bmPKTAeFw0yNTA3MTgwNzU0NTRaFw0yNzEwMTgwNzU0NTRaMF8xJzAlBgNVBAoT +Hm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTE0MDIGA1UECwwrc3VucGVu +Z0BzdW5wZW5nZGVNYWNCb29rLVByby5sb2NhbCAo5a2Z6bmPKTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL8VdR8r6vLyFQkfeZiGUO6RdbTmQtNGPIx3 +wbuzxC3cz0HV73avEc0pxTjn6VINMXxtvUkXoPMJeagKWif/ofxi8fMPdHLKmaoJ +GLXupDByHkm2mu1vyUIlVwEKUNQWOh5O09ekkxd0fk/GMtHoBrdngekGkgoyy+n1 +loAbRdANNSjy6twh5XXqJGxF+otG5/BDiBWVXG44lVuYeYrIElmNr6IFWWyJUGCA +xAmrZrYdATguTaW/7u5apsnYysvx3xo6Qki35i2qXFWa/a8ugvDaEB3h6acXaKMU +Rgm03N6knlfSeW3MAdB365bjixONaUw6imoqmoPX5H1k2wyx26kCAwEAAaNcMFow +DgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB8GA1UdIwQYMBaA +FF75UQ00SANrn+spMSH1elRO+PN9MBIGA1UdEQQLMAmCB2luc3RhbGwwDQYJKoZI +hvcNAQELBQADggGBAF7D7B5a2ZZ4vSgqGx8e0p08dVfUHM3v85pHi7VO7xVaHKWh +rS09S5sTVvDiEOujHXlnrGYBQjOZ3/DwBJyNQQG83EWZ1/TWyYIrmBUUBzpaAJ+t +gLap5KaQoV+xo46PfXCG5bQmqllcsZKWvY5c8chECHXWJMX7cF8/9XBI7gywIoRN +PRVKoXjh5SAoOzI8VjnQ3gEXywnuBfaSu/76oNT9ty/Y94G6RhYLq+s0Hpj5FAi2 +KbznwKAk14PfUDuZenPKac1NFNdlRnjaBoIQBtikSQjM9TaBMD1xvMhmxO779/0P +kBrtzJcYez/SOrTFGUWMaM9CQSqBrINOJah56QB0eP1o1NvnzV7L/2c0kDQVJMMM +oy+1Qfx9OhYvAv/xmORWNlGw+oF4bh71qqlWcQ3mqrYBsYIfxt/1MsYawtg3waer +h/CO5vPOlLplqW8H7hrU8e4zq9QRMSLlMNycYQs+S8JTlL42lT6a4dXDXBwx4NlP +QeQTR9ak1+yacEzE2w== +-----END CERTIFICATE----- diff --git a/nginx/nginx.conf b/nginx/nginx.conf index fd03845..1c4d050 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -26,6 +26,12 @@ http { '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; + # 在http块添加main_cookie日志格式 + log_format main_cookie '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for" ' + 'cookie:"$http_cookie"'; + # 访问日志 access_log /tmp/nginx_access.log main; @@ -42,10 +48,10 @@ http { gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; - # 前端应用服务器 (a.com) - HTTPS + # 前端应用服务器 (a.local.com) - HTTPS server { listen 443 ssl; - server_name a.com; + server_name a.local.com; # SSL配置 ssl_certificate /Users/sunpeng/workspace/remote/oauth2/ssl/certificate.crt; @@ -67,6 +73,11 @@ http { add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; } + # 防止前端server拦截OIDC登录页,/login直接返回404 + location = /login { + return 404; + } + # 处理OPTIONS预检请求 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; @@ -82,14 +93,14 @@ http { proxy_set_header X-Forwarded-Proto $scheme; # 处理CORS - add_header Access-Control-Allow-Origin "https://a.com" always; + add_header Access-Control-Allow-Origin "https://a.local.com" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; # 处理OPTIONS请求 if ($request_method = 'OPTIONS') { - add_header Access-Control-Allow-Origin "https://a.com" always; + add_header Access-Control-Allow-Origin "https://a.local.com" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; add_header Access-Control-Max-Age 1728000; @@ -108,14 +119,14 @@ http { proxy_set_header X-Forwarded-Proto $scheme; # 处理CORS - add_header Access-Control-Allow-Origin "https://a.com" always; + add_header Access-Control-Allow-Origin "https://a.local.com" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; # 处理OPTIONS请求 if ($request_method = 'OPTIONS') { - add_header Access-Control-Allow-Origin "https://a.com" always; + add_header Access-Control-Allow-Origin "https://a.local.com" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; add_header Access-Control-Max-Age 1728000; @@ -125,15 +136,45 @@ http { } } + # /oidc-logout 路径转发到 OIDC 服务 + location /oidc-logout { + proxy_pass http://localhost:9000/oidc-logout; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Cookie $http_cookie; + + # 记录cookie内容到专用日志 + access_log /tmp/nginx_oidc_logout.log main_cookie; + + # 处理CORS + add_header Access-Control-Allow-Origin "https://a.local.com" always; + add_header Access-Control-Allow-Credentials "true" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; + + # 处理OPTIONS请求 + if ($request_method = 'OPTIONS') { + add_header Access-Control-Allow-Origin "https://a.local.com" always; + add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; + add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; + add_header Access-Control-Max-Age 1728000; + add_header Content-Type "text/plain; charset=utf-8"; + add_header Content-Length 0; + return 204; + } + } + # 错误页面 error_page 404 /404.html; error_page 500 502 503 504 /50x.html; } - # OIDC服务器代理 (oidc.com) - HTTPS + # OIDC服务器代理 (oidc.local.com) - HTTPS server { listen 443 ssl; - server_name oidc.com; + server_name oidc.local.com; # SSL配置 ssl_certificate /Users/sunpeng/workspace/remote/oauth2/ssl/certificate.crt; @@ -144,25 +185,21 @@ http { # 代理到OIDC服务器 location / { - proxy_pass http://localhost:9000; + proxy_pass http://localhost:9000/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Ssl on; proxy_set_header Cookie $http_cookie; - - # 处理CORS - add_header Access-Control-Allow-Origin "https://a.com" always; + add_header Access-Control-Allow-Origin "https://a.local.com" always; add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; - - # 移除CSP限制 (开发环境) - add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http: https:;" always; - - # 处理OPTIONS请求 if ($request_method = 'OPTIONS') { - add_header Access-Control-Allow-Origin "https://a.com" always; + add_header Access-Control-Allow-Origin "https://a.local.com" always; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"; add_header Access-Control-Max-Age 1728000; @@ -180,7 +217,7 @@ http { # HTTP重定向到HTTPS server { listen 80; - server_name a.com oidc.com; + server_name a.local.com oidc.local.com; return 301 https://$server_name$request_uri; } 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 3e5fd6f..3b37e4a 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 @@ -80,6 +80,7 @@ public class SecurityConfig { .requestMatchers("/oauth2/jwks").permitAll() .requestMatchers("/logout").permitAll() .requestMatchers("/login").permitAll() + .requestMatchers("/oidc-logout").permitAll() .anyRequest().authenticated() ) .oauth2ResourceServer(oauth2 -> oauth2.jwt()) // 新增,支持JWT @@ -112,14 +113,14 @@ public class SecurityConfig { // 注册客户端(内存存储) @Bean public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) { - // a.com 前端应用的 client + // a.sun.com 前端应用的 client RegisteredClient aClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("a-client") .clientSecret(passwordEncoder.encode("a-secret")) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) - .redirectUri("https://a.com/callback") + .redirectUri("https://a.local.com/callback") .scope(OidcScopes.OPENID) .scope("read") .clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build()) diff --git a/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/OidcLogoutController.java b/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/OidcLogoutController.java new file mode 100644 index 0000000..9ce3f56 --- /dev/null +++ b/oidc/src/main/java/com/tuoheng/oauth/oidc/controller/OidcLogoutController.java @@ -0,0 +1,31 @@ +package com.tuoheng.oauth.oidc.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class OidcLogoutController { + + @GetMapping("/oidc-logout") + public String oidcLogout( + @RequestParam(value = "id_token_hint", required = false) String idTokenHint, + @RequestParam(value = "post_logout_redirect_uri", required = false) String redirectUri, + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication) { + + // 调用Spring Security的logout + new SecurityContextLogoutHandler().logout(request, response, authentication); + + // 重定向到指定页面 + if (redirectUri != null && !redirectUri.isEmpty()) { + return "redirect:" + redirectUri; + } + return "redirect:/login?logout"; + } +} \ No newline at end of file diff --git a/oidc/src/main/resources/application.properties b/oidc/src/main/resources/application.properties index d95aee0..2d9b20f 100644 --- a/oidc/src/main/resources/application.properties +++ b/oidc/src/main/resources/application.properties @@ -1,4 +1,6 @@ spring.application.name=oidc server.port=9000 -spring.security.oauth2.authorization-server.issuer-url: https://oidc.com -server.servlet.session.timeout=1m \ No newline at end of file +spring.security.oauth2.authorization-server.issuer-url: https://oidc.local.com +server.servlet.session.timeout=1m +server.servlet.session.cookie.domain=local.com +server.forward-headers-strategy=framework \ No newline at end of file diff --git a/resourceservice/target/classes/application.properties b/resourceservice/target/classes/application.properties deleted file mode 100644 index bafddce..0000000 --- a/resourceservice/target/classes/application.properties +++ /dev/null @@ -1 +0,0 @@ -server.port=8081 \ No newline at end of file diff --git a/resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class b/resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class deleted file mode 100644 index f5ffdcf..0000000 Binary files a/resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class and /dev/null differ diff --git a/resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class b/resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class deleted file mode 100644 index c558287..0000000 Binary files a/resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class and /dev/null differ diff --git a/resourceservicehtml/callback.html b/resourceservicehtml/callback.html index 422c1c0..8c6e5f9 100644 --- a/resourceservicehtml/callback.html +++ b/resourceservicehtml/callback.html @@ -63,9 +63,9 @@ const oidcConfig = { clientId: 'a-client', clientSecret: 'a-secret', - redirectUri: 'https://a.com/callback', - tokenEndpoint: 'https://oidc.com/oauth2/token', - userInfoEndpoint: 'https://oidc.com/userinfo' + redirectUri: 'https://a.local.com/callback', + tokenEndpoint: 'https://oidc.local.com/oauth2/token', + userInfoEndpoint: 'https://oidc.local.com/userinfo' }; // 页面加载时执行 diff --git a/resourceservicehtml/index.html b/resourceservicehtml/index.html index c25bec4..be70237 100644 --- a/resourceservicehtml/index.html +++ b/resourceservicehtml/index.html @@ -74,10 +74,10 @@ const oidcConfig = { clientId: 'a-client', clientSecret: 'a-secret', - redirectUri: 'https://a.com/callback', - authorizationEndpoint: 'https://oidc.com/oauth2/authorize', - tokenEndpoint: 'https://oidc.com/oauth2/token', - userInfoEndpoint: 'https://oidc.com/userinfo', + redirectUri: 'https://a.local.com/callback', + authorizationEndpoint: 'https://oidc.local.com/oauth2/authorize', + tokenEndpoint: 'https://oidc.local.com/oauth2/token', + userInfoEndpoint: 'https://oidc.local.com/userinfo', scope: 'openid read' }; @@ -110,7 +110,7 @@ // 验证token function validateToken(token) { - fetch('https://oidc.com/userinfo', { + fetch('https://oidc.local.com/userinfo', { headers: { 'Authorization': `Bearer ${token}` } @@ -266,13 +266,21 @@ // 退出登录 function logout() { - // 通过nginx代理调用logout // 清理本地token localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); localStorage.removeItem('oauth_state'); - // 跳转到Spring Security默认的退出页面(GET) - window.location.href = 'https://oidc.com/logout'; + // 获取id_token(如果有) + const idToken = localStorage.getItem('id_token'); // 登录时保存id_token + // 退出后跳转到首页 + const redirectUri = encodeURIComponent('https://a.local.com'); + // 拼接logout url + let logoutUrl = `https://a.local.com/oidc-logout?post_logout_redirect_uri=${redirectUri}`; + if (idToken) { + logoutUrl += `&id_token_hint=${idToken}`; + } + // 跳转到OIDC logout端点 + window.location.href = logoutUrl; } // 生成随机字符串 diff --git a/resourceservicehtml/test.html b/resourceservicehtml/test.html index ec990c6..95988f6 100644 --- a/resourceservicehtml/test.html +++ b/resourceservicehtml/test.html @@ -76,11 +76,11 @@ const oidcConfig = { clientId: 'a-client', clientSecret: 'a-secret', - redirectUri: 'https://a.com/callback', - authorizationEndpoint: 'https://oidc.com/oauth2/authorize', - tokenEndpoint: 'https://oidc.com/oauth2/token', - userInfoEndpoint: 'https://oidc.com/userinfo', - jwksEndpoint: 'https://oidc.com/oauth2/jwks', + redirectUri: 'https://a.local.com/callback', + authorizationEndpoint: 'https://oidc.local.com/oauth2/authorize', + tokenEndpoint: 'https://oidc.local.com/oauth2/token', + userInfoEndpoint: 'https://oidc.local.com/userinfo', + jwksEndpoint: 'https://oidc.local.com/oauth2/jwks', scope: 'openid read' }; diff --git a/ssl/certificate.crt b/ssl/certificate.crt index 7967223..5c0093f 100644 --- a/ssl/certificate.crt +++ b/ssl/certificate.crt @@ -1,23 +1,26 @@ -----BEGIN CERTIFICATE----- -MIID5jCCAs6gAwIBAgIUKxk4umxo+aYVDQ483sMNIjyV3iwwDQYJKoZIhvcNAQEL -BQAwZDELMAkGA1UEBhMCQ04xEDAOBgNVBAgMB0JlaWppbmcxEDAOBgNVBAcMB0Jl -aWppbmcxFDASBgNVBAoMC0RldmVsb3BtZW50MQswCQYDVQQLDAJJVDEOMAwGA1UE -AwwFKi5jb20wHhcNMjUwNzE4MDEyNTQzWhcNMjYwNzE4MDEyNTQzWjBkMQswCQYD -VQQGEwJDTjEQMA4GA1UECAwHQmVpamluZzEQMA4GA1UEBwwHQmVpamluZzEUMBIG -A1UECgwLRGV2ZWxvcG1lbnQxCzAJBgNVBAsMAklUMQ4wDAYDVQQDDAUqLmNvbTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANmJXA4sFsmdGU36bN5ThBAJ -JZTY1vUaewmM41+IbSClPQEiO2/gK/tmDWBE82+IG7TEqbbLqUkORWFmp3bn0wgE -nW3JamZMfPo43rr+3i9OmxPzQUMzIn5HTh9+QPxAPGUafbHJTcIc3ixCXHu150+6 -Mh4v/fkRk6Yog93VIwpADYEyocVn8SkTzGn2FsF5R+hu/t+bGAO+e5iYiwlaheCj -yLfgzOuNo0jkxr1wmV2juN1CJ0nntqHaajMDqszQZXeBQ1K/UMgt2Ln2CRiQoatl -UfeFBF/urK1VI8TrvayeMyMkXiAOxxvKLqbvLmvwKlr7iRtrE0SUsOdfjA4h788C -AwEAAaOBjzCBjDAdBgNVHQ4EFgQUZqqbZSCLhcMWDsRQYZ5/vYjPVFMwHwYDVR0j -BBgwFoAUZqqbZSCLhcMWDsRQYZ5/vYjPVFMwDwYDVR0TAQH/BAUwAwEB/zA5BgNV -HREEMjAwggUqLmNvbYIFYS5jb22CBWIuY29tgghvaWRjLmNvbYIJbG9jYWxob3N0 -hwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQC78BfqEOofJRjECXRLspkTRfz8LsN2 -EYu3bMvSmRjgbApY6Sh4Yg6LQUBRNkw17GMQ1WQTKVdygqZeQWYZvD1FgBHjoWwk -3yfi9tfuWiD5XMWri7gpuF3xlizvSIgc0dR14J3w++1xB/jq030/wMPaYbgBqgIR -8z2KtnvpwJvLnifBJLJ3a3g24hyKkZ+Ye0/0f8aLMZtUBB99QY6PMe9E3GDnQbQi -4u4hPVzhWdxUQsoZ26NqgvobITx/hl7uKbr36NkGHUGgo9Vo+JEmy56HqZZjuwUK -w8a4/EK0wjjPR9ooj4TWfYWfTr6Bde6v0awLENqDGJpFNacxgXGesoA4 +MIIEaTCCAtGgAwIBAgIRAO0/byq8GJJoBReBXr16yJUwDQYJKoZIhvcNAQELBQAw +gZMxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTE0MDIGA1UECwwrc3Vu +cGVuZ0BzdW5wZW5nZGVNYWNCb29rLVByby5sb2NhbCAo5a2Z6bmPKTE7MDkGA1UE +AwwybWtjZXJ0IHN1bnBlbmdAc3VucGVuZ2RlTWFjQm9vay1Qcm8ubG9jYWwgKOWt +mem5jykwHhcNMjUwNzE4MDc1ODM2WhcNMjcxMDE4MDc1ODM2WjBfMScwJQYDVQQK +Ex5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxNDAyBgNVBAsMK3N1bnBl +bmdAc3VucGVuZ2RlTWFjQm9vay1Qcm8ubG9jYWwgKOWtmem5jykwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDgP4hfcsywJ9SbioGTODJDSG8yx1djhPp4 +yQZZ2jBqDHviF8rM+JcryTfR2Giuy9bi2ygNr18R8DDotFAp+S6LJRaS82rusiY4 ++uvfptlBKF4/E967GHtPGN59LanP8d3k3aPevpdCnAKw94doPJFmEm328h28AQYE +v+VeQEmYbYkpuOVT5eAjDKp+GFDx+zzXQN+U4domZPHZe0rCKFtPTHzk18yXuSTZ +q2ssAcxP27QwPPusccND3oESyF46lEbVjxRvDIYXLOHxahN7RKsSXTHaGqpGsiUH +Si2ppRRQmTzOMxkQB9ooW1Q2yatz5gzAq0dc8ft3VLowmloas6lPAgMBAAGjazBp +MA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAW +gBRe+VENNEgDa5/rKTEh9XpUTvjzfTAhBgNVHREEGjAYggsqLmxvY2FsLmNvbYIJ +bG9jYWwuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQAL7KrS2TMcOKIoMzTnFLv0skL9 +evEf+9rERDPVb/J/iplaV5y1RqZjQDU9vCvsjT2igf5Xcqipu0p33pqpHwnMehDx +M37bxonJMLOYz91PLyPZPUt76YF5NoySBshXGo6o4+PmCHpMChrspXWT36hJmfX2 +8mMwkFA6iafHy0bV9Chy6h7lzymWd7XR21xMIVT25faf8hRbx/xZ/xUf0/tWlY8Q +wp01RWFVcP2OWgeZxttnALvgXnJPFH29S2seB2TmkuZcz6ogcuCnYH2CQCC/yL0d +Y5YFzUqsmDaF5YUdFcwb6MTfIQOXyGLbTgBdqPgRVtxp76BTVV4nDP5IxjET6++C +QzhJ1teqZTLdPvXIa+yqXQ/EI1NKNNRAYtmduH5kHgR1vKrQDMSHEhTaIevjN037 +2dJEdRYi47OQIocqd80StGuRNL51dAYYjB3hj0WpQpKlzc8TurB8JeVwjzn5H9pv +A0k1s/OvborcCUguZOwVglAaVlHadmANhoel+q0= -----END CERTIFICATE----- diff --git a/ssl/private.key b/ssl/private.key index 9d7389b..c743963 100644 --- a/ssl/private.key +++ b/ssl/private.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDZiVwOLBbJnRlN -+mzeU4QQCSWU2Nb1GnsJjONfiG0gpT0BIjtv4Cv7Zg1gRPNviBu0xKm2y6lJDkVh -Zqd259MIBJ1tyWpmTHz6ON66/t4vTpsT80FDMyJ+R04ffkD8QDxlGn2xyU3CHN4s -Qlx7tedPujIeL/35EZOmKIPd1SMKQA2BMqHFZ/EpE8xp9hbBeUfobv7fmxgDvnuY -mIsJWoXgo8i34MzrjaNI5Ma9cJldo7jdQidJ57ah2mozA6rM0GV3gUNSv1DILdi5 -9gkYkKGrZVH3hQRf7qytVSPE672snjMjJF4gDscbyi6m7y5r8Cpa+4kbaxNElLDn -X4wOIe/PAgMBAAECggEABECEFR7VfzFb6kNH13yoayvSmTs30GipGQGw/BANmgLA -04HYyZIHKg3Pmx8d5wMxD3J8or8OWwg1YPcBtPhJDrIQZbH3K3K5SqbL67nJnAEc -VOJ/VxHrza4VH9Z27LdQtuUyqcP2iiHIUfMmHaDrmYpZKm/jtfea/Dd0hGSDH9Mh -dXJPKRQuQ/7ugDBJnIRYoGvdTzDSWFIzWWsC+nfeokK5paNMjkcJMFJGDVsP3lq0 -V4oH3eO2po5c/Mq4wXu+fTZthNIoQx2bkoUdG2x2aaPjNmMh6Lb6NbmBy7CBQH53 -sHVX6MRWireJMVRSJvhVrOilqDXYz90k4J8F8l0MSQKBgQDz7zGsNEQagSGF9k0p -xpHUSU2hit6rJ/QgToKixm9ED0984zcaXDHpCcYH6oLI+YTzfGx960zgPpmrJKvc -41FqZogGeP1GF/ZRylAz4E8OSib5LsWyzPzbczGAi+WKj0cfJHXZsXMBwL+AfLHk -q0j/71S8qW4D5vBSyk9BHgugowKBgQDkS+eoWYPlAm+tjBVH8rgPWK/oNiI9ET1V -K6MeH5O0gIVGwxZftZwvFXXNfJLXGkNnM/6/kXL16L7wYu9gW06fvw3gEsc4mQ+e -5FHF7JnwfI/pnbZENwoDco1AQsGqOMZawR+OM84R4oUz4zEzgwD3aWOJudTjDumQ -uI/hJKWq5QKBgQC3yRKyvNpG4d3BEbZHcF11BRmhSYDEkaCkKqLQQxOXwrVP0d01 -VhsgigWS90Q8aYqa7LbNFFhiZ6fdww5dqUMxGDkKL2QbyHgEXZqZyzmk+YdtnKjF -Mx6btKmqQTzbbWHXe9/y+Xg97Nwb0Vcygz7H3akJT9ocxIVyywx1ck6uYwKBgQCJ -aocOdpNFjanbNK66mAbideesRqllSLM6SQHuZ+NoitOuPE+DXLWeQbSe85UPlOdt -f4afmNUx397Ooz6jKVKyJTYc4jC4iKk2Ywg1sq0WbGPTovLLLLYCTTlorMYVyAbd -KdHsrpIjgc3b5az/7KLwSad4hzr1UUyVqAIy6vQtYQKBgQCJSsEM5hRhbQjQFBY6 -MjeAE9Y2jfZu3LYh2w/3SgO5FwvkBzc0s2Rg7d301sYryJoF720O5dwhI3p0qMru -WoGWR7Pn3MmdDKubH8fnYhk8QDC0HLR5+7yMPNeHNfRk0VCjUkWJvAjYo/p/r3TE -2fBQ0cw3/nOQ3QRZpfP5XRxdWQ== +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDgP4hfcsywJ9Sb +ioGTODJDSG8yx1djhPp4yQZZ2jBqDHviF8rM+JcryTfR2Giuy9bi2ygNr18R8DDo +tFAp+S6LJRaS82rusiY4+uvfptlBKF4/E967GHtPGN59LanP8d3k3aPevpdCnAKw +94doPJFmEm328h28AQYEv+VeQEmYbYkpuOVT5eAjDKp+GFDx+zzXQN+U4domZPHZ +e0rCKFtPTHzk18yXuSTZq2ssAcxP27QwPPusccND3oESyF46lEbVjxRvDIYXLOHx +ahN7RKsSXTHaGqpGsiUHSi2ppRRQmTzOMxkQB9ooW1Q2yatz5gzAq0dc8ft3VLow +mloas6lPAgMBAAECggEBAIfhF2I2rp7C08oX6CHruFEar/6F2Yb9CcRsksOZOSLZ +Q6uhHQqMSxWGDKPDzNK1wxSdFS0Nqb612vz2XWjBi5lWtNIAWzgdjJmUOZ7Ae/5G +Vq1D/f9Ce11XRWF2bOIKvZizUFtlA0SiQeM3ab4YjUXbPvSWirvjpuDz4ij1LWMF +QeT+GM763+f+otBN3fMXxj0B2OXu2s8qxNj+iY98hGRqPs9xYYSoYDflptrJBCTw +wLw9U8v/Lv4uglEMbNc2JvwvmqUQ30eDnrrMzlPZdlSHQyBubO0AbYtIhFQKZFvD +sK+YCjDTu7n+WiTEolSFS563eQo5fbDFXL1G9dc/YLECgYEA7Bv+2WZ2AmvPZAGQ +REWYq0U41Ue/JqgIXaDRR+cCHPV4aOd+GHOxVmK3Py0xB/D7Yo76+RJeJB4Vo/zp +YsT0xSoSzgXPJIAMEjNv0NKiBqj/ayvxd/tKxXGlduyVSCfgIV8mNSCv88nf9MA1 +Mh7R4X7C/zKQkZ2LwL0ZZgr5hTkCgYEA8yO8Q/eSspoXRH6uIl3KR24FMQOw+OkC +lAeLj3ZRZ3e0RpJNw85AxpQoWh/PVh12p7YKjM7UQHvUjfM6kcoeTzXYMqeK58/g +xqzt67qXI/AVQsJx2oCXnA4nwyhQsU7d/deYpAFN06Zkmks8EKHq/miWEf2PfNqB +uTuTPdWv6scCgYAkCaXdYuEyP2hZOE/fy8ugoKErFJddfBpCyDAJTH4rE2B8ipDZ +hJcVu12C3A/2yVZlVbOC3sXVt23QKOMqeyttCJ30KjjStmShRo6TjgLDB3pszjk9 ++fIQrub1fujOKZ/xGAfJ5iJVEIQJZGj6LHAWffWfZAVi5GwXUAWXaKdrKQKBgFta +ofIno0bX/sYNkv/2nXoZLHouGOBtLDrSWu2cVxm5MFMTxYQ8iroSENdL/GsuxtZc +37noPHe+Dy8GpIsClkDMyl699MMEqD/92acohIFMQ7DBvmWKy2wnJWl+TFNSfrZR +u1hj5QoRCtuuSPM240STp087Jh6TOwqOB9TD5UUhAoGARrBidxer/nmMisoVMlby +9oHNsZt9BrrzHVW/g5AeyRobaKZObYFsZlleLu6k6amfjU8c8fU4vCePSdOuhDWg +uJgClKZawtDV2LS2oOOcTRIhLdwDzT8XZcrrhCLskCcdSJSAVIC2JmhsLtjCuPvi +5t/fBrOuubfljuu9ULZjMc0= -----END PRIVATE KEY----- diff --git a/token.txt b/token.txt new file mode 100644 index 0000000..de908b9 --- /dev/null +++ b/token.txt @@ -0,0 +1 @@ +eyJraWQiOiIyMjYyZjU4MS0wNzRiLTRmZDctOGM2OC1mNGY3ODU1OGEyZWYiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoiY2xpZW50IiwibmJmIjoxNzUyNzM5MjUyLCJzY29wZSI6WyJvcGVuaWQiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDAwIiwiZXhwIjoxNzUyNzM5ODUyLCJpYXQiOjE3NTI3MzkyNTIsImp0aSI6ImMwNTZkNjAxLTBlZWYtNDBjYy04ODdiLTdjNjg1NDZlMzY2MSJ9.YifGLvHLe2QMInBgdJmW_us3m-Q41XX8RyaUfCv7J9gKT22wFAFtXUeXhF1bHXn9XUoPd3I33uBFclsYSs5XPqWlBhpiE-drRsZT-ZzbZ-x0YEELTkBEabYj-wivzHZZ9sVOxdV3yV7dN8-GpGJGyDglbX8tcWziZ4hCmSWIVCipb9cezBZdQYJiXe5SKb4qSmsQzF8F1CbBHuckfwXkggwVitW2YVKaeFDkV99RZRmcNzcvwVc6VU59cPr7VeirRTvjUyZKzpy9D9qtexKkSp0Fp0S4-J1NRHYh5mfaOBGSIQ52LE7c5Vz0lg23E0qCG3QvFkGF2i01fAIbEIpGLA