JWT 構(gòu)建Rails API 授權(quán)登錄

移動(dòng)應(yīng)用開發(fā)中蹄梢,令牌授權(quán)(token-based) 是一種常用的移動(dòng)端與服務(wù)端的授權(quán)登錄方式 稽亏,但是使用它壶冒,需要面臨著一些問題,如:令牌的過期時(shí)間截歉,令牌狀態(tài)在服務(wù)器端的維護(hù)胖腾,服務(wù)端多子系統(tǒng)同步等問題。本文要說到的JWT(JSON Web Token) 輕量級(jí)的驗(yàn)證規(guī)范,就是一種非常好的解決方案咸作。

JWT

在JWT的規(guī)范定義中锨阿,它由頭部,載荷和簽名记罚,三部分字符串組成其中前兩部分是用JSON對(duì)象進(jìn)過Base64編碼而來的群井。

頭部 是由typ和alg兩部分組成,typ 表示自己是一個(gè)JWT毫胜,alg表示簽名使用了什么算法。

{
  "typ": "JWT",
  "alg": "HS256"
}

經(jīng)過base64編碼后的結(jié)果就是:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

載荷 是JWT中真正承載用戶信息的部分诬辈,它也是一個(gè)json對(duì)象酵使,由自定義部分和規(guī)范定義部分組成

JWT規(guī)范定義 中描述的幾個(gè)可選的信息

{
    "iss": "JWT-Rails-Server", // 簽發(fā)者
    "aud": "www.baidu.com", // 接收者
    "iat": 1472263256, // JWT 簽發(fā)的時(shí)間
    "exp": 1472522525, // 過期時(shí)間
    "sub": "jwt@baidu.com" // JWT對(duì)應(yīng)的用戶 
    "user_id": 1211 // 自定義
}

我們還可以在上面的JSON中添加我們自定義的部分。

最后載荷也是需要通過Base64進(jìn)行編碼的:

eyJpc3MiOiJKV1QtUmFpbHMtU2VydmVyIiwiYXVkIjoid3d3LmJhaWR1LmNv\nbSIsImlhdCI6MTQ3MjI2MzI1NiwiZXhwIjoxNDcyNTIyNTI1LCJzdWIiOjEx\nMjF9\n

簽名 就是將 頭部和載荷使用 "." 連接成的字符串 再使用我們自己提供的一個(gè)密鑰 進(jìn)行HS256加密后的字符串焙糟。

如果是用 "jwt-rails" 作為密鑰的話口渔,簽名:

cd5a6c7a135e811477918c5c0f864582bced820ff6b5ed6766974c3ef8ca9773

JWT的 安全重點(diǎn)就是在簽名的密鑰上,如果僅僅有服務(wù)器端知道密鑰的話穿撮,其他人如果獲得了 JWT字符串并對(duì)它進(jìn)行了篡改缺脉,那么它發(fā)送到服務(wù)端后就無法通過密鑰加密的簽名驗(yàn)證,這樣就有效的阻止這類安全問題悦穿。但是要注意的是攻礼,載荷部分所攜帶的信息是Base64編碼"非加密",所以我們不要把有關(guān)用戶的敏感信息存放在其中栗柒,一般在API接口開發(fā)中僅需要存放礁扮,能夠標(biāo)識(shí)用戶的ID或UUID即可。

JWT in Rails API

JWT-Ruby gem 已經(jīng)幫我們實(shí)現(xiàn)JWT規(guī)范的庫(kù)瞬沦,現(xiàn)在只有使用它提供的API就可以使用 JWT 進(jìn)行開發(fā)了太伊。

我們接下來就,開發(fā)一個(gè)具有rails 5 API的后端示例應(yīng)用逛钻。

rails new jwt_rails --api

再添加gem 到 Gemfile

gem 'jwt'
gem 'bcrypt'

我們先創(chuàng)建一個(gè)users controller僚焦,users_controller 會(huì)返回有關(guān)用戶的信息,但是求助這個(gè)

rails g controller users

然后在創(chuàng)建 User 模型

rails g model User username:string email:string password_digest:string

填充User模型的代碼

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end

console中創(chuàng)建一個(gè)用戶

2.3.0 :003 > User.create(username: 'json', email: 'json@gmail.com', password: '12345', password_confirmation: '12345')
 => #<User id: 1, username: "json", email: "json@gmail.com", created_at: "2016-08-27 03:19:44", updated_at: "2016-08-27 03:19:44", password_digest: "$2a$10$3KrwpUYEgYfBJTBJJMX.5uU9d14hs91rf5Fnt8cUEvZ...">

接下來就是把JWT集成到項(xiàng)目中曙痘,先創(chuàng)建叫Token的包裝類芳悲,其中使用了 Rails的secret key 作為JWT的加密密鑰。

# app/models/token.rb
class Token
  def self.encode(payload)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

  def self.decode(token)
    HashWithIndifferentAccess.new(JWT.decode(token, Rails.application.secrets.secret_key_base)[0])
  rescue
    nil
  end
end

再修改User模型边坤,讓其支持通過id作為承載信息芭概,然后生成的token的方法。

class User < ApplicationRecord
  has_secure_password

  def token
    {
      token: Token.encode(user_id: self.id)
    }
  end

  def to_json
    self.slice(:username, :email)
  end
end

app/controllers/application_controller.rb 中添加驗(yàn)證token是否有效的方法惩嘉。

class ApplicationController < ActionController::API

  attr_accessor :current_user

  protected

  def authenticate!
    render_failed and return unless token?
    @current_user = User.find_by(id: auth_token[:user_id])
  rescue JWT::VerificationError, JWT::DecodeError
    render_failed
  end

  private

  def render_failed(messages = ['驗(yàn)證失敗'])
    render json: { errors: messages}, status: :unauthorized
  end

  def http_token
    @http_token ||= if request.headers['Authorization'].present?
                      request.headers['Authorization'].split(' ').last
                    end
  end

  def auth_token
    @auth_token ||= Token.decode(http_token)
  end

  def token?
    http_token && auth_token && auth_token[:user_id].to_i
  end

end


app/controllers/authentication_controller.rb 中處理用戶登錄然后返回授權(quán)token罢洲。

class AuthenticationController < ApplicationController

  def create
    if user = User.find_by(username: params[:username]).try(:authenticate, params[:password])
      render json: user.token
    else
      render json: {errors: ['用戶名或密碼錯(cuò)誤']}, status: :unauthorized
    end
  end

end

然后通過授權(quán)的token 訪問用戶信息 app/controllers/users_controller.rb 其中使用了我們?cè)赼pplication_controller定義的驗(yàn)證方法,作為前置過濾器。

class UsersController < ApplicationController
  before_action :authenticate!

  def index
    render json: current_user.to_json
  end

end

最后添加路由:

Rails.application.routes.draw do
  resources :users, only: :index
  resources :authentication, only: :create
end

啟動(dòng)服務(wù)

rails s

下面我們使用curl來請(qǐng)求驗(yàn)證一下我們剛剛寫的API惹苗。

登錄驗(yàn)證:

curl -X POST -d username="json" -d password="12345" http://localhost:3000/authentication

返回結(jié)果:

{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjF9.j8-GAiEQ2LIzC8GdbqZ6H5aUA32Mux07uaY9RfOQrx8"}

如果不用Token直接訪問用戶信息的話殿较。

curl http://localhost:3000/users

會(huì)直接返回驗(yàn)證失敗:

{"errors":["驗(yàn)證失敗"]}

使用Token請(qǐng)求用戶信息

curl --header "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjF9.j8-GAiEQ2LIzC8GdbqZ6H5aUA32Mux07uaY9RfOQrx8" http://localhost:3000/users

返回結(jié)果:

{
  "username":"json",
  "email":"json@gmail.com"
}

通過驗(yàn)證桩蓉。

注銷

JWT 對(duì)應(yīng)注銷已簽發(fā)的token有三種方式:

  • payload中的exp過期時(shí)間
  • 客戶端丟棄本地緩存的token
  • 服務(wù)端維護(hù)一個(gè)token廢棄池

exp

使用JWT規(guī)范定義中payload可以攜帶的過期時(shí)間鍵值對(duì)淋纲,我們可以對(duì)上面的程序做一些修改。

首先在app/models/token.rb 中修改encode方法:

  def self.encode(payload)
    payload.merge!(exp: (Time.now.to_i + 3600)) # 添加過期時(shí)間為一小時(shí)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

然后再修改驗(yàn)證過濾器院究,讓它支持捕獲token過期異常

rescue JWT::ExpiredSignature
  render_failed ['授權(quán)已過期']
end

最后如果請(qǐng)求發(fā)送的token過期結(jié)果就是:

{"errors":["授權(quán)已過期"]}

廢棄池

在嚴(yán)格要求廢棄指定的token的場(chǎng)景下洽瞬,推薦使用Redis維護(hù)這樣一個(gè)廢棄池,在每次需要驗(yàn)證的請(qǐng)求中业汰,過濾掉已經(jīng)廢棄的token伙窃。

客戶端丟棄

這是成本最低的方式,把任務(wù)分散到各個(gè)客戶端样漆,可以很好的與現(xiàn)在的移動(dòng)端開發(fā)配合为障,每次用戶注銷只要?jiǎng)h除本地存放的token即可。

結(jié)論

JWT作為一種輕量級(jí)的令牌驗(yàn)證方案放祟,是很輕便的鳍怨,使用它,服務(wù)端就可以無需維護(hù)令牌的狀態(tài)跪妥,同時(shí)也解決了多系統(tǒng)的同步登錄問題鞋喇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市眉撵,隨后出現(xiàn)的幾起案子确徙,更是在濱河造成了極大的恐慌,老刑警劉巖执桌,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鄙皇,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡仰挣,警方通過查閱死者的電腦和手機(jī)伴逸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來膘壶,“玉大人错蝴,你說我怎么就攤上這事⊥前牛” “怎么了顷锰?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)亡问。 經(jīng)常有香客問我官紫,道長(zhǎng)肛宋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任束世,我火速辦了婚禮酝陈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘毁涉。我一直安慰自己沉帮,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布贫堰。 她就那樣靜靜地躺著穆壕,像睡著了一般铝噩。 火紅的嫁衣襯著肌膚如雪燎孟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天迫像,我揣著相機(jī)與錄音漫玄,去河邊找鬼。 笑死压彭,一個(gè)胖子當(dāng)著我的面吹牛睦优,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播壮不,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼汗盘,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了询一?” 一聲冷哼從身側(cè)響起隐孽,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎健蕊,沒想到半個(gè)月后菱阵,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缩功,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年晴及,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嫡锌。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡虑稼,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出势木,到底是詐尸還是另有隱情蛛倦,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布啦桌,位于F島的核電站溯壶,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茸塞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一躲庄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧钾虐,春花似錦噪窘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至菌仁,卻和暖如春浩习,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背济丘。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工谱秽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人摹迷。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓疟赊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親峡碉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子近哟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)鲫寄,斷路器吉执,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 轉(zhuǎn)載本文需注明出處:微信公眾號(hào)EAWorld,違者必究地来。 本文目錄: 一戳玫、單體應(yīng)用 VS 微服務(wù) 二、微服務(wù)常見安...
    72a1f772fe47閱讀 8,531評(píng)論 3 25
  • 本文目錄:一未斑、單體應(yīng)用 VS 微服務(wù)二量九、微服務(wù)常見安全認(rèn)證方案三、JWT介紹四颂碧、OAuth 2.0 介紹五荠列、思考總...
    挨踢的懶貓閱讀 17,948評(píng)論 5 29
  • 1. 微服務(wù)架構(gòu)介紹 1.1 什么是微服務(wù)架構(gòu)? 形像一點(diǎn)來說载城,微服務(wù)架構(gòu)就像搭積木肌似,每個(gè)微服務(wù)都是一個(gè)零件,并使...
    靜修佛緣閱讀 6,630評(píng)論 0 39
  • 前言 本文將首先概述基于cookie的身份驗(yàn)證方式和基于token的身份驗(yàn)證方式诉瓦,在此基礎(chǔ)上對(duì)兩種驗(yàn)證進(jìn)行比較川队。最...
    大蟒傳奇閱讀 38,429評(píng)論 17 285