如何使用 Yesod 正确处理 JWT 的到期日期?

Posted

技术标签:

【中文标题】如何使用 Yesod 正确处理 JWT 的到期日期?【英文标题】:How to properly handle the expiration date of a JWT with Yesod? 【发布时间】:2018-07-28 12:24:30 【问题描述】:

我目前正在为一个课程项目使用 Yesod 框架开发一个 Web 服务器。我是 Haskell 的新手,我着迷于它与我所知道的有关编程语言的所有其他事物的不同之处。然而,并不是所有的玫瑰。有时我会卡住几天,这个问题就是关于这样一个案例的。

这些是验证请求附带的不记名令牌的函数:

isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> IO (Maybe Bool)
isDateExpired exptime currtime = return $ (<) <$> exptime <*> currtime

validateToken :: Handler AuthResult
validateToken = do
    bearerToken <- lookupBearerAuth
    master <- getYesod
    when (isNothing bearerToken) $ permissionDenied "Token not present in headers."
    let decodedAndVerified  = join $ JWT.decodeAndVerifySignature (JWT.secret (clientSecret master)) <$> bearerToken
        claimset            = JWT.claims <$> decodedAndVerified
        audience            = join $ JWT.aud <$> claimset
        iss = join $ JWT.iss <$> claimset
        expiration = join $ JWT.exp <$> claimset
    case audience of
        Just a -> do
            case a of
                Left uniqueAud -> do 
                    when (Just uniqueAud /= JWT.stringOrURI (clientId master)) $ permissionDenied "Invalid aud."
                Right _ -> permissionDenied "Tokens with multiple aud values not currently supported."
        _ -> permissionDenied "Audience not defined."
    when (iss /= JWT.stringOrURI (configIssuer master)) $
        permissionDenied "Invalid issuer."
    when (isNothing claimset) $
        permissionDenied "Claimset invalid."
    let mExpired = JWT.numericDate <$> getPOSIXTime >>= isDateExpired expiration
    --FIXME Currently, this next part has to be at the end of the function.
    liftIO $ mExpired >>= 
        \y -> if isNothing y then return $ Unauthorized "Expiration date missing." 
            else if y==Just True then return $ Unauthorized "Invalid expiration date."
                else return $ Authorized

好吧,这段代码确实有效。它正确地验证了令牌。但是,正如您在 FIXME 中看到的那样,validateToken 函数的最后一部分非常hacky。它必须是最后一行,这让我很恼火。

根据我收集到的信息,处理此问题的正确方法是使用when,就像在上面的案例中所做的那样。问题是,我希望这里有人可以阐明,在验证到期日期时,我最终在 mExpired 变量中得到了一个 IO (Maybe Bool)。而when 不接受。

我想做的是(在伪 Haskell 中)这样的事情:

when (isNothing mExpired || mExpired == Just True) $ permissionDenied "Invalid expiration date."

然后我可以在此之后检查其他内容,并在函数的末尾输入Authorized,一切都正确且美观。

这样的事情可能吗?

仅供参考:permissionDenied 的类型是 Failure ErrorResponse m =&gt; String -&gt; m a

【问题讨论】:

旁注:在您的用例中,currtime in isDateExpiredNothing 意味着在 JWT.numericDate 中存在解析失败,您最终会将此案例与到期日期合并-失踪失败。在这种情况下,这并不重要(据我所知,getPOSIXTime 不能为此目的产生无效的日期),但最好注意这种情况,在这种情况下,某些事情可能会溜走裂缝。 【参考方案1】:

让我们再看看isDateExpired

isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> IO (Maybe Bool)
isDateExpired exptime currtime = return $ (<) <$> exptime <*> currtime

这实际上是一个纯函数:(&lt;$&gt;)(&lt;*&gt;) 这里是 Maybe 函子,结果在 IO 中只是因为末尾有 return。所以让我们摆脱它:

isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> Maybe Bool
isDateExpired exptime currtime = (<) <$> exptime <*> currtime

这让我们的观点清晰了很多;我们可以单独处理如何将其输入IO

鉴于isDateExpired 现在是一个纯函数,我们不再需要mExpired 中的(&gt;&gt;=)

    let mExpired = isDateExpired expiration . JWT.numericDate <$> getPOSIXTime

mExpired 仍然是IO (Maybe Bool),感谢getPOSIXTime。我们可以通过使用&lt;-(和liftIO)而不是let来改变它:

    mExpired <- liftIO $ isDateExpired expiration . JWT.numericDate <$> getPOSIXTime

(请注意,您做了几乎相同的事情。我将liftIO 与其余部分放在一起,这样我就不必为IO (Maybe Bool) 中间值想一个多余的名称,它是大部分是无趣的。)

决定下一步做什么的最直接的方法是在mExpired上进行模式匹配:

    case mExpired of
        Nothing -> permissionDenied "Expiration date missing."
        Just expired -> when expired $ permissionDenied "Invalid expiration date."

模式匹配往往比布尔测试更容易使用,除非您手中已经有了Bool。 (一个推论是,通常有一个更好的替代方法来使用 isJustisNothing ——尽管我觉得你在 do-block 的其他地方使用 isNothingwhen 很好。)


上面的重构假设您想要区分缺少日期的情况和无效的日期情况。尽管我怀疑这实际上是您想要/需要的,但让我们暂时假设您宁愿忽略差异(因此以相同的方式处理 NothingJust False)。来自Data.MaybefromMaybe 允许您以非常方便的方式做到这一点:

fromMaybe :: a -> Maybe a -> a
    when (fromMaybe True mExpired) $ permissionDenied "Invalid expiration date."

这相当于您的“伪Haskell”行——实际上,我们提供True 作为mExpired 的默认值。如果您要沿着这条路线走,您甚至可以将fromMaybe True 移动到isExpired,这样一开始就会产生Bool

另一个值得一提的函数是maybe,相当于Maybe打包成函数的案例分析:

maybe :: b -> (a -> b) -> Maybe a -> b

使用它,我上面写了几行的case-statement可以替换为:

    -- Line breaks added for clarity.
    maybe
        (permissionDenied "Expiration date missing.")
        (\expired -> when expired $ permissionDenied "Invalid expiration date.")
        mExpired

虽然在这种情况下,它的可读性可能不如 case 语句,但maybe 是一个您可以编写、部分应用等的函数;在其他情况下可以利用。

(一个小谜题:如果你用maybe来定义Maybe(&gt;&gt;=) 会是什么样子?)

【讨论】:

多么迷人的语言!非常感谢您的回答。我仍在为 monad 之类的整个概念而苦苦挣扎,但这只会让学习变得更有趣!我在您的回答中无法理解的一件事是 let mExpired = isDateExpired expiration . JWT.numericDate &lt;$&gt; getPOSIXTime 的工作原理。如果当前时间包裹在 IO 中,如何将其作为参数传递给 isDateExpired?关于你的谜题,我的猜测是:maybe (Nothing) (\v -&gt; ...) (k)。我接近了吗? @LéoVital (1) (.) 的优先级高于(&lt;$&gt;),因此等于(isDateExpired expiration . JWT.numericDate) &lt;$&gt; getPOSIXTime(也就是说,我们也是fmapping isDateExpired expiration。( 2) 是的,就是这样。顺便说一句,在(=&lt;&lt;)(与翻转参数绑定)方面看起来会更好——(=&lt;&lt;) = maybe Nothing

以上是关于如何使用 Yesod 正确处理 JWT 的到期日期?的主要内容,如果未能解决你的问题,请参考以下文章

使用 passport-jwt 进行 JWT 身份验证不考虑到期日期

如何使用 MQTT 处理 JWT 撤销

如何在 Javascript 中创建 JWT exp 样式日期

如何在 jwt laravel 5.3 中处理令牌到期?

JWT 令牌和刷新令牌的合理到期日期是啥?

我们是不是应该在很大程度上基于本地 jwt 令牌到期日期来验证用户