如何实现Whatsapp生活QR码验证

时间:2017-03-18 20:46:12

标签: ruby-on-rails ruby

如何在rails应用程序上创建动态QR码,以便在扫描和成功处理时,带有QR码的打开页面可以重定向到成功页面。

这类似于whatsapp web实现,其中android app扫描QR码的那一刻,页面加载消息。

更感兴趣的是会话的管理。扫描QR时,可以重新加载显示的页面,然后重定向到另一页。任何想法?

2 个答案:

答案 0 :(得分:3)

您可以更新用户模型,以便能够存储要在QR码中使用的唯一标记值; e.g。

$ rails generate migration add_token_to_user token:string

或单独的相关模型

$ rails generate model Token value:string user:belongs_to

然后生成可在URL中使用并对其进行编码的唯一标记值 进入QRCode

# Gemfile
gem "rqrcode"

# app/models/token.rb
require "securerandom"

class User < ActiveRecord::Base
  def generate_token
    begin
      self.token = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
    end while self.class.exists?(token: token)
  end

  def qr_code
    RQRCode::QRCode.new(
      Rails.application.routes.url_helpers.url_for(
        controller: "session",
        action: "create",
        email: email,
        token: token
      )
    )
  end
end

然后在您的应用程序中的某处显示此QRCode

# app/views/somewhere.html.erb
<%= @token.qr_code.as_html %>

然后连接应用程序的路由和控制器以处理生成的进程 和编码的QRCode URL

# config/routes.rb
Rails.application.routes.draw do
  # ...
  get "/login", to: "sessions#new"
end

# app/controller/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email], token: params[:token])
    if user
      session[:user_id] = user.id # login user
      user.update(token: nil) # nullify token, so it cannot be reused
      redirect_to user
    else
      redirect_to root_path
    end
  end
end

<强>参考文献:

答案 1 :(得分:3)

我添加了一个新答案有两个原因:

1。 Acacia重新解决了这个问题,重点是什么是App的重定向     有QR码的页面正在查看,我在初始时没有提到     解决方案是由于对问题的误解,

2。有些人发现第一个答案很有帮助,这个新答案会有所帮助     改变它虽然相似,但不再相同

  

扫描QR时,我可以重新加载显示的页面   然后重定向到另一个页面

     

- 金合欢

为了达到这个目的,需要在某种程度上开放连接 显示QRCode的页面,解释说QRCode的内容可以 用它来实现它。但是,由于您试图模仿的应用程序 要求只有那个查看页面的用户才会生效,而不是 实际上已登录,需要页面中的某些内容是唯一的。

要解决此问题,您需要做一些事情:

  1. 用于识别未登录用户的唯一令牌可用于联系/ 受外部浏览器的影响

  2. 一种使用JavaScript登录的方法,以便将查看的页面更新为 在上一步的事件

  3. 之后记录
  4. 某种身份验证令牌之间可以进行交换 应用程序和外部QRCode扫描仪应用程序,以便 将自己身份验证为特定用户

  5. 以下解决方案将以上第3步结果排除在外 演示这个想法,主要集中在服务器端 应用。话虽这么说,第三步的解决方案应该很简单 通过将知道用户身份验证令牌附加到其中的URL来传递 QRCode作为附加参数(并将其作为POST请求提交, 而不是在本演示中作为GET请求。)

    您需要使用一些随机令牌来验证用户和 通过嵌入在QCcode中的URL进行交换; e.g。

    $ rails generate model Token type:string value:string user:belongs_to
    

    type是Rails中的reserverd关键字,用于单表继承。 它将用于特定的不同种类/专用令牌 应用

    生成可在URL中使用并对其进行编码的唯一标记值 进入QRCode,使用类似以下模型和代码的东西:

    # Gemfile
    gem "rqrcode" # QRCode generation
    
    # app/models/token.rb
    require "securerandom" # used for random token value generation
    
    class Token < ApplicationRecord
      before_create :generate_token_value
    
      belongs_to :user
    
      def generate_token_value
        begin
          self.value = SecureRandom.urlsafe_base64 #=> "b4GOKm4pOYU_-BOXcrUGDg"
        end while self.class.exists?(value: value)
      end
    
      def qr_code(room_id)
        RQRCode::QRCode.new(consume_url(room_id))
      end
    
      def consume_url(room_id)
        Rails.application.routes.url_helpers.url_for(
          host: "localhost:3000",
          controller: "tokens",
          action: "consume",
          user_token: value,
          room_id: room_id
        )
      end
    end
    
    # app/models/external_token.rb
    class ExternalToken < Token; end
    
    # app/models/internal_token.rb
    class InternalToken < Token; end
    
    • InternalTokens将仅在应用程序本身中使用,并且是 短命

    • ExternalTokens将仅用于与应用程序进行交互 外;喜欢你的移动QRCode扫描仪应用程序;用户在哪里 先前已注册或已登录以允许 此身份验证令牌将生成并存储在外部应用程序中

    然后在您的应用程序中的某处显示此QRCode

    # e.g. app/views/tokens/show.html.erb
    <%= @external_token.qr_code(@room_id).as_html.html_safe %>
    

    我还会隐藏应用程序的@room_id标记内的当前<head> 使用以下内容:

    # e.g. app/views/tokens/show.html.erb
    <%= content_for :head, @room_id.html_safe %>
    
    # app/views/layouts/application.html.erb
    <!DOCTYPE html>
    <html>
      <head>
        <title>QrcodeApp</title>
        <!-- ... -->
    
        <%= tag("meta", name: "room-id", content: content_for(:head))  %>
        <!-- ... -->
      </head>
    
      <body>
        <%= yield %>
      </body>
    </html>
    

    然后连接应用程序的路由和控制器以处理生成的路由 和编码的QRCode URL。

    对于我们需要的路线:

    1. 路由以呈现QRCode令牌; "token#show"
    2. 使用/处理QRCode令牌的路由; "token#consume"
    3. 通过AJAX路由用户登录用户; "sessions@create"
    4. 我们还需要一些在显示令牌页面中打开连接的方法 可以与之互动以强制它登录,因为我们需要:

      mount ActionCable.server => "/cable"
      

      这将需要Rails 5和ActionCable来实现,否则需要另外一个 发布/子解决方案;像Faye;将需要使用旧版本。

      所有路线看起来都像这样:

      # config/routes.rb
      Rails.application.routes.draw do
        # ...
      
        # Serve websocket cable requests in-process
        mount ActionCable.server => "/cable"
      
        get "/token-login", to: "tokens#consume"
        post "/login", to: "sessions#create"
        get "/logout", to: "sessions#destroy"
        get "welcome", to: "welcome#show"
      
        root "tokens#show"
      end
      

      然后,这些操作的控制器如下:

      # app/controller/tokens_controller.rb
      class TokensController < ApplicationController
        def show
          # Ignore this, its just randomly, grabbing an User for their Token. You
          # would handle this in the mobile application the User is logged into
          session[:user_id] = User.all.sample.id
          @user = User.find(session[:user_id])
          # @user_token = Token.create(type: "ExternalToken", user: @user)
          @user_token = ExternalToken.create(user: @user)
      
          # keep this line
          @room_id = SecureRandom.urlsafe_base64
        end
      
        def consume
          room_id = params[:room_id]
          user_token = params[:user_token] # This will come from the Mobile App
      
          if user_token && room_id
            # user = Token.find_by(type: "ExternalToken", value: user_token).user
            # password_token = Token.create(type: "InternalToken", user_id: user.id)
            user = ExternalToken.find_by(value: user_token).user
            password_token = InternalToken.create(user: user)
      
            # The `user.password_token` is another random token that only the
            # application knows about and will be re-submitted back to the application
            # to confirm the login for that user in the open room session
            ActionCable.server.broadcast("token_logins_#{room_id}",
                                         user_email: user.email,
                                         user_password_token: password_token.value)
            head :ok
          else
            redirect_to "tokens#show"
          end
        end
      end
      

      令牌控制器show操作主要为@room_id生成show值 在视图模板中重用。 consume中的其余代码仅用于 证明了这种应用。

      令牌控制器room_id操作需要user_tokenInternalToken 继续,否则将用户重定向回QRCode登录页面。当它们是 然后,它会生成与用户关联的ExternalToken 然后它将用于将通知/事件推送到的room_id 所有房间都有InternalToken(其中只有一个是独一无二的 用户查看生成此URL的QRCode页面,同时提供 用户必需的身份验证信息(或者在这种情况下我们的 应用程序)快速登录没有密码的应用程序 生成# app/controller/sessions_controller.rb class SessionsController < ApplicationController def create user = User.find_by(email: params[:user_email]) internal_token = InternalToken.find_by(value: params[:user_password_token]) # Token.find_by(type: "InternalToken", value: params[:user_password_token]) if internal_token.user == user session[:user_id] = user.id # login user # nullify token, so it cannot be reused internal_token.destroy # reset User internal application password (maybe) # user.update(password_token: SecureRandom.urlsafe_base64) respond_to do |format| format.json { render json: { success: true, url: welcome_url } } format.html { redirect_to welcome_url } end else redirect_to root_path end end def destroy session.delete(:user_id) session[:user_id] = nil @current_user = nil redirect_to root_path end end 代替使用。

      如果是外部应用程序,您也可以将用户电子邮件作为参数传递 知道它,而不是在这个演示示例中假设它是正确的。

      对于会话控制器,如下:

      user_email

      此会话控制器接收user_password_tokensession[:user_id] 在继续之前,请确保这两个用户在内部匹配相同的用户 登录。然后使用internal_token创建用户会话并销毁 create,因为它只是一次性使用,仅在内部使用 在这种认证申请中。

      以及会话# app/controller/welcome_controller.rb class WelcomeController < ApplicationController def show @user = current_user redirect_to root_path unless current_user end private def current_user @current_user ||= User.find(session[:user_id]) end end 操作的某种欢迎控制器 登录后重定向到

      /cable

      由于此应用程序使用 ActionCable,我们 已经安装了room_id路径,现在我们需要设置一个频道 给定用户特有的。但是,由于用户尚未登录,我们使用 先前由令牌控制器show生成的# app/channels/tokens_channel.rb # Subscribe to `"tokens"` channel class TokensChannel < ApplicationCable::Channel def subscribed stream_from "token_logins_#{params[:room_id]}" if params[:room_id] end end 值 行动,因为它随机而独特。

      room_id

      <head>也嵌入了<div>(尽管它可能是隐藏的 id元素或QRCode的// app/assets/javascripts/channels/tokens.js var el = document.querySelectorAll('meta[name="room-id"]')[0]; var roomID = el.getAttribute('content'); App.tokens = App.cable.subscriptions.create( { channel: 'TokensChannel', room_id: roomID }, { received: function(data) { this.loginUser(data); }, loginUser: function(data) { var userEmail = data.user_email; var userPasswordToken = data.user_password_token; // Mobile App's User token var userData = { user_email: userEmail, user_password_token: userPasswordToken }; // `csrf_meta_tags` value var el = document.querySelectorAll('meta[name="csrf-token"]')[0]; var csrfToken = el.getAttribute('content'); var xmlhttp = new XMLHttpRequest(); // Handle POST response on `onreadystatechange` callback xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == XMLHttpRequest.DONE ) { if (xmlhttp.status == 200) { var response = JSON.parse(xmlhttp.response) App.cable.subscriptions.remove({ channel: "TokensChannel", room_id: roomID }); window.location.replace(response.url); // Redirect the current view } else if (xmlhttp.status == 400) { alert('There was an error 400'); } else { alert('something else other than 200 was returned'); } } }; // Make User login POST request xmlhttp.open( "POST", "<%= Rails.application.routes.url_helpers.url_for( host: "localhost:3000", controller: "sessions", action: "create" ) %>", true ); // Add necessary headers (like `csrf_meta_tags`) before sending POST request xmlhttp.setRequestHeader('X-CSRF-Token', csrfToken); xmlhttp.setRequestHeader("Content-Type", "application/json"); xmlhttp.send(JSON.stringify(userData)); } }); 属性,由您决定),这意味着 它可以在我们的JavaScript中用于接收传入的电路板广播 到那个房间/ QRCode; e.g。

      received

      此ActionCable订阅中只有两个操作;

        ActionCable需要
      1. loginUser来处理传入的请求/事件,
      2. loginUser我们的自定义功能
      3. userData执行以下操作:

        • 处理传入数据以构建新的数据对象user_email以POST回 我们的应用程序,包含用户信息; user_password_token&amp; new XMLHttpRequest();需要使用身份验证登录AJAX 令牌作为密码(因为它有点不安全,密码通常是 散列;意思是他们不知道,因为他们无法逆转)

        • 在没有jQuery的情况下创建一个userData对象POST,发送一个 使用xmlhttp.onreadystatechange作为登录信息的JSON登录URL的POST请求, 同时还附加当前的HTML页面CSRF令牌; e.g。

          否则JSON请求将在没有它的情况下失败

        • 在a上执行的xmlhttp.send(...)回调函数 从{{1}}函数调用回复。它将取消订阅 来自当前房间的用户,因为它不再需要,并重定向 当前页面到&#34; Welcomw页面&#34;它在回复中收到了回复。除此以外 它会警告用户出现故障或出错的情况

        这将产生以下类型的应用

        An image demonstrating the application, using multiple private browser sessions to log in the other request browsers via their given URLs

        您可以通过以下网址访问我所处理项目的副本:

        唯一没有解决的问题是滚动室令牌生成, 哪个要么需要JavaScript库来生成/重新生成 具有Room Token的URL或返回重新生成的Controller Action QRCode可以是图像或HTML,可以立即显示在 页。这两种方法仍然需要你有一些JavaScript关闭 当前连接并打开一个新的房间/会话令牌,可以 使用时只能在一定时间后才能收到消息。

        <强>参考文献: