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