单点登录平台设计

1.基本介绍

1.1什么是单点登录

单点登录(Single Sign-On,简称SSO)是一种身份认证的解决方案,它允许用户只需一次登录即可访问多个应用程序或系统。在一个典型的SSO系统中,用户只需通过一次身份认证,就可以获得访问多个应用程序的授权,而不需要在每个应用程序中单独进行身份验证。

单点登录平台设计_第1张图片

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.2客户端注册表

字段

字段类型

描述

id

varchar

主键,系统自动生成

archived

tinyint

用于标识客户端是否已存档(即实现逻辑删除),默认值为'0'(即未存档).
对该字段的具体使用请参考CustomJdbcClientDetailsService.java,在该类中,扩展了在查询client_details的SQL加上archived = 0条件 (扩展字段)

create_time

datetime

数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)

updated_time

timestamp

数据的最后更新时间,由数据库自行更新维护

client_id

varchar

唯一,不能为空.
用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务端自动生成).
对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appKey,与client_id是同一个概念.

client_id_issued_at

timestamp

client_id的签发时间, 默认为数据创建时间

client_secret

varchar

用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成),加密保存.
对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是同一个概念.

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".
在实际应用中,当注册时,该字段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: "authorization_code,refresh_token"(针对通过浏览器访问的客户端); "client_credentials"(针对另一个服务端的场景,不需要用户参与).
urn:ietf:params:oauth:grant-type:device_code与urn:ietf:params:oauth:grant-type:jwt-bearer是OAuth2.1中新增.

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.
在实际应用中, redirect_uris在注册时是必须填写的, 一般用来处理服务器返回的code, 验证state是否合法与通过code去换取access_token值.

post_logout_redirect_uris

varchar

OAuth2 退出时 post 的客户端重定向 uri; 可选 多个由逗号分隔, 一般在client注册时可填写

scopes

varchar

指定客户端申请的权限范围,可选值在OIDC协议中定义, 包括openid,profile,email,address,phone;若有多个值用逗号(,)分隔,如: "openid,email".
openid是必须有的,其他值若有则在获取的id_token中会包含对应的值.
在实际应该中, 该值一般由服务端指定, 常用的值为openid.

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

  1. 请求URI: /oauth2/token POST
  2. 请求参数说明:

参数名

参数值

必须?

备注

client_id

{client_id}

client_secret

{client_secret}

grant_type

authorization_code

固定值

code

{code}

redirect_uri

{redirect_uri}

code_verifier

{code_verifier}

PKCE时必须

  1. 请求示例:

  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"'

  1. 响应
  • 正常 [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)

  1. 请求URI: /oauth2/token POST
  2. 请求参数说明:

参数名

参数值

必须?

备注

client_id

{client_id}

client_secret

{client_secret}

grant_type

refresh_token

固定值

refresh_token

{refresh_token}

  1. 请求示例:

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"'

  1. 响应
  • 正常 [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的有效性

  1. 求URI: /oauth2/introspect POST
  2. 请求参数说明:

参数名

参数值

必须?

备注

client_id

{client_id}

client_secret

{client_secret}

token

{token}

token可以是access_token, refresh_token 或 id_token

  1. 请求示例:

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"'

  1. 响应
  • 正常 [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获取用户信息

  1. 请求URI: /userinfo GET
  2. 请求示例:

curl --location 'http://localhost:8080/userinfo' \

--header 'Content-Type: application/json' \

--header 'Authorization: Bearer eyJraWQiOiJzb3MtcnNhLWtpZDIiLCJhbGciOiJSUzI1NiJ9.eyJzdWI...'

  1. 响应
  • 正常 [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

你可能感兴趣的:(github)