如何使用 Yesod 正确处理 JWT 的到期日期?
How to properly handle the expiration date of a JWT with Yesod?
我目前正在使用 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
函数的最后一部分非常复杂。它必须是最后一行,这让我很烦。
根据我收集到的信息,处理此问题的正确方法是使用 when
,就像在上面的案例中所做的那样。这个问题,我希望这里有人可以阐明,是在验证到期日期时,我最终在那个 mExpired
变量中得到一个 IO (Maybe Bool)
。 when
不接受。
我想做的(伪Haskell)是这样的:
when (isNothing mExpired || mExpired == Just True) $ permissionDenied "Invalid expiration date."
然后我可以检查其他东西,在函数的末尾输入 Authorized
,一切都是正确和漂亮的。
这样的事情有可能吗?
仅供参考:permissionDenied
的类型是 Failure ErrorResponse m => String -> m a
再看一下isDateExpired
:
isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> IO (Maybe Bool)
isDateExpired exptime currtime = return $ (<) <$> exptime <*> currtime
这实际上是一个纯函数:(<$>)
和 (<*>)
这里是 Maybe
函子的那些,结果在 IO
中只是因为 return
最后。所以让我们摆脱它:
isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> Maybe Bool
isDateExpired exptime currtime = (<) <$> exptime <*> currtime
这使我们的观点更加清晰;我们可以单独处理如何将其放入 IO
。
鉴于 isDateExpired
现在是一个纯函数,我们不再需要 mExpired
中的 (>>=)
:
let mExpired = isDateExpired expiration . JWT.numericDate <$> getPOSIXTime
mExpired
仍然是 IO (Maybe Bool)
,感谢 getPOSIXTime
。我们可以通过使用 <-
(和 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
。 (一个推论是,通常有更好的替代方法来使用 isJust
和 isNothing
——尽管我觉得你在 [=89] 的其他地方使用 isNothing
和 when
=]很好。)
上面的重构假定您想要区分缺失日期案例和无效日期案例。虽然我怀疑这实际上是您 want/need,但让我们暂时假设您宁愿忽略差异(因此以相同的方式处理 Nothing
和 Just False
)。来自 Data.Maybe
的 fromMaybe
允许您以一种非常方便的方式做到这一点:
fromMaybe :: a -> Maybe a -> a
when (fromMaybe True mExpired) $ permissionDenied "Invalid expiration date."
这相当于您的 "pseudo-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-statement,但 maybe
是一个可以组合、部分应用等的函数;在其他情况下可以利用它。
(一个小谜题:(>>=)
for Maybe
如果你用 maybe
来定义它会是什么样子?)
我目前正在使用 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
函数的最后一部分非常复杂。它必须是最后一行,这让我很烦。
根据我收集到的信息,处理此问题的正确方法是使用 when
,就像在上面的案例中所做的那样。这个问题,我希望这里有人可以阐明,是在验证到期日期时,我最终在那个 mExpired
变量中得到一个 IO (Maybe Bool)
。 when
不接受。
我想做的(伪Haskell)是这样的:
when (isNothing mExpired || mExpired == Just True) $ permissionDenied "Invalid expiration date."
然后我可以检查其他东西,在函数的末尾输入 Authorized
,一切都是正确和漂亮的。
这样的事情有可能吗?
仅供参考:permissionDenied
的类型是 Failure ErrorResponse m => String -> m a
再看一下isDateExpired
:
isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> IO (Maybe Bool)
isDateExpired exptime currtime = return $ (<) <$> exptime <*> currtime
这实际上是一个纯函数:(<$>)
和 (<*>)
这里是 Maybe
函子的那些,结果在 IO
中只是因为 return
最后。所以让我们摆脱它:
isDateExpired :: Maybe JWT.NumericDate -> Maybe JWT.NumericDate -> Maybe Bool
isDateExpired exptime currtime = (<) <$> exptime <*> currtime
这使我们的观点更加清晰;我们可以单独处理如何将其放入 IO
。
鉴于 isDateExpired
现在是一个纯函数,我们不再需要 mExpired
中的 (>>=)
:
let mExpired = isDateExpired expiration . JWT.numericDate <$> getPOSIXTime
mExpired
仍然是 IO (Maybe Bool)
,感谢 getPOSIXTime
。我们可以通过使用 <-
(和 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
。 (一个推论是,通常有更好的替代方法来使用 isJust
和 isNothing
——尽管我觉得你在 [=89] 的其他地方使用 isNothing
和 when
=]很好。)
上面的重构假定您想要区分缺失日期案例和无效日期案例。虽然我怀疑这实际上是您 want/need,但让我们暂时假设您宁愿忽略差异(因此以相同的方式处理 Nothing
和 Just False
)。来自 Data.Maybe
的 fromMaybe
允许您以一种非常方便的方式做到这一点:
fromMaybe :: a -> Maybe a -> a
when (fromMaybe True mExpired) $ permissionDenied "Invalid expiration date."
这相当于您的 "pseudo-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-statement,但 maybe
是一个可以组合、部分应用等的函数;在其他情况下可以利用它。
(一个小谜题:(>>=)
for Maybe
如果你用 maybe
来定义它会是什么样子?)