自定义接口配置
1. 功能简介
SAML 2 是一个标准的 SSO 协议, 并受 Windows AD 支持, 因此在 SSO 领域有着广泛的应用,但其要求开发者具有一定的背景知识(建议阅读官方文档),对接成本较高, 因此我们补充实现了自定义接口。
自定义接口是简化的 SAML 协议,企业客户可在现有 SSO 接口基础上进行修改,按照简道云的要求调用服务和返回参数,并将认证后的用户信息返回给简道云,完成账号关联。
2. 设置步骤
2.1 设置入口
1)登录 简道云账号,进入「企业管理 >> 管理工具 >> 企业设置」页面中。
2)在「企业安全 >> 单点登录」处,打开单点登录的开关,并点击「配置」按钮。
2.2 选择配置方式
进入配置单点登录详情页中,选择单点登录配置方式为「自定义接口」。如下所示:
2.3 设置Idp登录接口
Idp 登录接口,指的是通过开发人员部署所需的基础内容,允许用户登录系统的接口。通常用于需要验证用户身份并且允许用户访问系统的场景当中。详情可参见文档:Idp 配置说明。
在简道云的单点登录中,您可以根据企业自身的服务器配置,设置登录接口:
2.4 生成认证密钥
认证密钥指的是信息的发送方和接收方,需要通过一个密钥去加密和解密数据。在自定义接口当中,用户可以自定义设置认证密钥,也可以点击「生成密钥」按钮,直接生成一串认证密钥。如下所示:
注:认证密钥需要与代码中的 SECRET 保持一致。
2.5 选择认证加密算法
简道云的单点登录中,支持以下 3 种加密算法,您可以根据自己的需求来选择合适的加密算法,从而完成单点登录配置:
- HS256
- HS384
- HS512
2.6 设置Issuer URL
Issuer URL 用于验证请求内容与服务后台是否能够匹配成功,若匹配成功则可以进行解析,否则将请求失败。您可自定义 Issuer URL 的内容。如设置为: Issuer.test。
注:若设置了 Issuer URL,则 Issuer URL 中的内容需要与代码中的 Issuer 保持一致。
2.7 设置登出接口
Idp 登出接口,是指当企业成员访问了简道云的单点登出地址时,简道云不仅会登出当前成员,同时还会将成员重定向至 Idp 并携带登出请求参数。
注:
1)IdP 可以销毁与此成员的会话以实现单点登出的效果。
2)单点登出请求参数格式与认证请求参数一致, 并额外包含 jti 或 nameId 参数, 但不包含 state 字段, 在 Token 中的 type 为常量「slo_req」。
3. 代码示例
绝大多数编程语言都有较为良好的 JWT 算法实现, 第三方库列表可在这个页面中查找,下面给出以 Python 代码和 Golang 代码 实现 IdP 配置 的简单示例:
3.1 Golang Demo
package main
import (
"fmt"
"github.com/dgrijalva/jwt-go"
"log"
"net/http"
"time"
)
const (
acs = "https://www.jiandaoyun.com/sso/custom/5cd91fe50e42834f41b7c6ef/acs"
issuer = "com.example"
username = "angelmsger"
secret = "jdy"
)
func ValidBody(body jwt.MapClaims) bool {
return body["iss"] == "com.jiandaoyun" && body["aud"] == issuer && body["type"] == "sso_req"
}
func ValidToken(query string) bool {
token, err := jwt.Parse(query, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected Signing Method: %v ", token.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return false
}
claims, ok := token.Claims.(jwt.MapClaims)
return ok && token.Valid && ValidBody(claims)
}
func GetTokenByUsername(username string) (string, error) {
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"type": "sso_res",
"username": username,
"iss": issuer,
"aud": "com.jiandaoyun",
"nbf": now.Unix(),
"iat": now.Unix(),
"exp": now.Add(1 * time.Minute).Unix(),
})
return token.SignedString([]byte(secret))
}
func BuildResponseUri(token string, state string) string {
target := acs + "?response=" + token
if state != "" {
target += "&state=" + state
}
return target
}
func handler(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
reqToken := query.Get("request")
if ok := ValidToken(reqToken); ok {
if resToken, err := GetTokenByUsername(username); err == nil {
target := BuildResponseUri(resToken, query.Get("state"))
http.Redirect(w, r, target, http.StatusSeeOther)
}
w.WriteHeader(404)
}
w.WriteHeader(404)
}
func main() {
http.HandleFunc("/sso", handler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
3.2 Pyhton Demo
from datetime import datetime, timedelta
from flask import Flask, abort, redirect, request
import jwt
from jwt import InvalidTokenError
class Const:
ACS = 'https://www.jiandaoyun.com/sso/custom/5cd91fe50e42834f41b7c6ef/acs'
SECRET = 'jdy'
ISSUER = 'com.example'
USERNAME = 'angelmsger'
app = Flask(__name__)
def valid_token(query):
try:
token = jwt.decode(
query, Const.SECRET,
audience=Const.ISSUER,
issuer='com.jiandaoyun'
)
return token.get('type') == 'sso_req'
except InvalidTokenError:
return False
def get_token_from_username(username):
now = datetime.utcnow()
return jwt.encode({
"type": "sso_res",
'username': username,
'iss': Const.ISSUER,
"aud": "com.jiandaoyun",
"nbf": now,
"iat": now,
"exp": now + timedelta(seconds=60),
}, Const.SECRET, algorithm='HS256').decode('utf-8')
@app.route('/sso', methods=['GET'])
def handler():
query = request.args.get('request', default='')
state = request.args.get('state')
if valid_token(query):
token = get_token_from_username(Const.USERNAME)
stateQuery = "" if not state else f"&state={state}"
return redirect(f'{Const.ACS}?response={token}{stateQuery}')
else:
return abort(404)
if __name__ == '__main__':
app.run(port=8080)
3.3 其它Demo
下面给出其它编程语言对应的代码 Demo,您可通过修改Demo来完成自定义接口的配置:
4. 单点登录方式
企业成员进行单点登录时,可以通过「企业 URL 」和「 iss 地址」两种方式访问企业,两种方式详情如下:
4.1 通过「企业 URL」登录
企业 URL 地址指的是在「企业信息 >> 基础信息 >> 企业账号 URL」处,企业根据自身需求设置的 URL 地址。成员通过企业 URL 进行登录时,会先跳转至单点登录的页面,点击「登录」后即可进入企业。
4.2 通过「iss 地址」登录
iss 地址指的是在「企业设置 >> 企业安全 >> 单点登录 >> 认证地址返回」处,将认证返回地址复制后,修改 acs 后缀为 iss。如下所示:
https://www.jiandaoyun.com/sso/custom/XXXXXXXX/acs //原认证返回地址
https://www.jiandaoyun.com/sso/custom/XXXXXXXX/iss //修改后的 iss 地址
企业成员通过 iss 地址进行登录时,不会跳转至单点登录的页面,可以直接进入企业。