JWT 身份验证
在我年轻的时候(具体有多年轻不在本文讨论范围之内),"token"这个词意味着乐趣。你看,每年我都会有几次机会去街机厅,和朋友们一起玩一些有趣的电子游戏。
如今,token意味着安全。JSON 网络令牌(JWT)身份验证已成为确保 REST API 安全的最流行标准之一。幸运的是,对于 IRIS 用户来说,我们有一种直接的方法来设置应用程序,使其受到这种方式的保护。然而,这种想法仍与我以前玩街机时的想法相似。如果你想玩游戏,就需要获取一些token!
设置
我们的首要任务是为 IRIS 做好准备,使用户能够获取token。首先,我们要在全系统范围内允许使用 JWT 身份验证。为此,我们将打开系统管理门户并登录。然后,我们将进入System Administration > Security > System Security > Authentication\Web Session选项。在同一区域,我们还可以授权其他身份验证替代方案,如 LDAP 和双因素身份验证。在变体列表的底部附近,我们会发现一个名为 "JWT Issuer Field "的字段,在这里我们必须键入一些值来标识token发行者。它可以是任何唯一的字符串,但通常是一个 URL 或域。这应该由应用程序接口和前端开发人员事先商定。您可以选择用户访问 API 时发送请求的 URL。在我的示例中,我将选择 www.myurl.com。
接下来,我们需要在系统管理门户中配置网络应用程序。为此,我们首先要回到System Administration > Security > Applications > Web Applications。然后,我们将选择 REST,并按常规方法设置应用程序的调度类,但这次应勾选下方的 "Use JWT Authentication"复选框。您还可以选择调整访问和刷新令牌的时间限制,但默认值通常已经足够。然后保存应用程序。结果应与下面的截图相似。

完成后,IRIS 会自动为我们的 API 添加几个端点。默认情况下,它们将是 /login、/logout、/refresh 和 /revoke。如果您想自定义它们,可以在调度类中定义一些参数。例如,如果我想在它们前面都加上"/auth",您可以在调度类中包含以下内容:
Parameter TokenLoginEndpoint = "jwtlogin";
Parameter TokenLogoutEndpoint = "jwtlogout";
Parameter TokenRevokeEndpoint = "jwtrevoke";
Parameter TokenRefreshEndpoint = "jwtrefresh";这样就可以修改所有这些端点,在它们的名称前添加 "jwt"。不过,就我们今天的目的而言,我更愿意让它们保持默认状态。
登录
现在,我们要确保这些令牌机器正常工作。您可以像访问其他 API 端点一样访问这些端点。我将举例说明如何使用 ObjectScript 和 %Net.HttpRequest 对象访问登录端点。如果您要从 IRIS 访问此类 API,可以根据自己的需要进行定制:
Class User.JWTTest Extends %RegisteredObject
{
ClassMethod getToken(Output tokenobj) As %Status
{
try{
set myreq = ##class(%Net.HttpRequest).%New()
set myobj = ##class(%Library.DynamicObject).%New()
set myreq.Server = "localhost"
set myreq.Location = "/iris/jwtauth/login"
set myreq.ContentType = "application/json"
do myobj.%Set("user","APIUser")
do myobj.%Set("password","mypassword")
do myobj.%ToJSON(myreq.EntityBody)
do myreq.Post()
set tokenobj = ##class(%Library.DynamicObject).%FromJSON(myreq.HttpResponse.Data)
return $$$OK
}
catch ex{
write ex.DisplayString()
return ex.AsStatus()
}
}
}这里有一个快速故障排除提示:如果您尝试访问登录端点,但一直收到 "404 not found"(404 未找到)的 HTTP 状态,可能是因为您的网络应用程序配置不正确,或者您尝试访问的 URL 不正确。不过,如果你忘记将请求的 ContentType 设置为 application/json,或者试图使用 GET 请求而不是 POST,也会出现 404 错误。
此时,我们可以在下面命名空间的终端会话中运行以下命令:
set sc = ##class(User.JWTTest).getToken(.tokenobj)
我们将得到一个包含登录请求响应的动态对象。如果检查这个对象,你会看到几个属性:
| 访问令牌 | 包含访问令牌字符串 |
| 刷新令牌 | 包含刷新令牌字符串 |
| sub | 是用户,表示token的对象(在本例中,是我们用来登录的用户名)。 |
| iat | 是令牌发出时的 Unix 时间戳。 |
| exp | 令牌过期时间的 Unix 时间戳。 |
因此,我们可以使用 tokenobj.%Get("access_token") 来获取令牌。既然我们已经成功获取了 JWT,那么它到底是什么呢?
JWT token 架构
每个 JSON 网络令牌都是独一无二的,与我年轻时大量生产的假硬币大相径庭。访问令牌是经过编码的,这一点你可能已经从它胡言乱语的外观猜到了。它还分为三部分,中间用句号隔开。这些部分包括标题、有效载荷和签名。前两个部分很容易解码。我们可以通过下面的几个命令来仔细查看我们的终端会话:
Set token = tokenobj.%Get(“acess_token”)
Set tokenheader = $P(token,”.”,1)
Write $SYSTEM.Encryption.Base64Decode(tokenheader)
Set tokenpayload = $P(token,”.”,2)
Write $SYSTEM.Encryption.Base64Decode(tokenpayload)这样做之后,我们就会发现这两个元素都是简单的 base64 编码 JSON 对象。头只有两个字段:包含签名算法的 alg 字段和包含令牌类型的 typ 字段。在这种情况下,我们只使用 JWT 令牌,这一点在这里得到了体现。令牌本身保留了与我们在原始令牌请求响应中看到的相同的签发时间戳、过期时间戳和用户信息。它还包含发行者(应与我们在System Management Portal我们已登录的网络应用程序中输入的发行者字段相匹配)和会话 ID。
令牌的第三部分与其他两部分略有不同。它是根据System Management Portal中的 JWT 签名算法设置加密的签名。它是通过对前两个部分的内容进行散列和加密创建的。服务器收到令牌后,可根据标头和有效载荷重建签名。如果重建的签名与令牌上的签名不一致,服务器就会断定令牌被篡改并拒绝接收。
使用令牌
有了令牌,我们就可以玩游戏了!让我们把调度类变得非常简单:
Class User.JWTAuth Extends %CSP.REST
{
XData UrlMap [ XMLNamespace = "http://www.intersystems.com" ]
{
<Routes>
<Route Url="/test" Method="GET" Call="Test" />
</Routes>
}
ClassMethod Test() As %Status
{
write "Success!"
return $$$OK
}
}要访问这个端点,我们必须先发送一个请求,请求的授权头包含 "Bearer:"和访问令牌的请求。我们将考虑以下方法,并将其放入一个名为 User.JWTTest 的类中:
ClassMethod getTest(Output myreq) As %Status
{
try{
set sc = ##class(User.JWTTest).getToken(.tokenobj)
set myreq = ##class(%Net.HttpRequest).%New()
set myobj = ##class(%Library.DynamicObject).%New()
set myreq.Server = "localhost"
set myreq.Location = "/iris/jwtauth/test"
set myreq.Authorization = "Bearer: "_tokenobj.%Get("access_token")
do myreq.Get()
return $$$OK
}
catch ex{
return ex.AsStatus()
}
}现在我们将在终端会话中调用该方法:
set sc = ##class(User.JWTTest).getTest(.myreq)当我们查看响应时,我们会发现 HTTP 状态是 200 OK,如果我们写出响应的数据,我们会看到 "成功!"。
刷新
最终,我们将不可避免地遇到 "插入令牌以继续(insert tokens to continue)"。这时,我们需要处理下一个端点,即 /refresh。如果你注意了超时设置,就会发现刷新令牌的超时时间总是比访问令牌长。一旦访问令牌过期,就可以使用刷新令牌检索新的访问令牌和新的刷新令牌,在此过程中,旧的访问令牌失效,而不会丢失会话并启动新的会话。使用该端点而不是登录端点的另一个结果是,如果System Management Portal设置了登录事件日志,登录将显示在System Management Portal的审计日志中。由于刷新不是真正的登录,因此不会反映在审计日志中。
要使用刷新端点,我们将发送一个与登录端点非常相似的请求,只是请求正文略有不同:
{
"refresh_token": "(your refresh token goes here",
"grant_type": "refresh_token"
}当我们发送这个请求时,我们会得到一个结构与登录请求完全相同的响应。它始终包含新的访问和刷新令牌。此时,旧的令牌会被注销,所以即使你的刷新令牌尚未过期,你也无法重新使用它。
如果您的刷新令牌已过期,该请求将以未授权错误失败,在这种情况下,您必须重新登录。
退出登录
当我们玩完所有代币并领取奖品后,就该回家了。在这里,我们有两个端点可供选择:刷新和撤销。为什么是两个呢?在设置网络应用程序时,我忘了提及 "按 ID 分组 "字段。虽然根据目前的文档,这个字段不应再使用,但你可能仍会遇到将其设置为某个值的旧版应用程序。所有共享组 ID 的应用程序也会被设置为共享身份验证。这意味着,当你登录或退出其中一个应用程序时,你也会登录或退出所有应用程序。如果指定了组 ID,注销端点将注销共享该组 ID 的所有会话。如果没有指定 ID 组,两个端点的功能都一样(它们会使当前访问令牌和相关刷新令牌失效)。这些端点也需要一个设置了授权头的 POST 请求。不过,它们不需要正文,也不会返回带正文的响应。因为 HTTP 状态是 200 OK,所以你可以知道它们已成功完成任务。
此后,任何使用访问令牌或刷新令牌的尝试都会导致 401 未授权 HTTP 状态代码。
希望这篇文章对你有所帮助,你现在就可以使用这些技巧了。至于我,我现在想玩点大金刚!