文章目录

  • 前言
  • 一、分析思路
    • 1、单点登录
      • 授权码认证
      • 隐式认证
      • 混合认证
    • 2、会话管理
  • 二、实现过程
    • 1、搭建DEX认证中⼼
    • 2、登录
      • 流程说明
      • 授权码认证示例代码
    • 3、登出
      • 流程说明
      • 登出代码示例

前言

通常,我们在登录单系统时,都希望只需要登录⼀次,就能访问本系统中包含的所有资源。但实际中,单系统往往⽆法囊括所有内容,总会出现其他系统资源的情况,⽽访问其他系统时,⼜需要重新登录。因此⼀次登录,访问多个系统的资源,成了⼤多⽤户的痛点。

然而,多系统的访问需要解决以下⼏个问题:

①⽤户只需要登录⼀次,就能访问所有系统的资源。

②⽤户退出登录(主动退出或超时⾃动退出),所有系统资源都不能访问。

本期我们就基于上述问题一起来探讨分析,看看如何解决实现。


一、分析思路

1、单点登录

单点登录(SSO,Single sign-on)⽤来解决第⼀个问题:⽤户只需要登录⼀次,就能访问所有系统的资源。

单点登录是⼀种身份验证解决⽅案,可让⽤户通过⼀次性⽤户身份验证登录多个应⽤程序和⽹站。

本次采⽤DEX来实现单点登录。DEX是基于OpenID Connect协议实现的⼀个认证服务,OpenID Connect是从oauth2认证协议演进过来的。

DEX⼤致分为两个部分:

  • ⼀个是实现OpenID Connect协议的服务端。服务端包含登录⻚⾯以及⼀些⽤于验证的http后端接⼝。
  • ⼀个是⽤于验证账号的连接器。连接器将⽤户输⼊的账号密码发送到账号系统进⾏认证。DEX官⽅⽀持的连接器有:LDAP,GitHub,SAML
    2.0,Gitlab,OpenID Connect,OAuth2.0,Google,LinkedIn,Microsoft,AuthProxy,Bitucket
    Cloud,OpenShift,Atlassian,Crowd,Gitea,Open Stack Keystone,Integration
    kubelogin and Active Directory。

OpenID Connect协议中包含了三种认证模式:授权码认证,隐式认证,混合认证。

授权码认证

OpenID Connect授权代码流程通过以下步骤进⾏:

1、客户端准备⼀个包含所需请求参数的身份验证请求。

2、 客户端将请求发送到授权服务器。

3、授权服务器对最终⽤户进⾏身份验证。

4、授权服务器获得最终⽤户同意/授权。

5、授权服务器使⽤授权代码将最终⽤户发送回客户端。

6、客户端使⽤令牌端点的授权代码请求响应。

7、 客户端在响应主体中收到包含ID令牌和访问令牌的响应。

8、客户端验证ID令牌并检索最终⽤户的主题标识符。

结合DEX给出的实现⽅案如下:

授权码

授权码在请求⼀次token端点后就会失效,超过⼀定时间,也会⾃动失效。

token

返回的token信息中,包含了access_token,id_token,refresh_token。

  • access_token:可⽤于应⽤内部的请求验证。其hash值包含于id_token中,即可通过id_token直接验证access_token。
  • id_token:可⽤于跨应⽤的请求验证。跨应⽤时,需在client中设置跨应⽤权限。id_token超过⼀定时间,会⾃动失效。id_token的验证需要通过dex提供的接⼝进⾏验证。
  • refresh_token:access_token或id_token失效时,⽤于刷新access_token,id_token。
  • refresh_token超过⼀定时间后,会⾃动失效。实际场景中,会将refresh_token的超时时间设置的⽐较⼤。

隐式认证

隐式流程按照以下步骤操作:

1、客户端准备⼀个包含所需请求参数的身份验证请求。

2、客户端将请求发送到授权服务器。

3、授权服务器对最终⽤户进⾏身份验证。

4、授权服务器获得最终⽤户同意/授权。

5、 授权服务器将最终⽤户发送回客户端,并带有ID令牌,如果需要,则发送访问令牌。

6、客户端验证ID令牌并检索最终⽤户的主题标识符。

结合DEX给出的实现⽅案如下:


token信息

隐式认证返回的token信息中,只包含了access_token和id_token。id_token到期后,需要重新认证。适⽤于认证周期⽐较短的场景。

混合认证

混合流遵循以下步骤:

1、客户端准备⼀个包含所需请求参数的身份验证请求。

2、客户端将请求发送到授权服务器。

3、授权服务器对最终⽤户进⾏身份验证。

4、授权服务器获得最终⽤户同意/授权。

5、授权服务器使⽤授权代码将最终⽤户发送回客户端,并根据响应类型发送⼀个或多个附加参数。

6、客户端使⽤令牌端点的授权代码请求响应。

7、 客户端在响应主体中收到包含ID令牌和访问令牌的响应。

8、 客户端验证ID令牌并检索最终⽤户的主题标识符。

结合DEX给出的实现⽅案如下:

授权码和token

认证完成后,DEX会返回授权码,access_token和id_token。

2、会话管理

会话管理⽤来解决第⼆个问题:⽤户退出登录(主动退出或超时⾃动退出),所有系统资源都不能访问。

⽤户登录后,开始会话,⽤户登出(主动登出或超时⾃动登出)后结束会话,整个会话期间,认为是同⼀个⽤户进⾏操作。

此时会话需要有以下⼏个要求:

  • 每个会话独⽴,所有系统共有同⼀个会话;
  • 不做任何操作时,会话⾃动到期;
  • 访问任意系统时,会话⾃动续期。

redis完美符合。redis中,key的唯⼀性,区分不同的会话,value可以存储会话⾥⾯的数据。redis超时删除机制,符合会话⾃动到期。redis重新设置超时时间,可以实现会话⾃动续期。


二、实现过程

1、搭建DEX认证中⼼

Step 1: 使⽤docker-compose搭建Openldap账号系统,DEX服务端和Redis认证中⼼。


version: "3"
services:
openldap:
image: bitnami/openldap:latest
ports:
- 1389:1389
environment:
- LDAP_ADMIN_USERNAME=admin
- LDAP_ADMIN_PASSWORD=adminpassword
dex:
image: bitnami/dex:latest
ports:
- 5556:5556
- 5557:5557
command:
- serve
- /dex/config.yaml
volumes:
- config.yaml:/dex/config.yaml
redis:
image: redis:latest
ports:
- 6379:6379

Step 2: 启动DEX时需要⽤到的配置⽂件,示例如下:


enablePasswordDB: true
# dex服务地址
issuer: http://localhost:5556/dex
oauth2:
# 可⽤的返回类型
responseTypes: [ "code","token","id_token" ]
skipApprovalScreen: true
staticClients:
- id: app1
name: app1
redirectURIs:
- http://localhost:8080/callback
secret: app1-secret
# trustedPeers表app2⽣成的token可⽤于app1的认证。
trustedPeers:
- app2
- id: app2
name: app2
redirectURIs:
- http://localhost:8081/callback
secret: app2-secret
trustedPeers:
- app1
storage:
type: sqlite3
config:
file: local-example/dex.db
web:
# http 接⼝地址
http: 0.0.0.0:5556
grpc:
# grpc接⼝地址。⽀持通过grpc来扩充dex配置。
addr: 0.0.0.0:5557
# # Server certs. If TLS credentials aren't provided dex will run in
plaintext (HTTP) mode.
# tlsCert: /Users/liujian/work/006-yhplatform/code/yunhang-platformservice/cert/dex-server.crt
# tlsKey: /Users/liujian/work/006-yhplatform/code/yunhang-platformservice/cert/dex-server.key
#
# # Client auth CA.
# tlsClientCA: /Users/liujian/work/006-yhplatform/code/yunhang-platformservice/cert/dex-client.crt
# enable reflection
reflection: true
connectors:
# 指定账号连接器。这⾥配置的是openldap
- type: ldap
name: OpenLDAP
id: ldap
config:
# The following configurations seem to work with OpenLDAP:
#
# 1) Plain LDAP, without TLS:
host: openldap:1389
insecureNoSSL: true
#
# 2) LDAPS without certificate validation:
#host: localhost:636
#insecureNoSSL: false
#insecureSkipVerify: true
#
# 3) LDAPS with certificate validation:
#host: YOUR-HOSTNAME:636
#insecureNoSSL: false
#insecureSkipVerify: false
#rootCAData: 'CERT'
# ...where CERT
=
"$( base64 -w 0 your-cert.crt )"
# This would normally be a read-only user.
bindDN: cn=admin,dc=example,dc=org
bindPW: adminpassword
usernamePrompt: LDAP ⽤户名
userSearch:
baseDN: ou=users,dc=example,dc=org
filter: "(objectClass=person)"
username: cn
# "DN" (case sensitive) is a special attribute name. It
indicates that
# this value should be taken from the entity's DN not an
attribute on
# the entity.
idAttr: DN
emailAttr: mail
nameAttr: cn
groupSearch:
baseDN: ou=Groups,dc=example,dc=org
filter: "(objectClass=groupOfNames)"
userMatchers:
# A user is a member of a group when their DN matches
# the value of a "member" attribute on the group entity.
- userAttr: DN
groupAttr: member
# The group name should be the "cn" value.
nameAttr: cn
# 超时时间设置
expiry:
deviceRequests: "5m"
signingKeys: "6h"
idTokens: "24h"
refreshTokens:
reuseInterval: "30s"
validIfNotUsedFor: "2160h" # 90 days
absoluteLifetime: "3960h" # 165 days

issuer:配置dex的服务地址。

oauth2:配置⽀持的oauth2认证类型。

staticClients:配置可以通过dex进⾏认证的客户端应⽤。这⾥配置了两个应⽤,app1和app2。⼀般情况下,应⽤⽣成的token只能⽤于本应⽤的认证,配置trustedPeers后,可以进⾏跨应⽤资源认证。

storage:DEX的数据存储。DEX需要存储的数据如下:


web:dex认证http服务。

grpc:dex配置修改的grpc服务。

connectors:配置账号连接器。

expiry:配置超时时间

2、登录

流程说明


Step 1:⽤户访问应⽤1前端,应⽤1前端根据路由进⾏鉴权,鉴权不通过跳转到SSO登录⻚⾯(DEX提供);

Step 2:通过LDAP账号进⾏登录,登录成功,回调应⽤1前端的callback⻚⾯,返回Authorization Code;

Step 3:应⽤1前端调⽤login接⼝,传⼊Authorization Code值;

Step 4:应⽤1后端根据Authorization Code从DEX进⾏认证;

Step 5:DEX认证成功,返回AccessToken,RefreshToken,IdToken;

Step 6:应⽤1后端在redis上构建⼀个全局会话(redis中通过随机⽣成的key值sid来表示),将AccessToken,RefreshToken,和IdToken存⼊全局会话,并⽣成应⽤1的局部认证⽅式(这⾥采⽤AccessToken1和RefreshToken1)返回到应⽤1前端;

Step 7:应⽤1前端将RefreshToken1和AccessToken1缓存到Local Storage中;

Step 8:应⽤1前端每次请求接⼝时携带AccessToken1到应⽤1后端;

Step 9:应⽤1后端校验AccessToken1是否有效和AccessToken1中包含的sid全局会话是否有效,当AccessToken1失效时,云航前端调取RefreshToken1接⼝,重新获取AccessToken1;

Step 10:应⽤1前端SSO认证应⽤2前端时,获取会话中的数据sid和全局IdToken传⼊到应⽤2前端;

Step 11:应⽤2调⽤登录接⼝,校验IdToken的有效性和全局会话sid的有效性,校验通过,⽣成应⽤2⾃⼰的认证⽅式⽤于前后端交互;

Step 12:应⽤2前端登录成功,跳转到应⽤2主⻚;

授权码认证示例代码

1、访问登陆页面

curl http://localhost:5556/dex/auth/ldap?
client_id=app1&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcallback&response_t
ype=code&scope=openid+profile+email+federated:id+offline_access+audience:server
:client_id:zadig+audience:server:client_id:app2&state=gHoisYYgsmpc

2、使⽤code获取token

func TestAuthCode(t *testing.T) {ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
defer cancel()
// 连接dex
provider, err := oidc.NewProvider(ctx, "http://localhost:5556/dex")
if err != nil {t.Error(err)
}
oauth2Config := &oauth2.Config{ClientID: "app1",
ClientSecret: "app1-secret",
Endpoint: provider.Endpoint(),
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"openid", "profile", "email", "groups"},
}
// 请求dex的token端点获取token
oauth2Token, err := oauth2Config.Exchange(ctx, authCode)
if err != nil {t.Error(err)
}
rawIDToken, _ = oauth2Token.Extra("id_token").(string)
t.Logf("accessToken:%v", oauth2Token.AccessToken)
t.Logf("refreshToken:%v", oauth2Token.RefreshToken)
t.Logf("idToken:%v", rawIDToken)
}

3、验证idToken

func TestIDToken(t *testing.T) {// 连接dex
provider, err := oidc.NewProvider(ctx, "http://localhost:5556/dex")
if err != nil {t.Error(err)
}
// 验证idToken
idTokenVerifier := provider.Verifier(&oidc.Config{ClientID: "app1"})
idToken, err := idTokenVerifier.Verify(ctx, rawIDToken)
if err != nil {t.Error(err)
}
ac := make(map[string]any)
if err := idToken.Claims(ac); err != nil {t.Error(err)
}
t.Logf("claims:%v", ac)
}

4、创建全局会话,并构建局部会话

func TestSession(t *testing.T){sk := "xxxxx" // base64格式的pem私钥
// ⽣成局部会话
accessToken := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.StandardClaims{Subject: "user1",
ExpiresAt: time.Now().Add(time.Hour).Unix(), // Second
})
accessTokenStr, err := accessToken.SignedString([]byte(sk))
if err != nil {t.Error(err)
}
refreshToken := jwt.NewWithClaims(jwt.SigningMethodHS256,
jwt.StandardClaims{Subject: "user1",
ExpiresAt: time.Now().Add(time.Hour*2).Unix(), // Second
})
refreshTokenStr, err := refreshToken.SignedString([]byte(sk))
if err != nil {t.Error(err)
}
// 构建全局会话
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
defer cancel()
sid := uuid.New().String()
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 0})
rc.Set(ctx, sid, map[string]any{"dex": map[string]any{ // 存储dex信息
"accessToken": "xxxx",
"refreshToken": "xxxx",
"idToken": "xxxx",
},
"local": map[string]string { // 存储局部会话
accessTokenStr: refreshTokenStr,
},
}.Hour)
}

5、局部会话滚动更新

func TestRefreshToken(t *testing.T){sid := "xxxx" //全局会话
refreshTokenLocal := "xxxxx" // 局部会话的refreshToken
refreshTokenDex := "xxxxx" // dex的refreshToken
// 解析局部会话的refreshToken中的jwt.Cl。重新⽣成accessToken
...
// 获取全局会话
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
defer cancel()
sid := uuid.New().String()
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 0})
val := make(map[string]any)
err := rc.Get(ctx, sid).Scan(val)
if err != nil {t.Error(err)
}
// 更新dex的token
oauth2Config := &oauth2.Config{ClientID: "app1",
ClientSecret: "app1-secret",
Endpoint: provider.Endpoint(),
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"openid", "profile", "email", "groups"},
}
oauth2Token, err := oauth2Config.TokenSource(ctx,
&oauth2.Token{RefreshToken: ac.Metadata["refreshToken"]}).Token()
if err != nil {t.Error(err)
}
rawIDToken, _ = oauth2Token.Extra("id_token").(string)
newLocal := val["local"].(map[string]string)
newLocal[newAccessTokenLocal] = newRefreshTokenLocal
// 更新全局会话
rc.Set(ctx, sid, map[string]any{"dex": map[string]any{ // 存储dex信息
"accessToken": oauth2Token.AccessToken,
"refreshToken": oauth2Token.RefreshToken,
"idToken": rawIDToken,
},
"local": newLocal,
}.Hour)
}

3、登出

流程说明

Step1:⽤户主动登出时,调⽤登出接⼝,失效全局会话(删除redis中的sid);

Step2:应⽤1,应⽤2全部不操作时,失效全局会话(redis的超时机制);

Step3:应⽤1或应⽤2进⾏访问时,检测到全局会话已经失效,需要失效本地局部会话。

登出代码示例

删除全局会话

func TestLogout(t *testing.T) {sid := "xxxxx" // 前端传⼊
ctx, cancel := context.WithTimeout(context.TODO(), time.Second*10)
defer cancel()
rc := redis.NewClient(&redis.Options{Addr: "localhost:6379", DB: 0})
rc.Set(ctx, sid, map[string]any{"accessToken": accessTokenStr,
"refreshToken": refreshTokenStr,
}, time.Hour)
}

通过以上操作,就能够实现DEX的单点登录(SSO),解决了⽤户只需要登录⼀次,就能访问所有系统的资源。⽤户退出登录(主动退出或超时⾃动退出),所有系统资源都不能访问。

版权申明:文章由神州数码武汉云基地团队实践整理输出,转载请注明出处。
微信公众号后台回复“技术合集”,可获取更多干货内容!


云原生丨三步教你使用DEX轻松实现单点登录(SSO)相关推荐

  1. 简单三步教你利用VMProtect轻松保护你的代码

    首先我们需要VMProtect.VMProtect是一款虚拟机保护软件,是目前最为流行的保护壳之一.VMProtect将保护后的代码放到虚拟机中运行,这将使分析反编译后的代码和破解变得极为困难.除了代 ...

  2. 思路+步骤+方法,三步教你如何快速构建用户画像?

    思路+步骤+方法,三步教你如何快速构建用户画像? 2016-10-07 超哥 互联网er的早读课 互联网er的早读课 数十万互联网从业者的共同关注! 作者:超哥.作者授权早读课发表,转载请联系作者. ...

  3. 【转】vscode下编译告警“undefined reference”?三步教你如何解决

    转自:vscode下编译告警"undefined reference"?三步教你如何解决_squall0984的博客-CSDN博客 近些年来,由于VS Studio体积庞大.价格昂 ...

  4. 云原生系列三:K8s应用安全加固技术

    今天叶秋学长带领大家学习云原生系列三:10大K8s应用安全加固技术~ 本文译自 Top 10 Kubernetes Application Security Hardening Techniques[ ...

  5. keyshot怎么贴logo_KeyShot图文教程,三步教你如何使用添加有织纹的Logo

    KeyShot图文教程,三步教你如何使用添加有织纹的Logo KeyShot软件是一款功能非常强大的渲染软件,我们可以实现各种效果逼真的渲染效果,同时知晓一定的技巧,我们也可以实现各种想要的小效果,比 ...

  6. 虚拟机服务器 资料安全,绝密:三步教你轻松窃取VMware虚拟机及其数据漏洞预警 -电脑资料...

    是否记得曾经虚拟化过的邮件服务器或者薪酬支付系统?如果拥有访问虚拟化工作环境管理员权限,就可以轻松地进入该虚拟化工作环境,并且窃取所有的数据,而又不会留下任何痕迹, 虚拟化技术可以提供很多物理服务器无 ...

  7. 多个PDF文件如何合并成一个?三步教你搞定

    在日常学习生活中,如果你需要将多个文档整合为一个完整的文件,比如说多篇文章.多张图片.多个表格等等,这时候就需要将这些文档合并成一个PDF文件.如何将多个PDF文件如何合并成一个?三步教你搞定. 步骤 ...

  8. 三步教你用Node做一个微信哄女友(基友)神器,小白可上手

    前言 不知道大家最近有没有被python版的<微信每日说>刷屏呢,他可是霸占了github的python热门快两周了.我们前端的小伙伴是不是也看着有点眼馋呢,因为毕竟是不那么熟悉的pyth ...

  9. 三步教你鉴别iPad阴阳屏

    三步教你鉴别iPad阴阳屏 转自 http://wanke.etao.com/detail/60700.html?spm=1002.1.15.774.hwqziD&t_lm_id=t_wank ...

最新文章

  1. python模拟高并发_Python基于gevent实现高并发代码实例
  2. 【Android 逆向】整体加固脱壳 ( 脱壳点简介 | 修改系统源码进行脱壳 )
  3. java中为什么要用注解_java中的注解,真的很重要,你理解了嘛?
  4. Problem D: 栈的基本运算(栈和队列)
  5. Mac上MacVim安装与配置
  6. 常用JavaScript函数 59 - 70(自我总结)
  7. ubuntu18.04安裝搜狗輸入法
  8. c语言程序由哪三个部分组成部分,C语言程序的组成部分
  9. 项目升级到Delphi 2010总结
  10. TCP编程、UDP编程
  11. mysql 枚举索引_MySQL 索引总结
  12. nacos is starting with cluster
  13. mysql 的一个错误 Error Code: 2013. Lost connection to MySQL server during...
  14. NYOJ - 找点【贪心】
  15. C++ Copy Elision 1
  16. I.Gree的心房(思维题)
  17. 操作体验极度舒适的多功能软件卸载工具 - iObit Uninstaller PRO
  18. 多台电脑/多系统共享键鼠神器(synergy)安装与使用
  19. python坐标系切图_Python写的切图脚本
  20. 光纤光谱仪在气体成分分析上应用

热门文章

  1. 小学计算机课教学工作总结,小学信息技术教学工作总结三篇
  2. Rides持久化问题
  3. ORACLE数据库中使用JAVA源实现SM3签名算法
  4. java入门书籍,linux资料
  5. 牛皮席为什么和床垫不贴合呢?
  6. 牛皮席--夏天新宠!
  7. swing:一种工业界广泛使用的召回算法
  8. 阿里云RDS——产品系列概述
  9. 几步教会你螺母拧出螺栓动画
  10. 2021年年终总结:工作10年宝妈级别的前端开发工程师年终总结