JWT 构建Rails API 授权登录

移动应用开发中,令牌授权(token-based) 是一种常用的移动端与服务端的授权登录方式 ,但是使用它,需要面临着一些问题,如:令牌的过期时间,令牌状态在服务器端的维护,服务端多子系统同步等问题。本文要说到的JWT(JSON Web Token) 轻量级的验证规范,就是一种非常好的解决方案。

JWT

在JWT的规范定义中,它由头部,载荷和签名,三部分字符串组成其中前两部分是用JSON对象进过Base64编码而来的。

头部 是由typ和alg两部分组成,typ 表示自己是一个JWT,alg表示签名使用了什么算法。

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

经过base64编码后的结果就是:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷 是JWT中真正承载用户信息的部分,它也是一个json对象,由自定义部分和规范定义部分组成

JWT规范定义 中描述的几个可选的信息

{
    "iss": "JWT-Rails-Server", // 签发者
    "aud": "www.baidu.com", // 接收者
    "iat": 1472263256, // JWT 签发的时间
    "exp": 1472522525, // 过期时间
    "sub": "jwt@baidu.com" // JWT对应的用户 
    "user_id": 1211 // 自定义
}

我们还可以在上面的JSON中添加我们自定义的部分。

最后载荷也是需要通过Base64进行编码的:

eyJpc3MiOiJKV1QtUmFpbHMtU2VydmVyIiwiYXVkIjoid3d3LmJhaWR1LmNv\nbSIsImlhdCI6MTQ3MjI2MzI1NiwiZXhwIjoxNDcyNTIyNTI1LCJzdWIiOjEx\nMjF9\n

签名 就是将 头部和载荷使用 "." 连接成的字符串 再使用我们自己提供的一个密钥 进行HS256加密后的字符串。

如果是用 "jwt-rails" 作为密钥的话,签名:

cd5a6c7a135e811477918c5c0f864582bced820ff6b5ed6766974c3ef8ca9773

JWT的 安全重点就是在签名的密钥上,如果仅仅有服务器端知道密钥的话,其他人如果获得了 JWT字符串并对它进行了篡改,那么它发送到服务端后就无法通过密钥加密的签名验证,这样就有效的阻止这类安全问题。但是要注意的是,载荷部分所携带的信息是Base64编码"非加密",所以我们不要把有关用户的敏感信息存放在其中,一般在API接口开发中仅需要存放,能够标识用户的ID或UUID即可。

JWT in Rails API

JWT-Ruby gem 已经帮我们实现JWT规范的库,现在只有使用它提供的API就可以使用 JWT 进行开发了。

我们接下来就,开发一个具有rails 5 API的后端示例应用。

rails new jwt_rails --api

再添加gem 到 Gemfile

gem 'jwt'
gem 'bcrypt'

我们先创建一个users controller,users_controller 会返回有关用户的信息,但是求助这个

rails g controller users

然后在创建 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中创建一个用户

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集成到项目中,先创建叫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 中添加验证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 = ['验证失败'])
    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 中处理用户登录然后返回授权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: ['用户名或密码错误']}, status: :unauthorized
    end
  end

end

然后通过授权的token 访问用户信息 app/controllers/users_controller.rb 其中使用了我们在application_controller定义的验证方法,作为前置过滤器。

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

启动服务

rails s

下面我们使用curl来请求验证一下我们刚刚写的API。

登录验证:

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

返回结果:

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

如果不用Token直接访问用户信息的话。

curl http://localhost:3000/users

会直接返回验证失败:

{"errors":["验证失败"]}

使用Token请求用户信息

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

返回结果:

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

通过验证。

注销

JWT 对应注销已签发的token有三种方式:

  • payload中的exp过期时间
  • 客户端丢弃本地缓存的token
  • 服务端维护一个token废弃池

exp

使用JWT规范定义中payload可以携带的过期时间键值对,我们可以对上面的程序做一些修改。

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

  def self.encode(payload)
    payload.merge!(exp: (Time.now.to_i + 3600)) # 添加过期时间为一小时
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

然后再修改验证过滤器,让它支持捕获token过期异常

rescue JWT::ExpiredSignature
  render_failed ['授权已过期']
end

最后如果请求发送的token过期结果就是:

{"errors":["授权已过期"]}

废弃池

在严格要求废弃指定的token的场景下,推荐使用Redis维护这样一个废弃池,在每次需要验证的请求中,过滤掉已经废弃的token。

客户端丢弃

这是成本最低的方式,把任务分散到各个客户端,可以很好的与现在的移动端开发配合,每次用户注销只要删除本地存放的token即可。

结论

JWT作为一种轻量级的令牌验证方案,是很轻便的,使用它,服务端就可以无需维护令牌的状态,同时也解决了多系统的同步登录问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,475评论 4 372
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,744评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,101评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,732评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,141评论 3 297
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,049评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,188评论 2 320
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,965评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,716评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,867评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,341评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,663评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,376评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,200评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,990评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,179评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,979评论 2 279

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,111评论 18 139
  • 转载本文需注明出处:微信公众号EAWorld,违者必究。 本文目录: 一、单体应用 VS 微服务 二、微服务常见安...
    72a1f772fe47阅读 8,461评论 3 25
  • 本文目录:一、单体应用 VS 微服务二、微服务常见安全认证方案三、JWT介绍四、OAuth 2.0 介绍五、思考总...
    挨踢的懒猫阅读 17,877评论 5 29
  • 1. 微服务架构介绍 1.1 什么是微服务架构? 形像一点来说,微服务架构就像搭积木,每个微服务都是一个零件,并使...
    静修佛缘阅读 6,549评论 0 39
  • 前言 本文将首先概述基于cookie的身份验证方式和基于token的身份验证方式,在此基础上对两种验证进行比较。最...
    大蟒传奇阅读 38,304评论 17 285