基本需求
我目前存在两个主力web服务运行
- 国内的服务 帐号主数据库在这里 同步到国外
- 国外的服务 博客主数据库在这里 同步到国内
我希望为自己的博客增加一个添加博文的功能,但是问题来了,我希望复用国内的帐号体系,虽然数据已经同步过来了,但是自己再去做hash验证,再去签发token是不是有点麻烦呢?能不能直接复用国内服务的token呢,国外只做校验。 jwt是可以这样用的,只要用于签发token的secret相同就可以了。
那么问题就转变为了,如何使用elixir phoenix框架去校验jwt,获取claims。
实现过程
使用joken
elixir存在签发和验证jwt的lib,名为joken,我们将其添加到mix.exs中。
defp deps do
...
{:joken, "~> 1.1"}
...
end
然后执行命令安装依赖
mix deps.get
这样,joken就添加到了phoenix项目中。
配置签发密钥
作为密钥,是不太方便直接写入配置文件里的,这里选择了环境变量的方式。
但是在开发环境又想用config/dev.exs文件来写开发环境配置,所以在参照了phoenix数据库的配置之后,作出如下代码变更。
- 于
config/dev.exs文件:# Joken jwt sign token config :joken, default_signer: "test" - 于
config/runtime.exs文件:joken_secret = System.get_env("JOKEN_SECRET") || raise """ environment variable JOKEN_SECRET is missing. """ # Joken jwt sign token config :joken, default_signer: joken_secret
这样,在开发的时候,我们可以直接使用config/dev.exs中配置的文本密钥,在程序运行的时候,则是读取环境中的JOKEN_SECRET变量来进行jwt校验与解密。
注入joken到web服务模块
根据joken的文档,它其实是可以单独调用的,不过更合适的方案是将其作为一个web模块独立起来。具体用法可以参考hexdocs。
我们这里新建lib/be_web/token.ex,内容如下
defmodule BeWeb.Token do
use Joken.Config
end
这样就可以用BeWeb.Token去调用了。
实现鉴权中间件
接下来是业务逻辑的部分,新建lib/be_web/jwt_auth.ex文件,在其中实现对jwt内容的解析。
defmodule BeWeb.JwtAuth do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
case conn |> get_req_header("token") do
[token_value] ->
case BeWeb.Token.verify_and_validate(token_value) do
{:ok, claims} ->
exp = claims["exp"]
# 判断JWT是否已过期
current_time = Timex.now() |> Timex.to_unix()
if current_time > exp do
conn
|> send_resp(401, "Token has expired.")
|> halt()
else
conn
|> assign(:user, claims["user"])
end
{:error, _} ->
conn
|> send_resp(401, "Invalid or expired token.")
|> halt()
end
_ ->
conn
|> send_resp(401, "Authorization token not provided.")
|> halt()
end
end
end
其中大致完成了如下内容:
- 从请求头中获取名为
token字段的值,若不存在,则返回401 - 调用的
BeWeb.Token.verify_and_validate函数,若校验失败,则返回401 - 对比
claims['exp']与当前时间戳,若过期,则返回401 - 将
claims['user']中的用户信息存入当前请求的上下文。
至此中间件就实现了,但感觉3、4这里有些粗糙,是否要增加对exp与user是否有效的校验呢?
为路由增加鉴权
事情到了这里,就变简单了许多。首先开放原先被我注释掉的:update,:delete,:create路由,修改lib/be_web/router.ex文件
scope "/api", BeWeb do
pipe_through :api
# 修改这一行
resources "/posts", PostController, only: [:index, :show, :update, :delete, :create]
end
指定增删改接口需要经过中间件的处理,修改lib/be_web/controllers/post_controller.ex文件。
alias Be.Blog
alias Be.Blog.Post
# 增加这一行
plug(BeWeb.JwtAuth when action in [:create, :update, :delete])
action_fallback BeWeb.FallbackController
结论与反思
至此,基于jwt的分布式鉴权校验就算完成了,就算是在国内的golang服务签发了一个jwt令牌,只要使用的是同一个签发密钥,在国外的elixir服务仍然可以进行权限鉴定。
但是,密钥到处复制粘贴并不是什么好习惯,因为选用了对称算法去做令牌的签发,所以要把密钥对两个服务器同步,这点显然并不安全。对此,可以使用非对称的算法去做令牌签发,私钥放在签发服务,而公钥放在校验服务进行分发。如此一来,就可以保证签发密钥不需要转移了。(这似乎是私钥加密的一个典例)。
参考与引用
致谢
感谢ChatGPT,帮助我解决了大量基础知识的问题。