自定义接口

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

不会跳转,可直接进入企业

咨询
扫码领取100+零代码资料简道云官方微信号400-111-0890
图标在线咨询
立即体验