美文网首页Ruby & RailsRubyRuby on Rails
JWT 在 Ruby 中的使用总结和 Sinatra 框架解读和

JWT 在 Ruby 中的使用总结和 Sinatra 框架解读和

作者: 老码农不上班 | 来源:发表于2017-07-16 16:01 被阅读377次

    JSON Web Token 介绍了 JSON Web Token 的基本原理,接下来这篇文章结合 Ruby 分析总结在实际项目中的使用。Ruby 有对应版本的 Gem ruby-jwt ,我们这篇讨论 JWT 在 Ruby 编程语言中的使用、并简单解析 Sinatra 框架

    ruby-jwt 介绍

    ruby-jwt 实现了RFC 7519 OAuth JSON Web Token 标准,结合文档总结在 Ruby 中如何使用 JWT

    加密和验签

    在 jwt 中通常我们使用两种加密方式 RS 256HS 256 ,这两者分别对应 RSAHMAC
    。如果是给第三方提供 HTTP API 接口,通常是使用 JWT 校验加密明文的特性。使用 RS 256 的话需要双方都给对方提供己方的 OpenSSL 公钥,若使用 HS256 ,则双方协商好使用同一个密钥串。

    使用 RS256 的流程是:甲方服务器用己方的私钥对数据进行加签,然后乙方使用甲提供的公钥验签并解析出明文并使用己方的私钥加签返回给乙方服务器的明文,甲方服务器再使用乙提供的公钥进行解签。

    使用 HS256 的流程是:双方都用协商好的密钥传解密需要传送的明文、验证和解签对方发送过来的 JWT token
    ruby-jwt 加密方法的源码:

    def encode(payload, key, algorithm = 'HS256', header_fields = {})
      encoder = Encode.new payload, key, algorithm, header_fields
      encoder.segments
    end
    
    def decode(jwt, key = nil, verify = true, custom_options = {}, &keyfinder)
      raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
    
      merged_options = DEFAULT_OPTIONS.merge(custom_options)
    
      decoder = Decode.new jwt, verify
      header, payload, signature, signing_input = decoder.decode_segments
      decode_verify_signature(key, header, payload, signature, signing_input, merged_options, &keyfinder) if verify
    
      Verify.verify_claims(payload, merged_options)
    
      raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
    
      [payload, header]
    end
    

    结合 encodedecode 源码,接下来学习签名和验证的例子。

    require 'jwt'
    
    payload = { data: 'test' }
    
    # HS256 encode
    hmac_secret = 'my$ecretK3y'
    token = JWT.encode payload, hmac_secret, 'HS256'
    # eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.ZxW8go9hz3ETCSfxFxpwSkYg_602gOPKearsf6DsxgY
    
    # HS256 decode
    decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
    # [{"data"=>"test"}, {"typ"=>"JWT", "alg"=>"HS256"}]
    
    rsa_private = OpenSSL::PKey::RSA.generate 2048
    rsa_public = rsa_private.public_key
    
    # RS256 encode
    token = JWT.encode payload, rsa_private, 'RS256'
    # eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkYXRhIjoidGVzdCJ9.vtkMG9wCJRWc8lwOQJxnV8vRWRiBfvIzsE-vnM168Pe4jXszc2p9_2upAi8SI5EuwR7CsVAPFO_SqNsJLCb_55srqyBxPAyy97gy-44VyFR-dsnt9xpt2meJ4DyolXwhWxHTF9WkmQPHoFlu_2ssOOszBj9MO1X7KhmgkrX9h9yBTTzT9qvZQkesAbZz1RrF3ZRhihwbBdtGCCbJvlGBI6NAoMf_b3vNqeaHawc5hMS9nfoN-5Sc9CleJdPPnWnN7OYXeI_xdhCNTok0b7nMPvgDuIj9nTW4_u3Fv-rq9IiM-62LU1JFdqFVWQU-f72nkbT4bVy_SJr11mJ9Q6pXNQ
    
    # RS decode
    decoded_token = JWT.decode token, rsa_public, true, { :algorithm => 'RS256' }
    [{"data"=>"test"}, {"typ"=>"JWT", "alg"=>"RS256"}]
    

    几个常用的 Claim

    • Expiration Time Claim(exp) 失效时间
    • Issued At Claim(iat) 表示这个JWT token 的签发时间
    • JWT ID Claim JWT token 唯一标识
    hmac_secret = 'my$ecretK3y'
    
    exp = Time.now.to_i + 4 * 3600
    iat = Time.now.to_i
    jti_raw = [hmac_secret, iat].join(':').to_s
    jti = Digest::MD5.hexdigest(jti_raw)
    
    payload = { :data => 'data', :exp => exp, :iat => iat, :jti => jti}
    token = JWT.encode payload, hmac_secret, 'HS256'
    
    # decode
    begin
      decoded_token = JWT.decode token, hmac_secret, true, { :verify_iat => true, :verify_jti => true, :algorithm => 'HS256' }
    rescue JWT::ExpiredSignature
      # Handle expired token, e.g. logout user or deny access
    rescue JWT::InvalidIatError
      # Handle invalid token, e.g. logout user or deny access
    rescue JWT::InvalidJtiError
      # Handle invalid token, e.g. logout user or deny access
    end
    

    Sinatra 原理解释、使用

    在这部分,我们先来介绍 Sinatra 基本实现原理以及如何实现 Sinatra 扩展原理。然后搭建一个完整的 Sinatra 应用。假设的场景是我们为第三方提供 HTTP API , 使用了 JWT 两个特性:明文加密和验签。实际开发中需要双方都给对方提供 OpenSSL 生成的公钥。

    Sinatra 基本原理

    描述约定

    1. top-level DSL 指 Sinatra 中的路由(Rails 中的 controller)
    2. classic style 文中描述 经典风格的 Sinatra 应用
    3. module style 文中描述为 模块化风格的 Sinatra 应用
    4. Sinatra.helpers 实现的扩展为“帮助方法扩展”
    5. Sinatra.register 实现的扩展为“路由扩展”

    经典风格和模块化风格的 Sinatra 应用

    通常使用 Sinatra 构建项目有两种方式:经典风格和模块化风格。

    一种是经典风格(classic style)这种模式通常应用在小型项目中,所有接受 HTTP 请求的路由只放在一个文件中(app.rb)。
    app.rb 文件中需要引入 require 'sinatra' 然后剩下的就是直接写路由。

    # app.rb
    require 'sinatra'
    
    get '/' do
      'Hello world'
    end
    

    另外一种是模块化风格(Modular style) 这种模式也是本文主要介绍的方式,通常应用在大型的 Rack-base 应用中和编写可复用的 Rack 中间件,它可以扩展为类 Rails 的 MVC 结构。通过这样的脚手架以此我们就能实现相对复杂的接口应用。

    
    # 注意这里引入的是 sinatra/base 而不是 sinatra
    require 'sinatra/base'
    require "sinatra/config_file"
    
    class MyApp < Sinatra::Base
      register Sinatra::ConfigFile
      config_file 'path/to/config.yml'
    
      get '/' do
        @greeting = settings.greeting
        haml :index
      end
    
      # The rest of your modular application code goes here...
    end
    

    实现模块化风格的 Sinatra 应用有继承 Sinatra::BaseSinatra::Application 两种方式,解释一下两者的差异。

    Sinatra::Application 和 Sinatra::Base 的关系

    通常模块化风格的 Sinatra 都要继承sinatra::base,但是一些设置,譬如 session, flash 这些 Sinatra 框架内部默认实现的扩展都是关闭状态;开启默认的扩展的最简单方法就是 require 'sinatra' 实现一个经典风格的 Sinatra 应用。require 'sinatra' 实现的经典风格的 Sinatra 应用的原理就是继承了 Sinatra::Application,而 Sinatra::Application 也是继承了 Sinatra::Base 。。通过看 sinatra 源码可知 Sinatra::Application 继承了 Sinatra::Base:

    # https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb#L1902
    class Application < Base
        set :logging, Proc.new { !test? }
        set :method_override, true
        set :run, Proc.new { !test? }
        set :app_file, nil
    
        def self.register(*extensions, &block) #:nodoc:
          added_methods = extensions.flat_map(&:public_instance_methods)
          Delegator.delegate(*added_methods)
          super(*extensions, &block)
        end
      end
    

    在 Application 类的注释中有这么一段话

    # Execution context for classic style (top-level) applications. All
    # DSL methods executed on main are delegated to this class.
    #
    # The Application class should not be subclassed, unless you want to
    # inherit all settings, routes, handlers, and error pages from the
    # top-level. Subclassing Sinatra::Base is highly recommended for
    # modular applications.
    

    足以说明继承 Application 类就会开启 Sinatra 默认的设置、路由。
    Sinatra::Base 中实现的一系列类方法 (e.g., get, post, before, configure, set, etc.)

    经典风格和模块化风格 Sinatra 应用的关系

    require 'sinatra'
    
    get '/' do
      ...
    end
    

    结合代码,上文提到实现经典风格的 Sinatra 应用,只需在 require 'sinatra' 即可。我们来看一下 Sinatra 中的源码。

    # sinatra/lib/sinatra.rb
    require 'sinatra/main'
    
    enable :inline_templates
    
    # sinatra/lib/sinatra/main.rb
    # https://github.com/sinatra/sinatra/blob/master/lib/sinatra/main.rb
    require 'sinatra/base'
    
    module Sinatra
      class Application < Base
    
        # we assume that the first file that requires 'sinatra' is the
        # app_file. all other path related options are calculated based
        # on this path by default.
        set :app_file, caller_files.first || $0
        ...
      end
    end
    

    经典风格的 Sinatra 应用本质也是一个最基本的模块化风格,其实现原理是继承了 Sinatra::Application 类。

    每个模块化风格的Sinatra必须要继承 Sinatra::Base 。因为 Sinatra::Application 也继承了 Sinatra::Base,因而经典风格就是最基本的模块化Sinatra应用。

    Sinatra 扩展

    优雅的代码讲究复用和模块化,把常用的功能模块抽象出来做成 Sinatra 扩展。在这章节中,我们继续讨论实现 Sinatra 扩展必备的三点: Sinatra.registerSinatra.helpersModule.registered

    扩展通常有两种,分别是使用 Sinatra.helpers 实现的帮助方法模块扩展、使用 Sinatra.register 实现的路由模块扩展。帮助方法扩展的方法在请求上下文中使用:view, routes, helper ,通常把一些能被复用的逻辑代码封装成帮助方法模块扩展,譬如页面渲染帮助方法、判断是否登录的逻辑代码;路由扩展中的方法是 Sinatra::Application 中的类方法,实现了某种功能特性的路由,通常用来封装某个功能模块,譬如�系统登录功能模块、接口访问验签功能模块。

    # lib/sinatra/base.rb
    
    # Extend the top-level DSL with the modules provided.
      def self.register(*extensions, &block)
        Delegator.target.register(*extensions, &block)
      end
    
      # Include the helper modules provided in Sinatra's request context.
      def self.helpers(*extensions, &block)
        Delegator.target.helpers(*extensions, &block)
      end
    

    Extending The DSL (class) Context with Sinatra.register

    通过 Sinatra.register 实现的扩展提供了是Sinatra::Application 的类方法。路由扩展例子:

    require 'sinatra/base'
    module Sinatra
      module LinkBlocker
        def block_links_from(host)
          before {
            halt 403, "Go Away!" if request.referer.match(host)
          }
        end
      end
      register LinkBlocker
    end
    

    Sinatra.registerSinatra::Application 添加了一个类方法。在经典风格和模块化风格中使用我们这个扩展如下

    # classic style
    require 'sinatra'
    require 'sinatra/linkblocker'
    
    block_links_from 'digg.com'
    
    get '/' do
      "Hello World"
    end
    
    # modular style
    require 'sinatra/base'
    require 'sinatra/linkblocker'
    
    class Hello < Sinatra::Base
      register Sinatra::LinkBlocker
    
      block_links_from 'digg.com'
    
      get '/' do
        "Hello World"
      end
    end
    

    路由扩展必须在 Sinatra 模块 中,且通过 Sinatra.register 注册之后才能被 Sinatra 应用使用。而且经典风格的 sinatra 应用直接在顶层 require 对应的扩展 module 即可,而模块化风格的 Sinatra 应用另外还必须在继承类内部通过 Sinatra.register 再次注册路由扩展。

    Extending The Request Context with Sinatra.helpers

    帮助方法模块扩展为路由、视图或者帮助方法添加方法。下面这个例子实现了方法名为 h 帮助方法。

    require 'sinatra/base'
    
    module Sinatra
      module HTMLEscapeHelper
        def h(text)
          Rack::Utils.escape_html(text)
        end
      end
    
      helpers HTMLEscapeHelper
    end
    

    在上面的例子中 helpers HTMLEscapeHelper 方法把扩展中定义的所有模块方法添加到了 Sinatra::Application 中。所以这些方法都能在经典风格中使用。

    require 'sinatra'
    require 'sinatra/htmlescape'
    
    get "/hello" do
      h "1 < 2"     # => "1 < 2"
    end
    

    在经典风格中引用扩展的方法,只需 require 'sinatra/htmlescape' 即可。

    但是要想在模块化风格中使用通过 sinatra.helpers 实现的扩展方法,除了在顶层 require 对应的扩展模块,还需要使用 helpers 方法引入:

    require 'sinatra/base'
    require 'sinatra/htmlescape'
    
    class HelloApp < Sinatra::Base
      helpers Sinatra::HTMLEscapeHelper
    
      get "/hello" do
        h "1 < 2"
      end
    end
    

    使用 registered 模块方法配置路由扩展

    通过在模块中实现 registered 模块方法可设置路由扩展的可选项、路由、过滤器和异常处理。模块被引用时,定义在扩展模块中的 registered 方法会被添加到 Sinatra::Base 模块中。registered 方法有一个 app 参数,路由扩展自实现注册后(体现在路由扩展中 register ExtensionsName), Sinatra 会传递当前应用的上下文给 registered 方法(也就是默认的 app 参数),而通过 app 参数就能调用 Sinatra::Base 中定义的 DSL 方法。

    实现一个简单的登录验证扩展例子

    # lib/sinatra/auth.rb 实现扩展
    require 'sinatra/base' # 实现扩展必须要 require 'sinatra/base'
    require 'sinatra/flash'
    
    module Sinatra
      module Auth
        module Helpers
          def authorized?
            session[:admin]
          end
    
          def protected!
            halt 401,slim(:unauthorized) unless authorized?
          end
        end
    
        def self.registered(app)
          app.helpers Helpers # 注册帮助方法模块
    
          app.enable :sessions # 开启 Sinatra session
          app.set :username => 'frank', :password => 'sinatra' # 设置可选项
    
          # 调用 ```Sinatra::Base 定义的 DSL 方法
          app.get '/login' do
            slim :login
          end
    
          app.post '/login' do
            if params[:username] == settings.username && params[:password] == settings.password
              session[:admin] = true
              flash[:notice] = "You are now logged in as #{settings.username}"
              redirect to('/')
            else
              flash[:notice] = "The username or password you entered are incorrect"
              redirect to('/login')
            end
          end
    
          app.get '/logout' do
            session[:admin] = nil
            flash[:notice] = "You have now logged out"
            redirect to('/')
          end
        end
      end
    
      register Auth # 自注册路由扩展
    
    end
    
    # config/application.rb 调用扩展
    require 'sinatra/base'
    require_relative '../sinatra/auth'
    
    class Application < Sinatra::Base
      set :root, File.dirname(__FILE__) # 设置项目根目录
      set :views, "#{settings.root}/../app/views" # 设置项目视图文件目录
      set :public_folder, 'public'  # 设置静态文件目录
    
      register Sinatra::Auth # 模块化风格的 Sinatra 应用注册 Auth 路由扩展
    end
    
    # 关于帮助方法
    # 在 Auth 路由扩展中我们也实现了一个名字为 Helpers 的帮助方法扩展:
    # 分别有帮助方法 authorized? 和 protected!
    # 上文提到帮助扩展中的帮助方法可以在视图、路由、中使用。
    # 使用例子:在视图文件中,想要现实登录按钮可以这样子写
    footer
      - if authorized?
        a href="/logout" log out
      - else
        a href="/login" log in
    
    

    写在最后

    对于不想使用 Rails 框架的 Ruby 开发者,Sinatra 是不错的替代框架,其轻量且能够快速开发项目。笔者使用它开发了一个支付渠道中台项目,提供 API 和渲染各种 H5 前端页面开发。这篇文章三个月前就开始写了,由于拖延和惰性断断续续写,每次写都需重新看一遍以前收藏的文章梳理写作思路,异常浪费时间和痛苦。
    告诉自己给自己一个交代,做个的项目如果没有总结和反思写下来过不久定会忘光。另外最近在学习 Go ,我想以后会更少地使用 Ruby 开发。

    参考链接并值得一读的文章

    相关文章

      网友评论

      本文标题:JWT 在 Ruby 中的使用总结和 Sinatra 框架解读和

      本文链接:https://www.haomeiwen.com/subject/anotkxtx.html