配置 IdentityServer 是个不容易的事情。我刚开始学习使用 IdentityServer 的时候,感觉真的像是走到了没有知识的荒原。与前端世界的 React,Vue关键词一比天壤之别。
我在开发过程中,遇到的第一个问题就是我需要自己搞个SSL证书,本地开发不使用HTTPS的话,最终生产环境部署和本地开发环境区别有些大,我选择尽可能保证一致性。顺便一提,在本地开发环境和生产环境我都将使用 Docker Compose,下文中的讲解也都将围绕我所使用的环境来展开。
如何搞到一个本地开发使用的证书呢?你可以使用下面的命令。我生成证书的时候写上去了两种域名,一种是为了模仿最终部署将使用的域名(xxx.local.mydomain.com),另一种是因为 docker 网络访问才写的(evrane-identity-identity-server)。之所以还要为 docker 网络访问再准备一个是因为 传统Web服务上使用OIDC的话,一方面是在 docker 网络中访问 IdentityServer 服务器必须,另一个是浏览器会被重定向到这个 docker中使用的网络的地址。第二行生成 pfx 加密证书文件是为了方便无Nginx直接启用TLS时方便,如果始终把服务放在 Nginx 后面,由 Nginx 处理 HTTPS 的话,不需要执行第二行
openssl req -x509 -newkey rsa:4096 -keyout evrane-local.key -out evrane-local.crt -nodes -subj "/CN=local.evrane.com" -addext "subjectAltName=DNS:admin.blog.local.evrane.com,DNS:admin.pine-hamster.local.evrane.com,DNS:api.pine-hamster.local.evrane.com,DNS:currency.pine-hamster.local.evrane.com,DNS:identity.local.evrane.com,DNS:currency.pine-hamster.local.evrane.com,DNS:yiran.local.evrane.com,DNS:evrane-identity-identity-server"
openssl pkcs12 -export -in evrane-local.crt -inkey evrane-local.key -out evrane-local.pfx -name "Evrane"
# Enter Export Password:password
# Verifying - Enter Export Password:password
然后根据刚才创建自签名证书时指定的域名,修改本地 hosts 文件
# same with dns settings on Google Domains
127.0.0.1 admin.blog.local.evrane.com
127.0.0.1 admin.pine-hamster.local.evrane.com
127.0.0.1 api.pine-hamster.local.evrane.com
127.0.0.1 currency.pine-hamster.local.evrane.com
127.0.0.1 identity.local.evrane.com
127.0.0.1 currency.pine-hamster.local.evrane.com
127.0.0.1 yiran.local.evrane.com
127.0.0.1 evrane-identity-identity-server
下一步,使用刚才生成的证书,提供给服务器使用。这里以 IdentityServer服务的说明文件为例,说明一下不使用 Nginx 而直接使用 Pfx 加密证书开启 TLS。
version: "3.9"
services:
identity-server:
build:
context: "../../.."
dockerfile: "./Evrane.IdentityServer/Dockerfile"
environment:
ASPNETCORE_URLS: "https://+:8000"
ASPNETCORE_Kestrel__Certificates__Default__Password: "${ASPNETCORE_Kestrel__Certificates__Default__Password}"
ASPNETCORE_Kestrel__Certificates__Default__Path: "/https/${ASPNETCORE_Kestrel__Pfx__FileName}"
ports:
- "8000:8000"
volumes:
- "./certs/${ASPNETCORE_Kestrel__Pfx__FileName}:/https/${ASPNETCORE_Kestrel__Pfx__FileName}:ro"
- "./certs/${ASPNETCORE_Kestrel__Crt__FileName}:/usr/local/share/ca-certificates/${ASPNETCORE_Kestrel__Crt__FileName}:ro"
networks:
- shared-network
networks:
shared-network:
driver: bridge
下面以身份服务的使用者业务Server的docker compose文件为例,说明一下使用 Nginx 开启TLS
# ...
nginx:
image: nginx:1.23
ports:
- "8100:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./certs:/etc/nginx/cert
# ...
# ...
server {
listen 443 ssl;
server_name admin.blog.local.evrane.com;
ssl_certificate /etc/nginx/cert/evrane-local.crt;
ssl_certificate_key /etc/nginx/cert/evrane-local.key;
location / {
proxy_pass http://evrane-blog-admin-server:8000;
}
}
# ...
此外需要额外注意的是,此时直接运行两个服务,业务服务是无法获取 identityserver 的配置并进行协议沟通的。原因是业务Server看到identity server 那边拿的是自签名证书, 不认。
解决方案有两个,其一是直接放弃自签名证书,identityserver直接放在有公网地址的服务器上,或把正式的证书下载到本地使用。第二个解决方法是在让业务Server的容器信任这个自签名证书。要么把证书打包进镜像,在build时把证书添加到白名单,要么在启动时mount证书,并修改 entry 执行添加白名单的同时启动服务(注:这种方式在Jetbrain Rider中无法 debug此 docker容器)
至此,当访问对应地址时,Nginx就会处理HTTPS并将HTTP请求发送给你的业务服务器。对于业务Server已经足够了。但是对于 IdentityServer 来说,还需要一些额外的配置(IdentityServer直接提供 HTTPS的方式不需要)。IdentityServer需要知道原本的 HTTP 请求头和使用的协议。
server {
# ...
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
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-Host $server_name;
}
}
在刚才的 nginx.conf 文件中增加这些 proxy相关的内容,使得Nginx会增加下面的请求头,将原始请求信息发送给 IdentityServer。此外在 IdentityServer 的 Program.cs 中需要增加下述的代码。
// ...
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// 信任所有的网络和代理
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var app = builder.Build();
app.UseForwardedHeaders();
其中两行调用 Clear() 方法的语句也格外重要。在微软的官方文档中提到“Only loopback addresses are configured for known proxies and known networks.”,只有本地环回地址才是被信任的,因为我的 IdentityServer 跑在Docker 容器中,所以需要清空,信任所有的网络和代理。
至此构建开发生产环境时最重要的部分就配置完成了。