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