自定义接口
1. 概述
SAML 2 是一个标准的 SSO 协议,并受 Windows AD 支持,因此在 SSO 领域有着广泛的应用,但其要求开发者具有一定的背景知识,对接成本较高,因此九数云补充实现了自定义接口。
自定义接口是简化的 SAML 协议,企业客户可在现有 SSO 接口基础上进行修改,按照九数云的要求调用服务和返回参数,并将认证后的用户信息返回给九数云,完成账号关联。
与 SAML 2.0 的差异
对比项 | SAML 2.0 | 自定义接口 |
加密算法 | SHA 系列(SHA-1/256/512) | HMAC-SHA 系列(HS256/HS384/HS512) |
密钥形式 | 基于 X.509 格式的公钥证书 | 普通密钥字符串,最大长度 128 |
传输载体 | XML 安全断言 | JWT Token |
开发难度 | 较高 | 低 |
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.test。
注:若设置了 Issuer URL,则其内容需要与代码中的 ISSUER 保持一致。

2.7 设置登出接口
IdP 登出接口,是指当企业成员访问了九数云的单点登出地址时,九数云不仅会登出当前成员,同时还会将成员重定向至 IdP 并携带登出请求参数。
注:
1)IdP 可以销毁与此成员的会话以实现单点登出的效果。
2)单点登出请求参数格式与认证请求一致,并额外包含 jti 或 nameId 参数,但不包含 state 字段,Token 中的 type 为常量 slo_req。

3. 代码示例
绝大多数编程语言都有较为良好的 JWT 算法实现,第三方库列表可在 jwt.io 中查找。下面给出 Golang 与 Python 的简单示例。
注:代码中的 ACS 地址需替换为九数云单点登录配置页生成的「认证返回地址」;aud / iss 中的平台常量以九数云官方文档为准。
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 Python 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)4. 单点登录方式
企业成员进行单点登录时,可以通过「企业 URL」和「iss 地址」两种方式访问企业:
方式 | 获取位置 | 是否跳转单点登录页 |
企业 URL | 「企业信息 > 基础信息 > 企业账号 URL」处获取
| 会先跳转至单点登录页,点击「登录」后进入企业 |
iss 地址 | 将「认证返回地址」末尾的 acs 后缀改为 iss 例如: 原认证返回地址 https://xxx/sso/custom/XXXXXXXX/acs 修改后的 iss 地址 https://xxx/sso/custom/XXXXXXXX/iss
| 不会跳转,可直接进入企业 |



400-111-0890
在线咨询