Skip to content

JWT

JWT(JSON、Web、Token),token可以理解为加密字符串。 关于web上用户的登录功能可以查看陈皓的文章

耳熟能详的登录解决方法为 Session + Cookie

Cookie属于http的内容,表示服务器给浏览器发送的一个特殊字符(token)。

浏览器每次向同一域名都服务器发送的每一个请求都自动带上这个特殊字符。


Session一般基于cookie实现,如果服务器给客户端发送明文的内容,会被黑客篡改,冒充登录,如果使用用随机数,则不会。

Session就是保存在服务器的内存文件中,用来做真实内容与密文的对应关系。Session需要占用服务器的资源。


JWT的定义

JWT就是将header、payload、signature用点分隔符连接后发送给前端。具体的定义可以查看维基百科的JSON Web Token。JWT是做web认证的。

JWT有三部分组成,分别是

  • header——标识用于生成签名的算法,{ "alg","HS256","typ":"JWT" },用于标识加密格式和类型。
  • payload(body)——包含一组声明,可以存放JSON的内容。
  • siginature(密文)——将私钥、header头和body加密。header和body需要用base64处理,防止出现奇怪的字符。
ruby
HMAC_SHA256(
  secret,
  base64urlEncoding(header) + '.' +
  base64urlEncoding(payload)
)

JWT的用法

请求JWT的时候可能会把自身的内容发给后端,后端明确后再将JWT发送给前端。前端需要手动去维护JWT,<font style="color:#E8323C;">localstorage</font><font style="color:#E8323C;">axios</font><font style="color:#E8323C;">header</font><font style="color:#E8323C;">Authorization</font>。在下一次请求的时候服务器会去看header头中Authorization否有JWT,解码后的内容和服务器发送的内容是否一致,一致后在返回数据给前端。

后端实现JWT

编写测试用例

创建controller,bin/rails g controller api/v1/sessions_controller,修改控制器的内容。

ruby
class Api::V1::SessionsController < ApplicationController
  def create
    # 如果测试环境,code 就永远为 123456
    if Rails.env.test?
      # 直接验证码必须为 123456
    else
      #
    end
  end
end

写测试用例。

ruby
require 'rails_helper'

RSpec.describe "Sessions", type: :request do
  describe "会话" do
    it "登录(创建会话)" do
      post '/api/v1/session', params: { email: '919041098@qq.com', code: '123456'}
      expect(response).to have_http_status(200)
      json = JSON.parse response.body
      expect(json['jwt']).to be_a(String)
    end
  end
end

实现登录接口

条件是发送过email和code,code未被使用即used_at为空。由于rails是没有JWT的,需要引入库,这边先写死。

ruby
class Api::V1::SessionsController < ApplicationController
  def create
    # 如果测试环境
    if Rails.env.test?
      return render status: :unauthorized if params[:code] != '123456'
    else 
      canSignin = ValidationCode.exists? email: params[:email], code: params[:code], used_at: nil
      # unless 如果不存在则返回
      return render status: :unauthorized unless canSignin
    end
    user = User.find_by_email params[:email]
    # 数据库中找不到email的话
    if user.nil?
      render status: :not_found, json: {error: '用户不存在'}
    else 
      render status: :ok, json: {
        jwt: 'xxxxxxxx'
      }
    end
    
  end
end
ruby
require 'rails_helper'

RSpec.describe "Sessions", type: :request do
  describe "会话" do
    it "登录(创建会话)" do
      # 默认创建一个邮箱
      User.create email: '919041098@qq.com'
      post '/api/v1/session', params: {email: '919041098@qq.com', code: '123456'}
      # 期待状态码为200
      expect(response).to have_http_status(200)
      json = JSON.parse response.body
      # 期待json中的jwt为字符串
      expect(json['jwt']).to be_a(String)
    end
  end
end

实现JWT encode

安装jwt的库,详细内容见链接。在gemfile文件中添加gem 'jwt',然后执行bundle install,可以用浏览器的window.atob()来解密base64。网站Online UUID Generator生成一个随机数用作hmac_sercet的密钥,执行EDITOR="code --wait" bin/rails credentials:edit,填入hmac_sercet密钥保存。然后通过Rails.application.credentials.hmac_secret来使用密钥。

ruby
require 'jwt'
class Api::V1::SessionsController < ApplicationController
  def create
    # 如果测试环境
    if Rails.env.test?
      return render status: :unauthorized if params[:code] != '123456'
    else 
      canSignin = ValidationCode.exists? email: params[:email], code: params[:code], used_at: nil
      return render status: :unauthorized unless canSignin
    end
    user = User.find_by_email params[:email]
    if user.nil?
      render status: :not_found, json: {error: '用户不存在'}
    else
      payload = { user_id: user.id }
      token = JWT.encode payload, Rails.application.credentials.hmac_secret, 'HS256'
      render status: :ok, json: {
        jwt: token
      }
    end
    
  end
end

用户接口测试用例

对创建好的jwt进行解密,执行bin/rails g controller api/v1/mes_controller,编写测试用例。

ruby
class Api::V1::MesController < ApplicationController
  def show
  	head 500
  end
end
ruby
require 'rails_helper'

RSpec.describe "Me", type: :request do
  describe "获取当前用户" do
    it "登录后成功获取" do
      # 发送获取邮箱code 得到jwt
      user =  User.create email: '919041098@qq.com'
      post '/api/v1/session', params: {email: '919041098@qq.com', code: '123456'}
      json = JSON.parse response.body
      jwt = json['jwt']
    	# 下次请求时请求头带上jwt Bearer为约定内容
      request.headers["Authorization"] = "Bearer #{jwt}"
      # 解密验证是否为同一个用户的id
      get '/api/v1/me' 
      expect(response).to have_http_status(200)
      json = JSON.parse response.body
      expect(json['resource']['id']).to eq user.id
    end
  end
end

实现JWT decode

对jwt的内容进行解密,还需要对数据进行try/catch处理,判断数据是否为空。rescue叫挽救,类似于try/catch,若出现报错则替换。

ruby
class Api::V1::MesController < ApplicationController
  def show
    # get jwt from header
    header = request.headers["Authorization"]
    jwt = header.split(' ')[1] rescue ''
    # decode jwt
    payload = JWT.decode jwt, Rails.application.credentials.hmac_secret, true, { algorithm: 'HS256' } rescue nil
    return head 400 if payload.nil?
    # get user_id from payload
    user_id = payload[0]['user_id'] rescue nil
    # get user from user_id
    user = User.find user_id
    return head 404 if user.nil?
    # render user
    render json: { resource: user }
  end
end

本章内容的代码,详见链接