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中连接其它数据库