با درود خدمت شما عزیزان چندی پیش تصمیم گرفتم از کتابخانه Guardian برای ساخت توکن و همینطور ورود به سایت در الکسیر استفاده کنم به همین ترتیب پستی رو در انجمن زدم که با کمک دوستان عزیزی مثل @toomaj , @lxsameer و مخصوصا دوست محترم @samdvr به سرانجام رسید .
لینک پرسش و پاسخ های مربوط به این کتابخانه
کار Guardian چیست :
یک برنامه نویس الکسیر کاری به نام ueberauth (ممکن اسم شرکت یا گروهشون باشه ) پلاگین های زیادی را برای زبان برنامه نویسی الکسیر و همینطور فریم ورک phoenix تا به حال زده اند که یکی از این دسته پلاگین ها Guardian می باشد که وضیفه ساخت و مدیریت توکن و کدگذاری آن را برعهده می گیرد. که برای اطلاعات بیشتر می توانید به لینک های زیر مراجعه کنید
https://hexdocs.pm/guardian
اگر می خواهید ببنید توکن جی سان چیست از لینک زیر استفاده کنید :
نکته : این آموزش در حال به روز رسانی می باشد و بعد از تکمیل سورس نمونه حتما این پست دوباره بازنگری می گردد
مراحل تنظیم Guardian در elixir
مرحله اول :
قبل از هرچیزی یک پروژه فونیکس بسازید برای آموزش ساخت پروژه جدید phoenix کلیک کنید . بعد از ساخت می توانید به فایل mix.exs
مراجعه کنید . و دو کتابخانه زیر را به آن اضافه نمایید .
defp deps do
[
....
{:guardian, "~> 1.0"},
{:jose, "~> 1.8"},
....
]
end
بعد از اضافه کردن در مسیر جاری پروژه لطفا قرار بگیرید و دستور mix deps.get
را اجرا کنید تا از منبع هر پروژه نسخه مورد نظر دریافت شود . لازم به ذکر هست در زمان ساخت پروژه ما از نسخه های بالا استفاده کردیم شما می توانید با مراجعه به سایت hex.pm آخرین نسخه آن را دریافت کنید .
شاید برای شما سوال شده که پلاگین jose برای چه کاری اضافه شده است . لازم به ذکر هست این کتابخانه برای ساخت و ایجاد کلید خصوصی می باشد که می توانید برای دیدن داکیومنت ساخت کلید خصوصی و همینطور نمونه کلید های خصوصی ساخته شده کلیک کنید
مرحله دوم :
در مرحله دوم ما به سمت این خواهیم رفت که کلید خصوصی ساخته شده را در فایل config.exs
قرار بدهیم پس لازمه کار ما قبل از هرچیزی این است که بیاییم کلید مذکور را بسازیم
برای این کار شما باید وارد ترمینال خود شده (در مسیر جاری پروژه phoenix ) و بعد بیایید کامند زیر را بزنید
iex -S mix phx.server
و بعد از آن لطفا بیایید و کامند زیر را برای ساخت کلید خصوصی ایجاد کنید :
JOSE.JWS.generate_key(%{"alg" => "ES512"}) |> JOSE.JWK.to_map |> elem(1)
لازم به ذکر هست شما می توانید ES512 به رمز های دیگری نیز تغییر دهید مثلا بنده از نسخه ۲۵۶ استفاده کردم .
خوب حالا شما باید بیایید این کلید خصوصی را در فایل کانفیگ به صورت زیر قرار بدهید
config :api_trangell, ApiTrangell.Guardian,
issuer: "api_trangell",
allowed_algos: ["ES512"],
secret_key: %{
"alg" => "ES512",
"crv" => "P-521",
"d" => "5lzq86dlBivEqOmJj_6x_xW5lxY5bCAV1cxJ1Lt8fsshKAkHOz3j0iOF_ruG2PYXF69kDnY3HvSiY2tbog2UjBc",
"kty" => "EC",
"use" => "sig",
"x" => "AIYZCBlSZ4jGvRHJnhWU_s85Uqu6Fl8F7TMMD1WjcibHIGIHfPjEwyfIdmAjvgMwKalqjhKIgqQEejvaPtxHwLMB",
"y" => "AOogDP-U1x4VcgL0xUr7TEXc9FmQv3wvJ_goDW6ZJ_1PBebpOVltZP-3ydG1nAr-ddqoq9AFUQP9UhY3wQhvKFD_"
}
- توجه فرمایید این یک نمونه از رمز می باشد شما برای پروژه خودتان باید از رمزی که در بالا ایجاد کردید استفاده کنید . هرچه کلید کوچک تر باشه موارد موجود در این رمز نیز کوچک تر است .
- شما باید بجای api_trangell اسم پروژه خود را قرار بدهید و همینطور بجای ApiTrangell
# مرحله سوم
حال زمان این هست که یک فایل با نام guardian.ex
به پروژه خود اضافه کنید .
defmodule ApiTrangell.Guardian do
use Guardian, otp_app: :api_trangell
def subject_for_token(resource, _claims) do
# You can use any value for the subject of your token but
# it should be useful in retrieving the resource later, see
# how it being used on `resource_from_claims/1` function.
# A unique `id` is a good subject, a non-unique email address
# is a poor subject.
sub = to_string(resource.id)
{:ok, sub}
end
def subject_for_token(_, _) do
{:error, :reason_for_error}
end
def resource_from_claims(claims) do
# Here we'll look up our resource from the claims, the subject can be
# found in the `"sub"` key. In `above subject_for_token/2` we returned
# the resource id so here we'll rely on that to look it up.
id = claims["sub"]
resource = ApiTrangell.get_resource_by_id(id)
{:ok, resource}
end
def resource_from_claims(_claims) do
{:error, :reason_for_error}
end
end
این فایل شما را با پلاگ گاردین اتصال می دهد و هم چنین در نظر داشته باشید که شما باید اسم پروژه خودتان را جایگزین اسم پروژه نمونه کنید ( ApiTrangell را تغییر دهید )
مرحله چهارم
در این مرحله شما نیاز دارید به فایل api_trangell.ex
بروید و یک فانکشن به آن اضافه کنید که به شرح زیر می باشد و همینطور توجه کنید شما باید به اسم فایل خودتان رفته و اسم بالا یک نمونه می باشد
def get_resource_by_id(id) do
%{id: id, user: "shahryar"}
end
مرحله پنجم
حال زمان این هست که یک فایلی را بسازید تا ارور های جاری را فراخوانی کند که من اسم این فایل را auth_error_handler.ex
قرار دادم
کد های مربوط به این فایل به شرح زیر می باشد :
defmodule ApiTrangell.AuthErrorHandler do
import Plug.Conn
def auth_error(conn, {type, _reason}, _opts) do
body = Poison.encode!(%{message: to_string(type)})
send_resp(conn, 401, body)
end
end
مرحله ششم
در این مرحله که دوباره به آن سر خواهیم زد در حقیقت ما می آییم به روتر این امکان را می دهید قبل از ارسال به فانکشن و فایل مورد نظر یک بررسی روی برخی از موارد امنیتی داشته باشد از جمله هدر درخواست و جلسه کاربری درست شده و …
پس یک فایل با اسم auth_pipeline.ex
می سازیم و بعد از آن به سمت این خواهیم رفت تا کد های زیر را در آن قرار بدهیم
defmodule ApiTrangell.AuthPipeline do
@claims %{typ: "access"}
use Guardian.Plug.Pipeline, otp_app: :api_trangell,
module: ApiTrangell.Guardian,
error_handler: ApiTrangell.AuthErrorHandler
plug Guardian.Plug.VerifySession, claims: @claims
plug Guardian.Plug.VerifyHeader, claims: @claims, realm: "Bearer"
plug Guardian.Plug.EnsureAuthenticated
plug Guardian.Plug.LoadResource, ensure: true
# json VerifyHeader
# plug Guardian.Plug.VerifyHeader, realm: "Bearer"
# plug Guardian.Plug.LoadResource, ensure: true, allow_blank: true
end
اگر به کد های بالا توجه کرده باشید من اومدم یک سری از کد هارو که در حقیقت فراخوانی پلاگ هست را کامنت کردم .تا متوجه بشوید که ما نیاز به همه آن ها در هر جایی نداریم به صورت مثال سشن یا همان جلسه کاربری در اپلیکیشن موبایل به کار نمی رود بستگی به هدف شما دارد /
- در بالای سایت ما یک متغییر گلوبال درست کردیم که در آن یک سری اطلاعات هست که در جلوتر آموزش به آن خواهیم رسید فقط برای شفاف سازی باید بگویم این بخش می تواند یک نشانه گر بر اساس نوع توکن باشد که به وسیله شما تولید می شود در حقیقت یک فیلتری بر ارسال درخواست و همینطور تفکیک آن می باشد .
مرحله هفتم
حال نوبت این هست که روتر و مسیر دهی را آماده کنیم تا به اصل موضوع یعنی کد و فانکشن های مربوطه برسیم پس به فایل روتر رفته و مورد زیر را اضافه کنید
defmodule ApiTrangellWeb.Router do
use ApiTrangellWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
pipeline :unauthorized do
plug :fetch_session
end
pipeline :authorized do
plug :fetch_session
plug ApiTrangell.AuthPipeline
end
scope "/api/users", ApiTrangellWeb do
pipe_through :api
pipe_through :unauthorized
post "/sign-in", PageController, :sign_in
post "/verify-token", PageController, :verify_token
post "/refresh-token", PageController, :refresh_token
end
scope "/api/users", ApiTrangellWeb do
pipe_through :api
pipe_through :authorized
post "/sign-out", PageController, :sign_out
post "/me", PageController, :show
end
end
اگر توجه کرده باشید فایلی که قبلا ساختیم یعنی ApiTrangell.AuthPipeline
را در اینجا اضافه کردیم . و دو نوع pipeline
ایجاد شد که به شرح بالاست یکی با اسم unauthorized که نیازی نمی بیند تا هدر و … چک شود و دیگیری که فایل مذکر در آن اضافه شده است authorized می باشد .
مرحله هشتم
حالا نوبت این هست که به کنترلر مورد نظر خودتان در phoenix رفته و فانکشن های زیر را اضافه کنید لازم به ذکر هست این فانکشن ها به صورت خام درج شدند و شما می توانید به بانک اطلاعاتی خودتان وصل شوید و چک کنید و موارد دیگیری که در پهنای این آموزش نمی گنجد
defmodule Person do
defstruct token: ""
end
defmodule ApiTrangellWeb.PageController do
use ApiTrangellWeb, :controller
def sign_in(conn, %{"password" => password,}) do
user = %{id: "1", user: "shahryar"}
case password do
"2" ->
{:ok, token, _claims} = ApiTrangell.Guardian.encode_and_sign(user, %{some: "claim", userid: 2, admin: 2}, token_type: "access",ttl: {99, :weeks})
json conn, %Person{token: token}
_ ->
conn |> send_resp(204, "")
json conn, %{error: "you have an error"}
end
end
def sign_in(conn, _params) do
send_resp(conn, 401, Poison.encode!(%{error: "Incorrect password"}))
end
def sign_out(conn, _params) do
conn
|> ApiTrangell.Guardian.Plug.sign_out()
|> send_resp(204, "")
end
def verify_token(conn, %{"token" => token}) do
case ApiTrangell.Guardian.decode_and_verify(token) do
{:ok, claims} ->
json conn, %Person{token: Map.get(claims, "exp")}
{:error, _} ->
conn
|> put_status(403)
|> json(%{ error: "khata"})
end
end
def refresh_token(conn, %{"token" => token}) do
case ApiTrangell.Guardian.refresh(token) do
{:ok, {old_token, old_claims}, {new_token, new_claims}} ->
json conn, %{
old_token: old_token,
old_claims: old_claims,
new_token: new_token,
new_claims: new_claims
}
{:error, %CaseClauseError{term: {:error, {:badmatch, false}}}} ->
conn
|> put_status(404)
|> json(%{ error: "errorrrrrr"})
end
end
def current_user(conn) do
Guardian.Plug.current_resource(conn)
end
end
سعی کردم اسم هر فانکشن را به طوری نام گذاری کنم تا شما به سادگی از آن به درک مناسبی برسید و نیاز به توضیح هر فانکشن به صورت جدا نباشد
امید وارم این آموزش مفید واقع شود
باز هم از دوستانی که در اول پست هست تشکر می کنم بدون وجود این دوستان این قسمت خیلی زمان بر می شد و شاید هم به خوبی درست نمی گردید.
سورس کامل این پروژه :
ارسال درخواست :
برای دریافت توکن باید به لینک زیر درخواست POST ارسال کنید . نیاز به ارسال یک پرامتر دارید : password با ارزش استرینگ “2”
http://localhost:4000/api/users/sign-in
برای از بین بردن در سشن می تونید به لینک زیر درخواست بزنید . لازم به ذکر هست شما باید در هدر درخواست POST خودتان بیایید Authorization با ارزش Bearer TOKEN
ارسال کنید . TOKEN رو باید با توکنی که در لینک بالا دریافت کردید تغییر بدهید
http://localhost:4000/api/users/sign-out
شما می توانید در لینک زیر توکن خودتان را بررسی کنید ببنید ورفای هست یا خیر ؟ برای این کار نیاز دارید شما یک درخواست POST بفرستید که پرامتر token
داشته باشه و ارزشش نیز همان توکنی که دریافت کردید باشد
http://localhost:4000/api/users/refresh-token
پیوست
نحوه کار کرد jwt
سطوح دسترسی
گاردین سطوح دسترسی هم در امکانات خودش قرار داده به صورت مثال شما می آیید در پروژه خودتان یک فایل می سازید به شرح زیر
defmodule ApiTrangell.Auth.Token do
use Guardian, otp_app: :api_trangell,
permissions: %{
default: [:public_profile, :user_about_me],
user_actions: %{
books: 0b1,
fitness: 0b100,
music: 0b1000,
}
}
use Guardian.Permissions.Bitwise
# snip
def build_claims(claims, _resource, opts) do
claims =
claims
|> encode_permissions_into_claims!(Keyword.get(opts, :permissions))
{:ok, claims}
end
end
خوب شما در این بخش اومدید سطح دسترسی به خصوصی معرفی کردید حالا نوبت این هست که بیایید در توکن دسترسی خودتان را قرار دهید
{:ok, token, claims} = ApiTrangell.Guardian.encode_and_sign(user, %{some: "claim", userid: 2, admin: 2, pem: %{default: [:public_profile], user_actions: [:books]}}, token_type: "access",ttl: {99, :weeks})
اگر توجه کرده باشید من گروه کاربری رو در public_profile و همینطور گفتم یوزر اکشن اون books باشه حالا می خواهیم این مورد رو ولید کنیم :
claims |> ApiTrangell.Auth.Token.decode_permissions_from_claims |> ApiTrangell.Auth.Token.all_permissions?(%{default: [:public_profile], user_actions: [:books]})
اگر در توکن هم books باشه به شما جواب true بر می گردوند و اگر نباشه false
به روز رسانی
نکته: در صورتی که در حال نصب این پلاگین روی پروژه الکسیر (غیر فونیکس) می باشید لطفا Plug را در فایل mix خود قرار دهید و اگر باز هم گاردین را شنایی نکرد یک بار deps را پاک کرده و بیلد را حذف نمایید و دوباره نصب نمایید.