自定义接口配置

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来完成自定义接口的配置:

文档内容是否对您有帮助?
有帮助
没帮助没帮助
如需获取即时帮助,请联系技术支持
咨询
扫码领取100+零代码资料简道云官方微信号400-111-0890
图标在线咨询
立即体验