HowTo文档

如何在 XSwitch 中使用 JWT

什么是 JWT

JWT 的全拼是 JSON Web Token,是 Token 认证的一种实现方式。JWT 是一个字符串,由三部分组成,分别为:Header、Payload 和 Signature。

  • Header: header 部分规定了签名算法和令牌类型,比如:
{
  "alg": "HS256",
  "typ": "JWT"
}
  • Payload: payload 部分是有效负载,可以用于存放一些用户信息,比如 username、domain、有效期之类的,由于这部分数据默认未加密处理,只是通过 base64 进行了编码,因此 decode 之后就可以看到真实的数据,因此不要存储一些敏感信息,比如密码之类的信息在里面。

  • Signature: signature 是签名部分,用于验证消息在发送过程中有没有被篡改。签名部分是将经过 base64 编码后的 Header 和 Payload,使用秘钥,通过指定的算法生成哈希。秘钥是不允许被公开的,只保存在特定的服务器上。假设我们使用的是 HMACSHA256 算法,那么 Signature 是通过如下方式创建:

HMACSHA256(
	base64UrlEncode(header) + "." +
	base64UrlEncode(payload),
	secret)

所以 JWT string = Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

在 XSwitch 中使用 JWT

在 XSwitch 中支持 JWT 认证的有XSwitch REST API接口、Verto Login鉴权、SIP呼叫鉴权。

XSwitch REST API

使用XSwitch REST API接口需要 Token 认证,调用 API 接口之前,先需要调用api/sesions接口申请到 Token,例如:

curl -XPOST -d 'login=1007&password=$VeryGoodPassw0rd' 192.168.1.100:8081/api/sessions

返回值例子:

{
  "code": 200,
  "expires": 1666653950,
  "currentAuthority": "user",
  "user_id": 2,
  "extn": "1007",
  "token": "75d54268-f85f-4008-88e2-8bf7cf46bbb9"
}

如上面例子所示,默认生成的 Token 是随机的 UUID 字符串,如果想获取到 JWT Token,需要在配置文件 xtra_config.lua 中开启force_jwt参数,值为true,或者在接口中添加参数jwt,值为true,例如:

curl -XPOST -d 'login=1007&password=$VeryGoodPassw0rd&jwt=true' 192.168.1.100:8081/api/sessions

注意,如果开启了xtra_config.lua中的force_jwt,所有的 Token 将是 JWT Token,因此如果要支持多种格式的 Token,不建议开启此全局参数。

返回值例子:

{
  "code": 200,
  "user_id": 2,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiIyNjU4ZjljYy1hOTg1LTQ4NTktYTUwZC0xODdkYmEwMDkzNGMiLCJleHBpcmVzIjoxNjcxODIyMjI0LCJ1c2VybmFtZSI6IjEwMDciLCJ1c2VyX2lkIjo4LCJsb2dpbiI6IjEwMDdAeHN3aXRjaC5jbiIsInVzZXJfZG9tYWluIjoieHN3aXRjaC5jbiIsImxhc3RfdGltZSI6MTY3MTc2NDYyNH0.0AvgYJikG055xxPDllC58hKriN3TMvLGYYweS35Z3bc",
  "extn": "1007",
  "expires": 1666654041,
  "currentAuthority": "user"
}

可以看到返回值的 Token 格式是XX.YY.ZZ,XX 部分是前面说到的Header(eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9),YY 是Payload(eyJzZXNzaW9uX3V1aWQiOiIyNjU4ZjljYy1hOTg1LTQ4NTktYTUwZC0xODdkYmEwMDkzNGMiLCJleHBpcmVzIjoxNjcxODIyMjI0LCJ1c2VybmFtZSI6IjEwMDciLCJ1c2VyX2lkIjo4LCJsb2dpbiI6IjEwMDdAeHN3aXRjaC5jbiIsInVzZXJfZG9tYWluIjoieHN3aXRjaC5jbiIsImxhc3RfdGltZSI6MTY3MTc2NDYyNH0),ZZ 是Signature(0AvgYJikG055xxPDllC58hKriN3TMvLGYYweS35Z3bc)

Verto Login

Verto Login接口支持账号和密码鉴权,也可以使用 UUID Token 或者 JWT Token 鉴权,如何获取 Token,请参考:XSwitch 认证鉴权接口

例如:

  • 账号密码鉴权例子:
{
  "jsonrpc": "2.0",
  "method": "login",
  "params": {
    "login": "1007@xswitch.cn",
    "passwd": "xxxx",
    "loginParams": {},
    "userVariables": {},
    "sessid": "8faafdd3-dc45-c333-c37d-9997320f354f"
  },
  "id": 1
}
  • UUID Token 鉴权例子:
{
  "jsonrpc": "2.0",
  "method": "login",
  "params": {
    "login": "1007@xswitch.cn",
    "loginParams": {
      "xui_sessid": "75d54268-f85f-4008-88e2-8bf7cf46bbb9"
    },
    "userVariables": {},
    "sessid": "8faafdd3-dc45-c333-c37d-9997320f354f"
  },
  "id": 1
}
  • JWT Token 鉴权例子:
{
  "jsonrpc": "2.0",
  "method": "login",
  "params": {
    "login": "1007@xswitch.cn",
    "loginParams": {
      "xui_sessid": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHRuIjoiMTAwNyIsInVzZXJuYW1lIjoiMTAwNyIsImV4cGlyZXMiOjE2NzE4MjI2OTksImV4dG5faWQiOiI4IiwiZXh0bl9uYW1lIjoiMTAwNyIsImRvbSI6Inhzd2l0Y2guY24iLCJsb2dpbiI6IjEwMDdAeHN3aXRjaC5jbiJ9.vuFr3QlVLM20N5ztZA24VE2Jg0_8DQFLYbeiCOMX86k"
    },
    "userVariables": {},
    "sessid": "8faafdd3-dc45-c333-c37d-9997320f354f"
  },
  "id": 1
}

如果要服务器支持JWT Token,需要修改 XSwitch 的配置文件verto.conf.xml,添加或者修改字段jwt-secret作为私钥,如果强制服务器使用JWT Token,则需要修改 XSwitch 中verto.conf.xml中的xui-auth字段,设置值为jwt <secret><secret>为对应的的私钥,比如jwt secret,注意字符串jwt字符串和秘钥之间需要用空格隔开。同时注意秘钥的安全性,不要外泄。

鉴权流程如下:

SIP 呼叫认证

openSIPS 里引入了auth_jwt模块,提供了 JWT 的认证方式,并提出了传统的Digest Authentication方式存在的一些缺陷。

  • Doing only Authentication not giving us any Authorization

  • Lack of expression

  • MD5 is not secure

  • Subscriber Table sizing problem

  • No safe way for third party auth integration

  • No ability for SSO or Dual-factor authentication

不过大部分的客户端不支持 JWT 的认证方式,我们可以使用 SIPp 进行模拟。uac_jwt.xml 的内容如下:

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE scenario SYSTEM "sipp.dtd">
<scenario name="UAC_invite">


<send retrans="500" start_rtd="invite">
	<![CDATA[

		INVITE sip:[field1]@[remote_ip]:[remote_port] SIP/2.0
		Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
		From: sipp <sip:[field0]@[local_ip]:[local_port]>;tag=[call_number]
		To: sut <sip:[field1]@[remote_ip]:[remote_port]>
		Call-ID: [call_id]
		CSeq: 1 INVITE
		Contact: sip:[field0]@[local_ip]:[local_port]
		Max-Forwards: 70
		Subject: Call Performance Test made by huanglz
		user-agent: SIPp client mode version [sipp_version]
		Content-Type: application/sdp
		Content-Length: [len]
		Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHRuIjoiMTAwNyIsInVzZXJuYW1lIjoiMTAwNyIsImV4cGlyZXMiOjE2NzE4MjI2OTksImV4dG5faWQiOiI4IiwiZXh0bl9uYW1lIjoiMTAwNyIsImRvbSI6Inhzd2l0Y2guY24iLCJsb2dpbiI6IjEwMDdAeHN3aXRjaC5jbiJ9.vuFr3QlVLM20N5ztZA24VE2Jg0_8DQFLYbeiCOMX86k

		v=0
		o=SIPp [pid][call_number] 8[pid][call_number]8 IN IP[local_ip_type] [local_ip]
		s=SIPp Call Test
		c=IN IP[media_ip_type] [media_ip]
		t=0 0
		m=audio [media_port] RTP/AVP 8 0
		a=rtpmap:8 PCMA/8000
		a=rtpmap:0 PCMU/8000
		a=ptime:20
		a=sendrecv

	]]>
	</send>

	<recv response="100" optional="true" rtd="invite">
	</recv>
	<recv response="180" optional="true" rtd="invite" next="normal">
	</recv>
	<recv response="183" optional="true" rtd="invite" next="normal">
	</recv>
	<recv response="403" optional="true" rtd="invite" next="abortcall">
	</recv>
	<recv response="480" optional="true" rtd="invite" next="abortcall">
	</recv>
	<recv response="486" optional="true" rtd="invite" next="abortcall">
	</recv>
	<recv response="500" optional="true" rtd="invite" next="abortcall">
	</recv>
	<recv response="503" optional="true" rtd="invite" next="abortcall">
	</recv>

	<recv response="100" optional="true" rtd="reinvite">
	</recv>
	<recv response="180" optional="true" rtd="reinvite" next="normal">
	</recv>
	<recv response="183" optional="true" rtd="reinvite" next="normal">
	</recv>

	<label id="normal"/>

	<!-- By adding rrs="true" (Record Route Sets), the route sets         -->
	<!-- are saved and used for following messages sent. Useful to test   -->
	<!-- against stateful SIP proxies/B2BUAs.                             -->
	<recv response="200" rtd="reinvite">
	</recv>

	<!-- Packet lost can be simulated in any send/recv message by         -->
	<!-- by adding the 'lost = "10"'. Value can be [1-100] percent.       -->
	<send>
	<![CDATA[

		ACK sip:[field1]@[remote_ip]:[remote_port] SIP/2.0
		Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
		From: sipp <sip:[field0]@[local_ip]:[local_port]>;tag=[call_number]
		To: sut <sip:[field1]@[remote_ip]:[remote_port]>[peer_tag_param]
		Call-ID: [call_id]
		CSeq: 2 ACK
		Contact: sip:[field0]@[local_ip]:[local_port]
		Max-Forwards: 70
		Subject: Call Performance Test made by huanglz
		user-agent: SIPp client mode version [sipp_version]
		Content-Length: 0

	]]>
	</send>
	<pause/>
	<send retrans="500" start_rtd="bye">
	<![CDATA[

		BYE sip:[field1]@[remote_ip]:[remote_port] SIP/2.0
		Via: SIP/2.0/[transport] [local_ip]:[local_port];branch=[branch]
		From: sipp <sip:[field0]@[local_ip]:[local_port]>;tag=[call_number]
		To: sut <sip:[field1]@[remote_ip]:[remote_port]>[peer_tag_param]
		Call-ID: [call_id]
		CSeq: 2 BYE
		Contact: sip:sipp@[local_ip]:[local_port]
		Max-Forwards: 70
		Subject: Performance Test
		Content-Length: 0

	]]>
	</send>

	<recv response="200" crlf="true" rtd="bye">
	</recv>

	<label id="abortcall"/>

	<!-- definition of the response time repartition table (unit is ms)   -->
	<ResponseTimeRepartition value="10, 20, 30, 40, 50, 100, 150, 200"/>

	<!-- definition of the call length repartition table (unit is ms)     -->
	<CallLengthRepartition value="10, 50, 100, 500, 1000, 5000, 10000"/>

</scenario>

call.csv 内容如下:

SEQUENTIAL,,
1007;3000

使用如下命令发起呼叫:

sipp -d 30000 -sf uac_jwt.xml -inf call.csv -i 192.168.1.26 -p 8090 -m 1 -r 1  demo.xswitch.cn:10160  -trace_err

可以看到 SIP 服务器收到如下 INVITE 请求:

recv 1023 bytes from udp/[114.240.222.46]:8090 to udp/[140.143.134.19]:10160 at 2022-10-25 01:53:51.864165:
------------------------------------------------------------------------
INVITE sip:3000@140.143.134.19:10160 SIP/2.0
Via: SIP/2.0/UDP 192.168.1.26:8090;branch=z9hG4bK-10561-1-0
From: sipp <sip:1007@192.168.1.26:8090>;tag=1
To: sut <sip:3000@140.143.134.19:10160>
Call-ID: 1-10561@192.168.1.26
CSeq: 1 INVITE
Contact: sip:1007@192.168.1.26:8090
Max-Forwards: 70
Subject: Call Performance Test made by huanglz
user-agent: SIPp client mode version 3.6.1
Content-Type: application/sdp
Content-Length:   191
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHRuIjoiMTAwNyIsInVzZXJuYW1lIjoiMTAwNyIsImV4cGlyZXMiOjE2NzE4MjI2OTksImV4dG5faWQiOiI4IiwiZXh0bl9uYW1lIjoiMTAwNyIsImRvbSI6Inhzd2l0Y2guY24iLCJsb2dpbiI6IjEwMDdAeHN3aXRjaC5jbiJ9.vuFr3QlVLM20N5ztZA24VE2Jg0_8DQFLYbeiCOMX86k

v=0
o=SIPp 105611 81056118 IN IP4 192.168.1.26
s=SIPp Call Test
c=IN IP4 192.168.1.26
t=0 0
m=audio 6000 RTP/AVP 8 0
a=rtpmap:8 PCMA/8000
a=rtpmap:0 PCMU/8000
a=ptime:20
a=sendrecv

SIP 服务器认成功后,直接回复了 100 Trying,如下:

send 344 bytes from udp/[140.143.134.19]:10160 to udp/[114.240.222.46]:8090 at 2022-10-25 01:53:51.866594:
------------------------------------------------------------------------
SIP/2.0 100 Trying
Via: SIP/2.0/UDP 192.168.1.26:8090;branch=z9hG4bK-10561-1-0;received=114.240.222.46
From: sipp <sip:1007@192.168.1.26:8090>;tag=1
To: sut <sip:3000@140.143.134.19:10160>
Call-ID: 1-10561@192.168.1.26
CSeq: 1 INVITE
User-Agent: FreeSWITCH-mod_sofia/1.10.8-dev+git~20221014T092130Z~84bd6eafda~64bit
Content-Length: 0

注意,上面的 SIP INVITE 消息中,除了保证 SIP 头Authorization有效性外,payload 数据中还需要涵盖username,比如上述 JWT Token,可以在 https://jwt.io 进行 decode,decode 之后的的 payload 数据如下:

{
  "user_domain": "xswitch.cn",
  "expires": 1666691176,
  "last_time": 1666633576,
  "user_id": 2,
  "username": "1007",
  "login": "1007@xswitch.cn",
  "session_uuid": "c5c9c0a1-f94f-41ac-8475-6fe4f64123e0"
}

除了保证username字段外,还需要username和 Invite From 里的user值相同,才能认证通过,否则会收到 403 认证失败的结果。

参考:

  • https://jwt.io/

  • https://www.opensips.org/events/Summit-2020Distributed/assets/presentations/Vlad_Paiu-Opensips%20Jwt%202.pdf

如何在XSwitch中连接其它数据库