单点登录(Single Sign-On,简称SSO)是一种身份认证的解决方案,它允许用户只需一次登录即可访问多个应用程序或系统。在一个典型的SSO系统中,用户只需通过一次身份认证,就可以获得访问多个应用程序的授权,而不需要在每个应用程序中单独进行身份验证。
单点登录(Single Sign-On,简称SSO)的实现原理通常涉及到以下几个步骤:
需要注意的是,SSO服务器需要能够识别和验证来自不同应用程序的令牌。为了实现这一点,通常使用标准的认证协议,如OAuth、OpenID Connect等,这些协议为应用程序提供了一种标准的方式来与SSO服务器交互。此外,SSO服务器还需要实现一些安全机制来防止令牌被盗用或伪造,如Token加密、过期时间等等。
OpenID Connect 是一项在 OAuth 2.0 协议基础上构建的简单身份协议和开放式标准。 它使客户机应用程序依赖于 OpenID Connect 提供者执行的认证来验证用户身份。
OpenID Connect 使用 OAuth 2.0 进行认证和授权,然后构建用于唯一地标识用户的身份。 客户机还可以通过互操作和类似 REST 的方式从 OpenID Connect 提供者中获取关于用户的基本概要文件信
字段 |
字段类型 |
描述 |
id |
varchar |
主键,系统自动生成 |
archived |
tinyint |
用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档). |
create_time |
datetime |
数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段) |
updated_time |
timestamp |
数据的最后更新时间,由数据库自行更新维护 |
client_id |
varchar |
唯一,不能为空. |
client_id_issued_at |
timestamp |
client_id的签发时间, 默认为数据创建时间 |
client_secret |
varchar |
用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成),加密保存. |
client_secret_expires_at |
datetime |
client_secret的过期时间,null表示永不过期 |
client_name |
varchar |
客户端(client)的名称,一般是一个有业务意义的名称 |
client_authentication_methods |
varchar |
认证支持的方式,多个由逗号分隔; 如: client_secret_basic,client_secret_post; 一般指认证时传递client_secret支持哪些方式 |
authorization_grant_types |
varchar |
指定客户端支持的grant_type,可选值包括authorization_code,urn:ietf:params:oauth:grant-type:device_code,refresh_token, urn:ietf:params:oauth:grant-type:jwt-bearer,client_credentials, 若支持多个grant_type用逗号(,)分隔,如: "authorization_code,refresh_token". |
redirect_uris |
varchar |
OAuth2 认证后回调uri, 一般传递code, 多个由逗号分隔; 可为空, 当grant_type为authorization_code时, 在OAuth的流程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:当grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 'code'时客户端发起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第二步 用 'code' 换取 'access_token' 时客户也必须传递相同的redirect_uri. |
post_logout_redirect_uris |
varchar |
OAuth2 退出时 post 的客户端重定向 uri; 可选 多个由逗号分隔, 一般在client注册时可填写 |
scopes |
varchar |
指定客户端申请的权限范围,可选值在OIDC协议中定义, 包括openid,profile,email,address,phone;若有多个值用逗号(,)分隔,如: "openid,email". |
client_settings |
varchar |
客户端的各类设置, 如是否支持PKCE,用户授权(consent)确认是否必须等; 详见代码ClientSettings.java; 此字段存储JSON格式的数据值. |
token_settings |
varchar |
对token的各类设置; 如 token有效期, refresh_token有效期等; 详见代码TokenSettings.java; 此字段存储JSON格式的数据值. |
http://sso-server.com/oauth2/authorize
参数 |
值 |
说明 |
response_type |
code |
固定值 'code' |
scope |
openid profile email |
OIDC标准中定义的scope有: openid, profile, email, address, phone; 具体支持哪些由注册的client决定 |
client_id |
客户端注册生成的client_id |
|
redirect_uri |
回调用于检查server端返回的 'code'与'state',并发起对 access_token 的调用 |
|
state |
一个随机值, oauth-server 将原样返回,用于检测是否为跨站请求(CSRF)等 |
根据参数,最终生成地址如下:
http://sso-server.com/oauth2/authorize?response_type=code&scope=openid profile email&client_id=3b10c5b6a2534ed980767d5e03029f93&redirect_uri=http://localhost:8082/authorization_code_callback&state=0226f30c-d62d-4261-8241-c4971386f068
使用grant_type=authorization_code 方式来获取access_token, 需要先获取code
参数名 |
参数值 |
必须? |
备注 |
client_id |
{client_id} |
是 |
|
client_secret |
{client_secret} |
是 |
|
grant_type |
authorization_code |
是 |
固定值 |
code |
{code} |
是 |
|
redirect_uri |
{redirect_uri} |
是 |
|
code_verifier |
{code_verifier} |
否 |
PKCE时必须 |
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="client11"' \
--form 'grant_type="authorization_code"' \
--form 'redirect_uri="http://localhost:8083/oauth2/callback"' \
--form 'code="-VEnyAcEflDxjMh4Hr-6YejZq4Mel5gihFy_FMyotDxLhILeMBQheJkL4mdJ0sKD_C8xpa_sMNGf_I2tYJIVki8a4ktT2QsHojhbV3HpbGLVhJ0qDc8kfXjWt7u_24QO"' \
--form 'client_secret="secret22"'
{
"access_token": "7154afT_cxvLDq1naSg6Aq9ueSFSW8xRr5txryW5MlddRe7nV0RogTYwPsJc_rrRqwaIvLleerLhkjtIN2E2U-4J_BzvYNCsv8BVLqeerCObwgwpP3t__NMMUakzRL2i",
"refresh_token": "TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr",
"scope": "openid profile",
"id_token": "eyJraWQiOiJzb3MtZWNjLWtpZDEiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ1bml0eSIsImF1ZCI6IjZ1ck5MZ1I2b3NrMkU1NmVrcCIsInVwZGF0ZWRfYXQiOiIiLCJhenAiOiI2dXJOTGdSNm9zazJFNTZla3AiLCJhdXRoX3RpbWUiOjE2OTc3MDczNTQsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6ODA4MCIsIm5pY2tuYW1lIjoiIiwiZXhwIjoxNjk3NzA5MjA4LCJpYXQiOjE2OTc3MDc0MDgsImp0aSI6IjEyNTc0MjU2NTk4MDI2ODY2NzI3NDAwMTMxNjk5NDk0Iiwic2lkIjoidXdwN255RnJwdlNtWmlQS2hCdWVSVFZfcVRKYkN6ZjAyTmYwQTZGN1lrSSJ9.3w-7EY9SwKA-UkXlhDfD2BbSwP6nCSLZxNgKwhkkMY8YPbMkygbj374SmEmsit7NlpRXHCtW6ULZ9_IVZ9MTBg",
"token_type": "Bearer",
"expires_in": 3599
}
{
"error": "invalid_grant"
}
用于在access_token要过期时换取新的access_token (grant_type需要有refresh_token)
参数名 |
参数值 |
必须? |
备注 |
client_id |
{client_id} |
是 |
|
client_secret |
{client_secret} |
是 |
|
grant_type |
refresh_token |
是 |
固定值 |
refresh_token |
{refresh_token} |
是 |
curl --location 'http://localhost:8080/oauth2/token' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'grant_type="refresh_token"' \
--form 'refresh_token="TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr"'
{
"access_token": "YnVdTXl0MhslsrOjiz1ffSixvPnWCN-XS-UBlkS89daZbd_TvXtSSo_ODuFVWPWw1KsO5WQykVPjwSe_Kreo8ngIP9DglaXJMbYJJu4Wa6_geOINj5ksmnbfb6pHrQHr",
"refresh_token": "TZ9tzVwE_VLoJxALUSw4A4A0Nj7SLSWXCc69U9rvNmSnqR8Hbz-1m4uHebJWsAK0sa7SDIR4SNXOB3iaM0p1bH_8EBrljoBApQgdYi1uYzcVwYq55OVV2RUHN2BJwfSr",
"scope": "openid profile",
"id_token": "eyJraWQiOiJzb3MtZWNjLWtpZDEiLCJhbGciOiJFUzI1NiJ9.eyJzdWIiOiJ1bml0eSIsImF1ZCI6IjZ1ck5MZ1I2b3NrMkU1NmVrcCIsInVwZGF0ZWRfYXQiOjAsImF6cCI6IjZ1ck5MZ1I2b3NrMkU1NmVrcCIsImF1dGhfdGltZSI6MTY5NzcwNzM1NCwiaXNzIjoiaHR0cDovLzEyNy4wLjAuMTo4MDgwIiwibmlja25hbWUiOiIiLCJleHAiOjE2OTc3MjQyNjMsImlhdCI6MTY5NzcyMjQ2MywianRpIjoiMDc4OTc4MTUxNzEwNTgwNDE2ODY0NzgxMDQ1OTM5MDYiLCJzaWQiOiJ1d3A3bnlGcnB2U21aaVBLaEJ1ZVJUVl9xVEpiQ3pmMDJOZjBBNkY3WWtJIn0.j0KVv7bAi85zbX-0wvWe83n_CQdmJLGrHJNFwF5jA1-wa8QzaSwJbznpjbHLGTv-UbI2YeHLn8N5iGXDarbC9Q",
"token_type": "Bearer",
"expires_in": 3599
}
{
"error": "invalid_client"
}
校验, 检查token的有效性
参数名 |
参数值 |
必须? |
备注 |
client_id |
{client_id} |
是 |
|
client_secret |
{client_secret} |
是 |
|
token |
{token} |
是 |
token可以是access_token, refresh_token 或 id_token |
curl --location 'http://localhost:8080/oauth2/introspect' \
--header 'Content-Type: application/json' \
--form 'client_id="6urNLgR6osk2E56ekp"' \
--form 'client_secret="6urNLgR6osk2E56ekp"' \
--form 'token="GaHu88XEEAz41xMHfDk05bg9uSJ5Go1RF6jOe5eX7OhHD_52NK_fuwvVWq_dTRIhK8WR9SnCAtBBc0fVsOyGgz8-MhmVTG-dcDi6QtGQQtYxwmGrD-fOhpmePdUv6pwV"'
{
"active": true,
"sub": "admin",
"aud": [
"6urNLgR6osk2E56ekp"
],
"nbf": 1697721873,
"scope": "openid profile",
"iss": "http://127.0.0.1:8080",
"exp": 1697725474,
"iat": 1697721874,
"jti": "a1aa8f82-c885-45b3-a469-c2f595e8f12d",
"client_id": "6urNLgR6osk2E56ekp",
"token_type": "Bearer"
}
根据不同类型的token响应结果不相同; active=true表示token为有效的
{
"active": false
}
客户端带上access_token获取用户信息
curl --location 'http://localhost:8080/userinfo' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJraWQiOiJzb3MtcnNhLWtpZDIiLCJhbGciOiJSUzI1NiJ9.eyJzdWI...'
{
"sub": "unity",
"updated_at": 0,
"nickname": ""
}
通过 wx.login 接口获得临时登录凭证 code 后调用此接口,参考https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
接口返回微信登录信息和统一登录平台的accessToken
参考 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html