This commit is contained in:
parent
e9208eb51b
commit
955f1c59a2
|
|
@ -9,8 +9,9 @@
|
||||||
<change beforePath="$PROJECT_DIR$/../oidc/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/pom.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../oidc/pom.xml" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/pom.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/src/main/java/com/tuoheng/oauth/oidc/config/SecurityConfig.java" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../oidc/src/main/resources/application.properties" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/src/main/resources/application.properties" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../oidc/src/main/resources/application.properties" beforeDir="false" afterPath="$PROJECT_DIR$/../oidc/src/main/resources/application.properties" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/application.properties" beforeDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/HelloController.class" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../resourceservice/target/classes/com/tuoheng/resourceservice/ResourceServiceApplication.class" beforeDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../resourceservicehtml/callback.html" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservicehtml/callback.html" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../resourceservicehtml/callback.html" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservicehtml/callback.html" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../resourceservicehtml/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservicehtml/index.html" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../resourceservicehtml/index.html" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservicehtml/index.html" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/../resourceservicehtml/test.html" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservicehtml/test.html" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/../resourceservicehtml/test.html" beforeDir="false" afterPath="$PROJECT_DIR$/../resourceservicehtml/test.html" afterDir="false" />
|
||||||
|
|
@ -115,7 +116,7 @@
|
||||||
<workItem from="1752741336600" duration="2274000" />
|
<workItem from="1752741336600" duration="2274000" />
|
||||||
<workItem from="1752745264222" duration="2832000" />
|
<workItem from="1752745264222" duration="2832000" />
|
||||||
<workItem from="1752751160098" duration="1320000" />
|
<workItem from="1752751160098" duration="1320000" />
|
||||||
<workItem from="1752798688493" duration="8093000" />
|
<workItem from="1752798688493" duration="8426000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ public class SecurityConfig {
|
||||||
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
|
||||||
http
|
http
|
||||||
.authorizeExchange(exchanges -> exchanges
|
.authorizeExchange(exchanges -> exchanges
|
||||||
.pathMatchers("/test/**").permitAll() // 测试路径不需要认证
|
.pathMatchers("/a/**").authenticated()
|
||||||
.pathMatchers("/api/**").authenticated()
|
.pathMatchers("/b/**").authenticated()
|
||||||
.anyExchange().permitAll()
|
.anyExchange().permitAll()
|
||||||
)
|
)
|
||||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
server.port=8080
|
server.port=8080
|
||||||
|
|
||||||
spring.cloud.gateway.routes[0].id=resource-server
|
spring.cloud.gateway.routes[0].id=resource-server-a
|
||||||
spring.cloud.gateway.routes[0].uri=http://localhost:8081
|
spring.cloud.gateway.routes[0].uri=http://localhost:8081
|
||||||
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
|
spring.cloud.gateway.routes[0].predicates[0]=Path=/a/**
|
||||||
spring.cloud.gateway.routes[0].filters[0]=TokenRelay
|
spring.cloud.gateway.routes[0].filters[0]=RewritePath=/a/(?<segment>.*), /api/${segment}
|
||||||
|
spring.cloud.gateway.routes[0].filters[1]=TokenRelay
|
||||||
|
|
||||||
# 添加一个不需要认证的测试路由
|
spring.cloud.gateway.routes[1].id=resource-server-b
|
||||||
spring.cloud.gateway.routes[1].id=test-route
|
spring.cloud.gateway.routes[1].uri=http://localhost:8082
|
||||||
spring.cloud.gateway.routes[1].uri=http://localhost:8081
|
spring.cloud.gateway.routes[1].predicates[0]=Path=/b/**
|
||||||
spring.cloud.gateway.routes[1].predicates[0]=Path=/test/**
|
spring.cloud.gateway.routes[1].filters[0]=RewritePath=/b/(?<segment>.*), /api/${segment}
|
||||||
spring.cloud.gateway.routes[1].filters[0]=RewritePath=/test/(?<segment>.*), /api/$\{segment}
|
spring.cloud.gateway.routes[1].filters[1]=TokenRelay
|
||||||
|
|
||||||
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000/oauth2/jwks
|
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000/oauth2/jwks
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
server.port=8080
|
|
||||||
|
|
||||||
spring.cloud.gateway.routes[0].id=resource-server
|
|
||||||
spring.cloud.gateway.routes[0].uri=http://localhost:8081
|
|
||||||
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
|
|
||||||
spring.cloud.gateway.routes[0].filters[0]=TokenRelay
|
|
||||||
|
|
||||||
# 添加一个不需要认证的测试路由
|
|
||||||
spring.cloud.gateway.routes[1].id=test-route
|
|
||||||
spring.cloud.gateway.routes[1].uri=http://localhost:8081
|
|
||||||
spring.cloud.gateway.routes[1].predicates[0]=Path=/test/**
|
|
||||||
spring.cloud.gateway.routes[1].filters[0]=RewritePath=/test/(?<segment>.*), /api/$\{segment}
|
|
||||||
|
|
||||||
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:9000/oauth2/jwks
|
|
||||||
Binary file not shown.
Binary file not shown.
147
nginx/nginx.conf
147
nginx/nginx.conf
|
|
@ -17,6 +17,11 @@ events {
|
||||||
|
|
||||||
# HTTP模块配置
|
# HTTP模块配置
|
||||||
http {
|
http {
|
||||||
|
map $http_origin $cors_origin {
|
||||||
|
default "";
|
||||||
|
"https://a.local.com" "https://a.local.com";
|
||||||
|
"https://b.local.com" "https://b.local.com";
|
||||||
|
}
|
||||||
# MIME类型
|
# MIME类型
|
||||||
include /opt/homebrew/etc/nginx/mime.types;
|
include /opt/homebrew/etc/nginx/mime.types;
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
|
|
@ -51,7 +56,7 @@ http {
|
||||||
# 前端应用服务器 (a.local.com) - HTTPS
|
# 前端应用服务器 (a.local.com) - HTTPS
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name a.local.com;
|
server_name b.local.com;
|
||||||
|
|
||||||
# SSL配置
|
# SSL配置
|
||||||
ssl_certificate /Users/sunpeng/workspace/remote/oauth2/ssl/certificate.crt;
|
ssl_certificate /Users/sunpeng/workspace/remote/oauth2/ssl/certificate.crt;
|
||||||
|
|
@ -61,14 +66,14 @@ http {
|
||||||
ssl_prefer_server_ciphers off;
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
# 静态文件目录
|
# 静态文件目录
|
||||||
root /Users/sunpeng/workspace/remote/oauth2/resourceservicehtml;
|
root /Users/sunpeng/workspace/remote/oauth2/resourceservicehtmlb;
|
||||||
|
|
||||||
# 首页和静态文件
|
# 首页和静态文件
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
# 添加CORS头
|
# 添加CORS头
|
||||||
add_header Access-Control-Allow-Origin *;
|
add_header Access-Control-Allow-Origin $cors_origin;
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
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-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
|
||||||
}
|
}
|
||||||
|
|
@ -86,6 +91,7 @@ http {
|
||||||
|
|
||||||
# /api 路径转发到网关
|
# /api 路径转发到网关
|
||||||
location /api/ {
|
location /api/ {
|
||||||
|
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;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
|
@ -93,40 +99,14 @@ http {
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
# 处理CORS
|
# 处理CORS
|
||||||
add_header Access-Control-Allow-Origin "https://a.local.com" always;
|
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||||
add_header Access-Control-Allow-Credentials "true" 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-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-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
|
||||||
|
|
||||||
# 处理OPTIONS请求
|
# 处理OPTIONS请求
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
add_header Access-Control-Allow-Origin "https://a.local.com" always;
|
add_header Access-Control-Allow-Origin $cors_origin 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;
|
|
||||||
add_header Content-Type "text/plain; charset=utf-8";
|
|
||||||
add_header Content-Length 0;
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# /test 路径转发到网关 (测试用)
|
|
||||||
location /test/ {
|
|
||||||
proxy_pass http://localhost:8080;
|
|
||||||
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;
|
|
||||||
|
|
||||||
# 处理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, 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.local.com" always;
|
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
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-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 Access-Control-Max-Age 1728000;
|
||||||
|
|
@ -149,14 +129,111 @@ http {
|
||||||
access_log /tmp/nginx_oidc_logout.log main_cookie;
|
access_log /tmp/nginx_oidc_logout.log main_cookie;
|
||||||
|
|
||||||
# 处理CORS
|
# 处理CORS
|
||||||
add_header Access-Control-Allow-Origin "https://a.local.com" always;
|
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||||
add_header Access-Control-Allow-Credentials "true" always;
|
add_header Access-Control-Allow-Credentials "true" always;
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
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-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
|
||||||
|
|
||||||
# 处理OPTIONS请求
|
# 处理OPTIONS请求
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
add_header Access-Control-Allow-Origin "https://a.local.com" always;
|
add_header Access-Control-Allow-Origin $cors_origin 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name a.local.com;
|
||||||
|
|
||||||
|
# SSL配置
|
||||||
|
ssl_certificate /Users/sunpeng/workspace/remote/oauth2/ssl/certificate.crt;
|
||||||
|
ssl_certificate_key /Users/sunpeng/workspace/remote/oauth2/ssl/private.key;
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
|
||||||
|
# 静态文件目录
|
||||||
|
root /Users/sunpeng/workspace/remote/oauth2/resourceservicehtml;
|
||||||
|
|
||||||
|
# 首页和静态文件
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
|
||||||
|
# 添加CORS头
|
||||||
|
add_header Access-Control-Allow-Origin $cors_origin;
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
# 防止前端server拦截OIDC登录页,/login直接返回404
|
||||||
|
location = /login {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 处理OPTIONS预检请求
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# /api 路径转发到网关
|
||||||
|
location /api/ {
|
||||||
|
rewrite ^/api/(.*)$ /a/$1 break;
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
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;
|
||||||
|
|
||||||
|
# 处理CORS
|
||||||
|
add_header Access-Control-Allow-Origin $cors_origin 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 $cors_origin 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;
|
||||||
|
add_header Content-Type "text/plain; charset=utf-8";
|
||||||
|
add_header Content-Length 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# /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 $cors_origin 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 $cors_origin always;
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
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-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 Access-Control-Max-Age 1728000;
|
||||||
|
|
@ -194,12 +271,12 @@ http {
|
||||||
proxy_set_header X-Forwarded-Host $host;
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
proxy_set_header X-Forwarded-Ssl on;
|
proxy_set_header X-Forwarded-Ssl on;
|
||||||
proxy_set_header Cookie $http_cookie;
|
proxy_set_header Cookie $http_cookie;
|
||||||
add_header Access-Control-Allow-Origin "https://a.local.com" always;
|
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||||
add_header Access-Control-Allow-Credentials "true" 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-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-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization";
|
||||||
if ($request_method = 'OPTIONS') {
|
if ($request_method = 'OPTIONS') {
|
||||||
add_header Access-Control-Allow-Origin "https://a.local.com" always;
|
add_header Access-Control-Allow-Origin $cors_origin always;
|
||||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
|
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-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 Access-Control-Max-Age 1728000;
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,24 @@ public class SecurityConfig {
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// b.local 前端应用的 client
|
||||||
|
RegisteredClient bClient = RegisteredClient.withId(UUID.randomUUID().toString())
|
||||||
|
.clientId("b-client")
|
||||||
|
.clientSecret(passwordEncoder.encode("b-secret"))
|
||||||
|
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||||
|
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||||
|
.redirectUri("https://b.local.com/callback")
|
||||||
|
.scope(OidcScopes.OPENID)
|
||||||
|
.scope("read")
|
||||||
|
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(false).build())
|
||||||
|
.tokenSettings(TokenSettings.builder()
|
||||||
|
.accessTokenTimeToLive(Duration.ofMinutes(10))
|
||||||
|
.refreshTokenTimeToLive(Duration.ofHours(1))
|
||||||
|
.build())
|
||||||
|
.build();
|
||||||
|
|
||||||
return new InMemoryRegisteredClientRepository(aClient);
|
return new InMemoryRegisteredClientRepository(aClient, bClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成 RSA 密钥对
|
// 生成 RSA 密钥对
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<annotationProcessing>
|
||||||
|
<profile name="Maven default annotation processors profile" enabled="true">
|
||||||
|
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||||
|
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||||
|
<outputRelativeToContentRoot value="true" />
|
||||||
|
<module name="resourceservice" />
|
||||||
|
</profile>
|
||||||
|
</annotationProcessing>
|
||||||
|
</component>
|
||||||
|
<component name="JavacSettings">
|
||||||
|
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||||
|
<module name="resourceservice" options="-parameters" />
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Encoding">
|
||||||
|
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RemoteRepositoriesConfiguration">
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Central Repository" />
|
||||||
|
<option name="url" value="http://maven.aliyun.com/nexus/content/groups/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="snapshot-repo" />
|
||||||
|
<option name="name" value="Snapshot Repository" />
|
||||||
|
<option name="url" value="http://192.168.10.101:48081/repository/maven-snapshots/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="release-repo" />
|
||||||
|
<option name="name" value="Release Repository" />
|
||||||
|
<option name="url" value="http://192.168.10.103:48081/repository/maven-public/" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="central" />
|
||||||
|
<option name="name" value="Maven Central repository" />
|
||||||
|
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||||
|
</remote-repository>
|
||||||
|
<remote-repository>
|
||||||
|
<option name="id" value="jboss.community" />
|
||||||
|
<option name="name" value="JBoss Community repository" />
|
||||||
|
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||||
|
</remote-repository>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="MavenProjectsManager">
|
||||||
|
<option name="originalFiles">
|
||||||
|
<list>
|
||||||
|
<option value="$PROJECT_DIR$/pom.xml" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="openjdk-23" project-jdk-type="JavaSDK" />
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>2.7.18</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.tuoheng</groupId>
|
||||||
|
<artifactId>resourceservice</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>resourceservice</name>
|
||||||
|
<description>Simple RESTful Resource Service</description>
|
||||||
|
<properties>
|
||||||
|
<java.version>11</java.version>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.tuoheng.resourceservice;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class HelloController {
|
||||||
|
@GetMapping("/api/hello")
|
||||||
|
public Map<String, Object> hello() {
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("message", "Hello from Resource Service!");
|
||||||
|
response.put("timestamp", System.currentTimeMillis());
|
||||||
|
response.put("service", "Resource Service");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.tuoheng.resourceservice;
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class ResourceServiceApplication {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ResourceServiceApplication.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
server.port=8082
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>OIDC回调处理</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||||
|
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||||
|
.loading { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||||||
|
.spinner {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin: 20px auto;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>处理OIDC回调</h1>
|
||||||
|
|
||||||
|
<div id="status" class="status loading">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>正在处理认证回调...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="result" style="display: none;">
|
||||||
|
<div id="resultContent"></div>
|
||||||
|
<button onclick="goToMain()" style="margin-top: 20px; padding: 10px 20px; background-color: #007bff; color: white; border: none; border-radius: 5px; cursor: pointer;">
|
||||||
|
返回主页
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// OIDC配置
|
||||||
|
const oidcConfig = {
|
||||||
|
clientId: 'b-client',
|
||||||
|
clientSecret: 'b-secret',
|
||||||
|
redirectUri: 'https://b.local.com/callback',
|
||||||
|
tokenEndpoint: 'https://oidc.local.com/oauth2/token',
|
||||||
|
userInfoEndpoint: 'https://oidc.local.com/userinfo'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面加载时执行
|
||||||
|
window.onload = function() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
const state = urlParams.get('state');
|
||||||
|
const error = urlParams.get('error');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
showError('认证失败: ' + error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
showError('未收到授权码');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证state参数
|
||||||
|
const savedState = localStorage.getItem('oauth_state');
|
||||||
|
if (state !== savedState) {
|
||||||
|
showError('状态验证失败,可能存在CSRF攻击');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交换授权码为token
|
||||||
|
exchangeCodeForToken(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 交换授权码为token
|
||||||
|
function exchangeCodeForToken(code) {
|
||||||
|
const tokenData = new URLSearchParams();
|
||||||
|
tokenData.append('grant_type', 'authorization_code');
|
||||||
|
tokenData.append('code', code);
|
||||||
|
tokenData.append('redirect_uri', oidcConfig.redirectUri);
|
||||||
|
|
||||||
|
// 使用Basic认证
|
||||||
|
const credentials = btoa(oidcConfig.clientId + ':' + oidcConfig.clientSecret);
|
||||||
|
|
||||||
|
fetch(oidcConfig.tokenEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': 'Basic ' + credentials
|
||||||
|
},
|
||||||
|
body: tokenData
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.json().then(err => {
|
||||||
|
throw new Error(`Token交换失败: ${err.error || response.statusText}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.access_token) {
|
||||||
|
// 保存token
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
if (data.refresh_token) {
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除state
|
||||||
|
localStorage.removeItem('oauth_state');
|
||||||
|
|
||||||
|
showSuccess('认证成功!正在获取用户信息...');
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
return fetch(oidcConfig.userInfoEndpoint, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${data.access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('响应中未包含access_token');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取用户信息失败');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(userInfo => {
|
||||||
|
showSuccess(`认证成功!欢迎 ${userInfo.sub || 'user'}`);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('认证过程出错:', error);
|
||||||
|
showError('认证失败: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示成功信息
|
||||||
|
function showSuccess(message) {
|
||||||
|
document.getElementById('status').className = 'status success';
|
||||||
|
document.getElementById('status').innerHTML = `
|
||||||
|
<h3>✅ 成功</h3>
|
||||||
|
<p>${message}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('result').style.display = 'block';
|
||||||
|
document.getElementById('resultContent').innerHTML = `
|
||||||
|
<h3>认证完成</h3>
|
||||||
|
<p>您已成功登录系统B</p>
|
||||||
|
<p>Token已保存到本地存储</p>
|
||||||
|
`;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误信息
|
||||||
|
function showError(message) {
|
||||||
|
document.getElementById('status').className = 'status error';
|
||||||
|
document.getElementById('status').innerHTML = `
|
||||||
|
<h3>❌ 错误</h3>
|
||||||
|
<p>${message}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('result').style.display = 'block';
|
||||||
|
document.getElementById('resultContent').innerHTML = `
|
||||||
|
<h3>认证失败</h3>
|
||||||
|
<p>请检查错误信息并重试</p>
|
||||||
|
`;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回主页
|
||||||
|
function goToMain() {
|
||||||
|
window.location.href = '/';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,294 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>系统B - OIDC登录</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
|
||||||
|
.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
|
||||||
|
.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
|
||||||
|
button {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
button:hover { background-color: #0056b3; }
|
||||||
|
.logout { background-color: #dc3545; }
|
||||||
|
.logout:hover { background-color: #c82333; }
|
||||||
|
.api-result {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin: 10px 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>系统B - OIDC登录</h1>
|
||||||
|
|
||||||
|
<div id="status" class="status info">
|
||||||
|
正在检查登录状态...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="userInfo" style="display: none;">
|
||||||
|
<h2>用户信息</h2>
|
||||||
|
<div id="userDetails"></div>
|
||||||
|
<button onclick="callApi()">调用API</button>
|
||||||
|
<button class="logout" onclick="logout()">退出登录</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loginSection" style="display: none;">
|
||||||
|
<h2>请登录</h2>
|
||||||
|
<button onclick="login()">登录到OIDC</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="apiResult" class="api-result" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// OIDC配置
|
||||||
|
const oidcConfig = {
|
||||||
|
clientId: 'b-client',
|
||||||
|
clientSecret: 'b-secret',
|
||||||
|
redirectUri: 'https://b.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'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查URL参数中的授权码
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const code = urlParams.get('code');
|
||||||
|
const state = urlParams.get('state');
|
||||||
|
|
||||||
|
// 页面加载时执行
|
||||||
|
window.onload = function() {
|
||||||
|
if (code) {
|
||||||
|
// 有授权码,处理回调
|
||||||
|
handleCallback(code, state);
|
||||||
|
} else {
|
||||||
|
// 检查现有token
|
||||||
|
checkAuthStatus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 检查认证状态
|
||||||
|
function checkAuthStatus() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (token) {
|
||||||
|
// 验证token是否有效
|
||||||
|
validateToken(token);
|
||||||
|
} else {
|
||||||
|
showLoginSection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token
|
||||||
|
function validateToken(token) {
|
||||||
|
fetch('https://oidc.local.com/userinfo', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error('Token无效');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(userInfo => {
|
||||||
|
showUserInfo(userInfo);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Token验证失败:', error);
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
showLoginSection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示用户信息
|
||||||
|
function showUserInfo(userInfo) {
|
||||||
|
document.getElementById('status').className = 'status success';
|
||||||
|
document.getElementById('status').textContent = '登录成功!';
|
||||||
|
|
||||||
|
document.getElementById('userInfo').style.display = 'block';
|
||||||
|
document.getElementById('loginSection').style.display = 'none';
|
||||||
|
|
||||||
|
document.getElementById('userDetails').innerHTML = `
|
||||||
|
<p><strong>用户名:</strong> ${userInfo.sub || 'user'}</p>
|
||||||
|
<p><strong>Token:</strong> ${localStorage.getItem('access_token').substring(0, 50)}...</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示登录部分
|
||||||
|
function showLoginSection() {
|
||||||
|
document.getElementById('status').className = 'status info';
|
||||||
|
document.getElementById('status').textContent = '请登录以继续';
|
||||||
|
|
||||||
|
document.getElementById('userInfo').style.display = 'none';
|
||||||
|
document.getElementById('loginSection').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
function login() {
|
||||||
|
const state = generateRandomString();
|
||||||
|
localStorage.setItem('oauth_state', state);
|
||||||
|
|
||||||
|
const authUrl = new URL(oidcConfig.authorizationEndpoint);
|
||||||
|
authUrl.searchParams.set('response_type', 'code');
|
||||||
|
authUrl.searchParams.set('client_id', oidcConfig.clientId);
|
||||||
|
authUrl.searchParams.set('redirect_uri', oidcConfig.redirectUri);
|
||||||
|
authUrl.searchParams.set('scope', oidcConfig.scope);
|
||||||
|
authUrl.searchParams.set('state', state);
|
||||||
|
|
||||||
|
window.location.href = authUrl.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理回调
|
||||||
|
function handleCallback(code, state) {
|
||||||
|
const savedState = localStorage.getItem('oauth_state');
|
||||||
|
if (state !== savedState) {
|
||||||
|
document.getElementById('status').className = 'status error';
|
||||||
|
document.getElementById('status').textContent = '状态验证失败';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交换授权码为token
|
||||||
|
exchangeCodeForToken(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交换授权码为token
|
||||||
|
function exchangeCodeForToken(code) {
|
||||||
|
const tokenData = new URLSearchParams();
|
||||||
|
tokenData.append('grant_type', 'authorization_code');
|
||||||
|
tokenData.append('code', code);
|
||||||
|
tokenData.append('redirect_uri', oidcConfig.redirectUri);
|
||||||
|
|
||||||
|
// 使用Basic认证
|
||||||
|
const credentials = btoa(oidcConfig.clientId + ':' + oidcConfig.clientSecret);
|
||||||
|
|
||||||
|
fetch(oidcConfig.tokenEndpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': 'Basic ' + credentials
|
||||||
|
},
|
||||||
|
body: tokenData
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.access_token) {
|
||||||
|
localStorage.setItem('access_token', data.access_token);
|
||||||
|
if (data.refresh_token) {
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
return fetch(oidcConfig.userInfoEndpoint, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${data.access_token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('获取token失败: ' + JSON.stringify(data));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(userInfo => {
|
||||||
|
// 清除URL中的参数
|
||||||
|
window.history.replaceState({}, document.title, window.location.pathname);
|
||||||
|
showUserInfo(userInfo);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Token交换失败:', error);
|
||||||
|
document.getElementById('status').className = 'status error';
|
||||||
|
document.getElementById('status').textContent = '登录失败: ' + error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API
|
||||||
|
function callApi() {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (!token) {
|
||||||
|
alert('请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('apiResult').style.display = 'block';
|
||||||
|
document.getElementById('apiResult').textContent = '正在调用API...';
|
||||||
|
|
||||||
|
fetch('/api/hello', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('apiResult').textContent = 'API调用成功:\n' + JSON.stringify(data, null, 2);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('apiResult').textContent = 'API调用失败:\n' + error.message;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
function logout() {
|
||||||
|
// 清理本地token
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
localStorage.removeItem('refresh_token');
|
||||||
|
localStorage.removeItem('oauth_state');
|
||||||
|
// 获取id_token(如果有)
|
||||||
|
const idToken = localStorage.getItem('id_token'); // 登录时保存id_token
|
||||||
|
// 退出后跳转到首页
|
||||||
|
const redirectUri = encodeURIComponent('https://b.local.com');
|
||||||
|
// 拼接logout url
|
||||||
|
let logoutUrl = `https://b.local.com/oidc-logout?post_logout_redirect_uri=${redirectUri}`;
|
||||||
|
if (idToken) {
|
||||||
|
logoutUrl += `&id_token_hint=${idToken}`;
|
||||||
|
}
|
||||||
|
// 跳转到OIDC logout端点
|
||||||
|
window.location.href = logoutUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机字符串
|
||||||
|
function generateRandomString() {
|
||||||
|
const array = new Uint32Array(28);
|
||||||
|
window.crypto.getRandomValues(array);
|
||||||
|
return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue