简体   繁体   English

Rails 6 API + React + Google 登录:登录后如何授权用户访问某些服务器路由?

[英]Rails 6 API + React + Google login: how to authorize user to access certain server routes after login?

Please let me know if you need more information.如果您需要更多信息,请告诉我。 I'm building a website with a React frontend and Rails 6, which is in api-only mode.我正在构建一个带有 React 前端和 Rails 6 的网站,该网站处于 api-only 模式。 I've added Google login to the frontend using the react-google-login component.我已经使用 react-google-login 组件将 Google 登录添加到前端。 Once a user has logged in on the frontend, I send the ID token provided by Google to the server, which validates it using the google-id-token gem.用户登录前端后,我将 Google 提供的 ID 令牌发送到服务器,服务器使用 google-id-token gem 对其进行验证。 This all works fine, but afterwards, I'd like the user to be able to post to certain routes on the server.这一切都很好,但之后,我希望用户能够发布到服务器上的某些路由。 From googling, I've found a few different solutions, but they all seem to involve sending cookies back and forth.通过谷歌搜索,我找到了一些不同的解决方案,但它们似乎都涉及来回发送 cookies 。

  1. Using Rails session, store user ID.使用 Rails session,存储用户 ID。 If configuration is set up correctly, this is sent to the frontend as an HttpOnly cookie, which (as long as credentials: true) sends back the cookie in each following get/post request.如果配置设置正确,它将作为 HttpOnly cookie 发送到前端,该 cookie(只要凭据:true)在每个后续的 get/post 请求中发回 cookie。

  2. Using Rails cookies, store user ID or JWT as an HttpOnly cookie.使用 Rails cookies,将用户 ID 或 JWT 存储为 HttpOnly cookie。 Seems basically the same as above but with extra work?似乎与上述基本相同,但需要额外的工作?

  3. Using Authorize header and JWT.使用授权 header 和 JWT。 Not 100% clear on how this works - how does Rails get the JWT to the frontend - also cookies?不是 100% 清楚这是如何工作的——Rails 如何将 JWT 送到前端——还有 cookies?

I've tried all 3 to varying extents, but spent the most time on the first method.我已经在不同程度上尝试了所有 3 种方法,但在第一种方法上花费的时间最多。 However, I can't seem to get Rails to send the session[:token] (that I set after user authentication using Google's ID token) to the frontend.但是,我似乎无法让 Rails 将 session[:token](我在使用 Google 的 ID 令牌进行用户身份验证后设置)发送到前端。 I'm open to switching to another method or one that I haven't listed.我愿意切换到另一种方法或我没有列出的方法。 Here is what my code looks like.这是我的代码的样子。

# application.rb
module MyBackendApi
  class Application < Rails::Application
    # ... other configs
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore


    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'https://my-frontend-app.netlify.app'
        resource '*', 
          credentials: true,
          headers: :any, 
          methods: [:get, :post]
      end
    end

    config.hosts << "my-backend-api.herokuapp.com"
    config.hosts << "localhost"
  end
end

I also have include::ActionController::Cookies in ApplicationController.我在 ApplicationController 中也有include::ActionController::Cookies

Below is how I authenticate the user using google-id-token and set the session hash.下面是我如何使用 google-id-token 验证用户并设置 session hash。

class UsersController < ApplicationController
  def signin
    validator = GoogleIDToken::Validator.new
    begin
      payload = validator.check(user_params[:idtoken], ENV['GOOGLE_CLIENT_ID'])
      user_id = payload['sub']
      user = User.find_by(user_id: user_id)
      if user.nil?
        user = User.create({
          user_id: user_id
        })
      end
      session[:user_id] = user_id
      render json: {status: :ok}
    rescue GoogleIDToken::ValidationError => e
      puts "Cannot validate: #{e}"
      render json: {message: 'Bad Request', reason: 'Error validating Google token ID'}, status: :bad_request
    end
  end

Here is how I check whether a user is logged in or not.这是我检查用户是否登录的方法。 The method logged_in_user is used as a before_action method for the appropriate controller actions.方法logged_in_user用作适当的 controller 操作的before_action方法。

  def current_user
    user_id = session[:user_id]
    if !user_id.nil?
      user ||= User.find_by(user_id: user_id)
    end
  end

  def logged_in?
    !current_user.nil?
  end

  def logged_in_user
    unless logged_in?
      render json: {message: 'Not logged in'}, status: :forbidden
    end
  end

Now for my frontend code.现在是我的前端代码。 Below is how I authenticate the user (taken almost directly from the Google docs).以下是我对用户进行身份验证的方式(几乎直接取自 Google 文档)。 Note that /googlesignin routes to users#signin in the backend API.请注意, /googlesignin路由到后端 API 中的users#signin signin。

    var xhr = new XMLHttpRequest();
    xhr.open('POST', 'https://my-backend-api.herokuapp.com/googlesignin');
    xhr.withCredentials = true;
    xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
    xhr.send('idtoken=' + idtoken);

Then, I try to post to a /submit route in the backend API.然后,我尝试发布到后端 API 中的/submit路由。

      let headers = new Headers({
        'Content-Type': 'application/json'
      });
      fetch('https://my-backend-api.herokuapp.com/submit', {
        method: 'POST',
        mode: 'cors',
        headers: headers,
        cache: 'no-cache',
        redirect: 'follow',
        referrer: 'no-referrer',
        credentials: 'include',
        body: JSON.stringify(this.state.selected),
      })
      .then(function (response) {
        let data = response.json()
        console.log(data)
        return data;
      })

I also tried the following, with axios:我还使用 axios 尝试了以下操作:

      axios.post('https://my-backend-api.herokuapp.com/submit', {
        submission: this.state.selected,
      }, {
        withCredentials: true,
      })
        .then(response => {
          console.log(response)
        })
        .catch(error => {
          console.log(error)
        })

As I'm very new to cookies, I'm not sure whether I should be able to see the session[:user_id] cookie in Chrome devtools (application tab) or not.由于我对 cookies 非常陌生,因此我不确定是否应该能够在 Chrome 开发工具(应用程序选项卡)中看到 session[:user_id] cookie。 I don't see it, so I assume that the frontend is either not accepting or not receiving the cookie from the backend.我没有看到它,所以我假设前端要么不接受要么不接收来自后端的 cookie。 I'm not sure though.不过我不确定。

Any help is super appreciated, I've been struggling with this issue for a while, with no idea what I could be doing wrong.非常感谢任何帮助,我一直在努力解决这个问题,不知道我可能做错了什么。 I have been finding user auth to be a little overwhelming thus far.到目前为止,我一直发现用户身份验证有点压倒性。 Again, I'm willing to switch to another method.同样,我愿意改用另一种方法。 Please let me know if I should post more information.如果我应该发布更多信息,请告诉我。 Thank you!谢谢!

Update: One thing that works is to manually set a JWT Access-Token header on Rails, and on the frontend, take that header and put it into a cookie.更新:有效的一件事是在 Rails 上手动设置 JWT Access-Token header,在前端,将 header 放入 cookie 中。 Then when I send later requests, I stick the cookie into an Authorization header, which I check for and validate in Rails.然后,当我稍后发送请求时,我将 cookie 粘贴到Authorization header 中,我在 Rails 中检查并验证它。 But, is this safe?但是,这样安全吗? Intuitively, it doesn't seem secure?直觉上,它似乎不安全?

I don't think you need to use sessions at all, as that is the main idea by using Rails in API mode.我认为您根本不需要使用会话,因为这是在 API 模式下使用 Rails 的主要思想。

I remember I did something similar like that a few years ago(having the API(Rails API without devise) in one subdomain and the fronted(Backbone.js) in a separate one), in this repository我记得几年前我做过类似的事情(在一个子域中使用 API(Rails API),在一个单独的子域中使用前端(Backbone.js)),在这个存储库中

  • How it looks the current_user based on a token included on each request(This token was generated as a result of the Google authentication interaction)根据每个请求中包含的令牌,它看起来如何current_user (此令牌是由于 Google 身份验证交互而生成的)
https://github.com/heridev/rails_public_api/blob/master/app/controllers/application_controller.rb#L18
  • How do we create the initial user's record for later searching and authentication purposes:我们如何创建初始用户的记录以供以后搜索和身份验证:
https://github.com/heridev/rails_public_api/blob/master/app/controllers/api/sessions_controller.rb#L4

Here is another gist you can take as a reference这是您可以作为参考的另一个要点

In general, the workflow looks like this:一般来说,工作流程如下所示:

  • The frontend calculates the Google URL and allows the user to log in on the G-suite.前端计算 Google URL 并允许用户登录 G-suite。
  • Then a token is returned from Google and received as a query string in the Single Page App of your preference.然后从Google返回一个令牌,并在您偏好的Single Page App中作为查询字符串接收。
  • On that route onLoad , it uses that token to make an HTTP request to the backend API in a separate subdomain.在该路由onLoad上,它使用该token向单独子域中的后端 API 发出 HTTP 请求。
  • In the backend API, we validate the Google token(by making a second HTTP request to Google most likely) and if valid it searches/creates the user and saves that token in the right record.在后端 API 中,我们验证 Google 令牌(通过向 Google 发出第二个 HTTP 请求最有可能),如果有效,它将searches/creates用户并将该令牌保存在正确的记录中。
  • The API endpoint returns the user details(google_token, user_email, etc). API 端点返回用户详细信息(google_token、user_email 等)。
  • The frontend saves that data in localStorage前端将该数据保存在 localStorage
  • On every subsequent request to the API , the frontend includes the google_token , so we can search the actual user by that Google token.在对API的每个后续请求中,前端都包含google_token ,因此我们可以通过该 Google 令牌搜索实际用户。
  • To log out the current_user in the backend it is just a matter of making the User.google_token field as nil要在后端注销current_user ,只需将User.google_token字段设为nil

I'm planning to work on a similar application, but with a more recent Stack(React 17 and Rails 6), if this gets more attention, I can come back in a few and share more details on how that ended up.我计划开发一个类似的应用程序,但使用更新的 Stack(React 17 和 Rails 6),如果这得到更多关注,我可以回来分享更多关于它如何结束的细节。

Last words: No no need for JWT or any devise stuff(devise_token_auth) unless you need to support also a user/password authentication strategy.最后的话:不需要JWT或任何 devise 东西(devise_token_auth),除非您还需要支持user/password身份验证策略。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM