1.基本介绍
1.1什么是单点登录
单点登录(Single Sign-On,简称SSO)是一种身份认证的解决方案,它允许用户只需一次登录即可访问多个应用程序或系统。在一个典型的SSO系统中,用户只需通过一次身份认证,就可以获得访问多个应用程序的授权,而不需要在每个应用程序中单独进行身份验证。
1.2单点登录的原理
单点登录(Single Sign-On,简称SSO)的实现原理通常涉及到以下几个步骤:
- 用户登录认证:用户在访问第一个应用程序时,需要进行身份验证并登录系统。这个过程可以使用任何一种常规的认证方式,比如用户名和密码、二次验证等等。
- 生成令牌:认证通过后,系统会生成一个安全令牌(Token),将它存储在用户的浏览器中,同时将该令牌的信息存储在SSO服务器中。
- 传递令牌:当用户访问其他应用程序时,应用程序将向SSO服务器发送令牌验证请求。SSO服务器会检查浏览器中的令牌信息,并确认用户的身份。
- 验证身份:如果令牌有效且用户已经进行过身份验证,SSO服务器会向应用程序返回一个授权令牌,授权用户访问该应用程序。
- 访问应用程序:应用程序会使用授权令牌来验证用户的身份,并允许用户访问应用程序的资源。
需要注意的是,SSO服务器需要能够识别和验证来自不同应用程序的令牌。为了实现这一点,通常使用标准的认证协议,如OAuth、OpenID Connect等,这些协议为应用程序提供了一种标准的方式来与SSO服务器交互。此外,SSO服务器还需要实现一些安全机制来防止令牌被盗用或伪造,如Token加密、过期时间等等。
2.基于OpenID Connect的SSO实现
OpenID Connect 是一项在 OAuth 2.0 协议基础上构建的简单身份协议和开放式标准。 它使客户机应用程序依赖于 OpenID Connect 提供者执行的认证来验证用户身份。
OpenID Connect 使用 OAuth 2.0 进行认证和授权,然后构建用于唯一地标识用户的身份。 客户机还可以通过互操作和类似 REST 的方式从 OpenID Connect 提供者中获取关于用户的基本概要文件信
2.1单点登录流程
- 用户1访问应用1,应用1判断用户未登录,发送302跳转并携带回调地址参数到SSO单点登录服务
- 在SSO服务中,输入用户名和密码,登录SSO服务
- SSO服务登录成功后,跳转到回调地址,并携带code参数到应用1
- 应用1根据code参数,调用接口/oauth2/token 获取AccessToken IdToken信息,根据IdToken获取对应的用户信息,自动登录应用1
- 用户1访问应用2,应用2判断用户未登录,发送302跳转并携带回调地址参数到SSO单点登录服务
- SSO服务已登录,跳转到回调地址,并携带code参数到应用2
- 应用2根据code参数,调用接口/oauth2/token 获取AccessToken IdToken信息,根据IdToken获取对应的用户信息,自动登录应用2
2.2客户端注册表
字段 | 字段类型 | 描述 |
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格式的数据值. |
2.3获取Authorization Uri
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
2.4获取access_token (grant_type=authorization_code)
使用grant_type=authorization_code 方式来获取access_token, 需要先获取code
- 请求URI: /oauth2/token POST
- 请求参数说明:
参数名 | 参数值 | 必须? | 备注 |
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"'
- 响应
- 正常 [200]
{
"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
}
- 异常 [401]
{
"error": "invalid_grant"
}
2.5刷新access_token (grant_type=refresh_token)
用于在access_token要过期时换取新的access_token (grant_type需要有refresh_token)
- 请求URI: /oauth2/token POST
- 请求参数说明:
参数名 | 参数值 | 必须? | 备注 |
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"'
- 响应
- 正常 [200]
{
"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
}
- 异常 [401]
{
"error": "invalid_client"
}
2.6检查token (/oauth2/introspect) public
校验, 检查token的有效性
- 求URI: /oauth2/introspect POST
- 请求参数说明:
参数名 | 参数值 | 必须? | 备注 |
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"'
- 响应
- 正常 [200]
{
"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为有效的
- 异常 [200]
{
"active": false
}
2.7 OIDC /userinfo 获取用户信息
客户端带上access_token获取用户信息
- 请求URI: /userinfo GET
- 请求示例:
curl --location 'http://localhost:8080/userinfo' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer eyJraWQiOiJzb3MtcnNhLWtpZDIiLCJhbGciOiJSUzI1NiJ9.eyJzdWI...'
- 响应
- 正常 [200]
{
"sub": "unity",
"updated_at": 0,
"nickname": ""
}
- 具体有哪些属性值由scope范围来决定
- 异常 [401]
3.小程序接口
登录凭证校验 /minapp/code2session
通过 wx.login 接口获得临时登录凭证 code 后调用此接口,参考https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
接口返回微信登录信息和统一登录平台的accessToken
获取小程序手机号 /minapp/getuserphonenumber
参考 https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html