over 2 years ago
  1. Sessions,半永久性:登入必須要用session。瀏覽器關掉後,session就會被刪除。
    Sessions:web applications requiring user login must use a session, which is a semi-permanent connection between two computers (such as a client computer running a web browser and a server running Rails).

  2. Rails 是用cookies達到sessions的效果,並將cookies放到瀏覽器內。譬如,存入user id,就不用一直重復存取資料庫。
    The most common techniques for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a user id) that can be used by the application to retrieve the logged-in user from the database.

  3. session method:暫時的sessions,Session Controller不會用到database(所以不需要用到Model)
    In this section and Section 8.2, we’ll use the Rails method called session to make temporary sessions that expire automatically on browser close。
    Unlike the Users resource, which uses a database back-end (via the User model) to persist data, the Sessions resource will use cookies, and much of the work involved in login comes from building this cookie-based authentication machinery.

  4. cookies method:生存期比較長的是cookies
    Section 8.4 we’ll add longer-lived sessions using another Rails method called cookies.

  5. find: raises an exception if the user id doesn’t exist.
    find_by:returns nil (indicating no such user) if the id is invalid.

  6. Sessions_Controller其實也是符合RESTful,new / create / destroy
    The elements of logging in and out correspond to particular REST actions of the Sessions controller: the login form is handled by the new action (covered in this section), actually logging in is handled by sending a POST request to the create action (Section 8.2), and logging out is handled by sending a DELETE request to the destroy action (Section 8.3).

  7. Sign in page用到的就是Sessions_Controller (註冊是用到User#new)

    routes.rb
    get    'login'   => 'sessions#new'
    post   'login'   => 'sessions#create'
    delete 'logout'  => 'sessions#destroy'
    
  8. 註冊錯誤會自動出現錯誤訊息 =>Active Record 。不過登入錯誤不會有,因為Session不是Active Record object。
    In Section 7.3.3, we used an error-messages partial to display error messages, but we saw in that section that those messages are provided automatically by Active Record. This won’t work for session creation errors because the session isn’t an Active Record object, so we’ll render the error as a flash message instead.

  9. User有Model,在使用form_for所以可以使用@user。不過,因為Session沒有Model,所以沒有@session這種東西,要把resource name跟相關的URL放上去:form_for(:session, url: login_path)
    Recall from Listing 7.13 that the signup form uses the form_for helper, taking as an argument the user instance variable @user:

    <%= form_for(@user) do |f| %>
    .
    .
    .
    <% end %>
    

    The main difference between the session form and the signup form is that we have no Session model, and hence no analogue for the @user variable. This means that, in constructing the new session form, we have to give form_for slightly more information; in particular, whereasform_for(@user)allows Rails to infer that the action of the form should be to POST to the URL /users, in the case of sessions we need to indicate the name of the resource and the corresponding URL:form_for(:session, url: login_path)With the proper form_for in hand, it’s easy to make a login form to match the mockup in Figure 8.1 using the signup form (Listing 7.13) as a model, as shown in Listing 8.2.

  10. params[:session][:email] and params[:session][:password]
    login form will result in a params hash where params[:session][:email] and params[:session][:password] correspond to the email and password fields.

  11. &&只有兩邊的值都是true時,結果才會是true。如果有一邊是false或nil,就會回傳false。
    This uses && (logical and) to determine if the resulting user is valid. Taking into account that any object other than nil and false itself is true in a boolean context (Section 4.2.3), the possibilities appear as in Table 8.2. We see from Table 8.2 that the if statement is true only if a user with the given email both exists in the database and has the given password, exactly as required.

  12. 有Model的,才會有Rails內建的error message(user.errors.full_messages)(<% @user.errors.full_messages.each do |msg| %>)。session因為只有controller,沒有model,所以只好自己寫(flash[:danger] = 'Invalid email/password combination')。
    (7.3.3 Signup error messages:As a final step in handling failed user creation, we’ll add helpful error messages to indicate the problems that prevented successful signup. Conveniently, Rails automatically provides such messages based on the User model validations. For example, consider trying to save a user with an invalid email address and with a password that’s too short:Here the errors.full_messages object (which we saw briefly before in Section 6.2.2) contains an array of error messages.)

  13. flash.now會在下一頁消失。
    flash.now disappear as soon as there is an additional request

    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # Log the user in and redirect to the user's show page.
    
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
    end
    
  14. 產生Sessions_controller的時候,自動會產生Sessions_helper,Veiw就可以馬上用這些helper。不過,controller要include module之後,才能夠使用helper。所以要記得Application controllerli中,把她include進來。
    Conveniently, a Sessions helper module was generated automatically when generating the Sessions controller (Section 8.1.1). Moreover, such helpers are automatically included in Rails views; by including the module into the base class of all controllers (the Application controller), we arrange to make them available in our controllers as well (Listing 8.11).

    Listing 8.11: Including the Sessions helper module into the Application controller.
    app/controllers/application_controller.rb
    class ApplicationController < ActionController::Base
    protect_from_forgery with: :exception
    include SessionsHelper
    end
    

    With this configuration complete, we’re now ready to write the code to log users in.

  15. Rails有內建的session method,可以用來產生session[:user_id] = user.id,會把user.id自動加密之後處存在瀏覽器內,然後可以攜帶到其他頁面去。
    Logging a user in is simple with the help of the session method defined by Rails. (This method is separate and distinct from the Sessions controller generated in Section 8.1.1.) We can treat session as if it were a hash, and assign to it as follows:session[:user_id] = user.idThis places a temporary cookie on the user’s browser containing an encrypted version of the user’s id, which allows us to retrieve the id on subsequent pages using session[:user_id]. In contrast to the persistent cookie created by the cookies method (Section 8.4), the temporary cookie created by the session method expires immediately when the browser is closed.

  16. 登入,log_in,session[:user_id] = user.id會在sessions_controller.rb跟users_controller.rb用到,可以在sessions_helper.rb定義一個log_in method。
    登出,log_out,用destroy,只會在一個地方用到,在sessions_helper.rb定義一個log_out method
    Because we’ll want to use the same login technique in a couple of different places, we’ll define a method called log_in in the Sessions helper, as shown in Listing 8.12.

    Listing 8.12: The log_in function. app/helpers/sessions_helper.rb
    module SessionsHelper
    # Logs in the given user.
    
    def log_in(user)
    session[:user_id] = user.id
    end
    end
    
  17. 我的理解是,如果用find,會直接出現錯誤訊息。但是用find_by,因為是出現nil,所以程式還是可以繼續運作下去,接著作nil該作的事情。find: raises an exception if the user id doesn’t exist.find_by:returns nil (indicating no such user) if the id is invalid.
    But recall from Section 6.1.4 that find raises an exception if the user id doesn’t exist. This behavior is appropriate on the user profile page because it will only happen if the id is invalid, but in the present case session[:user_id] will often be nil (i.e., for non-logged-in users). To handle this possibility, we’ll use the same find_by method used to find by email address in the create method, with id in place of emailUser.find_by(id: session[:user_id])Rather than raising an exception, this method returns nil (indicating no such user) if the id is invalid.We could now define the current_user method as follows:

    def current_user
    User.find_by(id: session[:user_id])
    end
    
  18. 寫成這樣,會一直讀取資料庫,所以把寫成@會比較好。

    def current_user
    User.find_by(id: session[:user_id])
    end
    

    可以先簡化成:

    if @current_user.nil?
    @current_user = User.find_by(id: session[:user_id])
    else
    @current_user
    end
    

    然後變這樣:
    @current_user = @current_user || User.find_by(id: session[:user_id])
    最後變這樣:
    @current_user ||= User.find_by(id: session[:user_id])

  19. ||= (“or equals”)
    This uses the potentially confusing but frequently used ||= (“or equals”) operator (Box 8.1).

  20. 登入跟目前使用者的設定都在sessions_helper.rb

    app/helpers/sessions_helper.rb
     module SessionsHelper
    # Logs in the given user.
    
    def log_in(user)
    session[:user_id] = user.id
    end
    # Returns the current logged-in user (if any).
    
    def current_user
    @current_user ||= User.find_by(id: session[:user_id])
    end
    end
    
  21. 根據user有沒有登入,決定要去哪一個頁面。 xdite的建議

    <% if logged_in? %>
    # Links for logged-in users
    <% else %>
    # Links for non-logged-in-users
    
    <% end %>
    
  22. logged_in?會回傳true or false。!current_user.nil?如果current_user不是nil。
    logged_in? boolean method:
    A user is logged in if there is a current user in the session, i.e., if current_user is not nil. Checking for this requires the use of the “not” operator (Section 4.2.3), written using an exclamation point ! and usually read as “bang”. The resulting logged_in? method appears in Listing 8.15.

    app/helpers/sessions_helper.rb
    module SessionsHelper
    # Logs in the given user.
    
    def log_in(user)
    session[:user_id] = user.id
    end
    # Returns the current logged-in user (if any).
    
    def current_user
    @current_user ||= User.find_by(id: session[:user_id])
    end
    # Returns true if the user is logged in, false otherwise.
    
    def logged_in?
    !current_user.nil?
    end
    end
    
  23. dropdowndropdown-menu是 Bootstrap 的功能,要在application.js加上//= require bootstrap才會動。
    As part of including the new links into the layout, Listing 8.16 takes advantage of Bootstrap’s ability to make dropdown menus.8 Note in particular the inclusion of the special Bootstrap CSS classes such as dropdown, dropdown-menu, etc. To activate the dropdown menu, we need to include Bootstrap’s custom JavaScript library in the Rails asset pipeline’s application.js file, as shown in Listing 8.17.

    app/assets/javascripts/application.js
     //= require jquery
    //= require jquery_ujs
    //= require bootstrap
    //= require turbolinks
    //= require_tree .
    
  24. has_secure_password是rails 內建的method,加到User.rb之後,就可以有hashed password_digest attribute,password and password_confirmationauthenticate method。
    The ability to save a securely hashed password_digest attribute to the database
    A pair of virtual attributes18 (password and password_confirmation), including presence validations upon object creation and a validation requiring that they match
    An authenticate method that returns the user when the password is correct (and false otherwise)

  25. 登出,log_out,用destroy,只會在一個地方用到,在sessions_helper.rb定義一個log_out method

    app/helpers/sessions_helper.rb
    module SessionsHelper
    # Logs in the given user.
    
    def log_in(user)
    session[:user_id] = user.id
    end
    # Logs out the current user.
    
    def log_out
    session.delete(:user_id)
    @current_user = nil
    end
    end
    
    app/controllers/sessions_controller.rb
    class SessionsController < ApplicationController
    def new
    end
    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
    log_in user
    redirect_to user
    else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
    end
    end
    def destroy
    log_out
    redirect_to root_url
    end
    end
    
  26. remember token: 用cookies method產生永久性cookiesremember digest:授權給remember token
    remember token appropriate for creating permanent cookies using the cookies method, together with a secure remember digest for authenticating those tokens.

  27. remember token是隨機產生的string,會被保留在瀏覽器裡面,瀏覽器裡面還會有被加密過的使用者id
    hash過的remember token則會存在資料庫裡面,就變成remember digist
    當接收到瀏覽器裡面的cookie時,cookie內含的使用者id,將被用來在資料庫中找到相同的使用者
    然後cookie裡面的remember token跟資料庫裡面的remember digist(也就是加密過的remember token)做比較。
    Create a random string of digits for use as a remember token.
    Place the token in the browser cookies with an expiration date far in the future.
    Save the hash digest of the token to the database.
    Place an encrypted version of the user’s id in the browser cookies.
    When presented with a cookie containing a persistent user id, find the user in the database using the given id, and verify that the remember token cookie matches the associated hash digest from the database.

  28. 使用Rails內建的SecureRandom moduleurlsafe_base64 method,去產生long random string去當作remember token
    The urlsafe_base64 method from the SecureRandom module in the Ruby standard library fits the bill

  29. 如果沒有用self的話,會產生local variable
    self還可以定義成userattribute->remember_token (attr_accessor :remember_token)。
    User的attribute只有remember_digest(資料庫內),並沒有remember_token(因為只需要存在瀏覽器內,不需要存在資料庫),但是我們又需要用到remember_token,所以我們要用self去產生remember_token這個"attribute"。
    (意思好像是,self可以無中生有出一個暫時的attribute)
    update_attributes可以一次更新很多個attributes,
    update_attribute一次只能更新一個attribute,但是可以避免產生錯誤訊息。
    (Listing 8.18:new_token這個method不需要object,所以直接弄成Class Method)
    (Remembering users involves creating a remember token and saving the digest of the token to the database. We’ve already defined a digest method for use in the test fixtures (Listing 8.18), and we can use the results of the discussion above to create a new_token method to create a new token. As with digest, the new token method doesn’t need a user object, so we’ll make it a class method.19 The result is the User model shown in Listing 8.31.)

    def User.new_token
    SecureRandom.urlsafe_base64
    end
    

    Our plan for the implementation is to make a user.remember method that associates a remember token with the user and saves the corresponding remember digest to the database. Because of the migration in Listing 8.30, the User model already has a remember_digest attribute, but it doesn’t yet have a remember_token attribute. We need a way to make a token available via user.remember_token (for storage in the cookies) without storing it in the database. We solved a similar issue with secure passwords in Section 6.3, which paired a virtual password attribute with a secure password_digest attribute in the database. In that case, the virtual password attribute was created automatically by has_secure_password, but we’ll have to write the code for a remember_token ourselves. The way to do this is to use attr_accessor to create an accessible attribute, which we saw before in Section 4.4.5:
    Note the form of the assignment in the first line of the remember method. Because of the way Ruby handles assignments inside objects, without self the assignment would create a local variable called remember_token, which isn’t what we want. Using self ensures that assignment sets the user’s remember_token attribute. (Now you know why the before_save callback from Listing 6.31 uses self.email instead of just email.) Meanwhile, the second line of remember uses the update_attribute method to update the remember digest. (As noted in Section 6.1.5, this method bypasses the validations, which is necessary in this case because we don’t have access to the user’s password or confirmation.)

  30. (1)用 SecureRandom.urlsafe_base64先產生一個夠長的亂數,放到remember_token裡面。
    (2)再用digestremember_token加密。
    (3)然後把加密過的內容存到資料庫的:remember_digest裡面。

    app/models/user.rb
    class User < ActiveRecord::Base
    attr_accessor :remember_token
    has_secure_password
    validates :password, presence: true, length: { minimum: 6 }
    # Returns the hash digest of the given string.
    
    def User.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
    end
    (1)# Returns a random token.
    
    def User.new_token
    SecureRandom.urlsafe_base64
    end
    # Remembers a user in the database for use in persistent sessions.
    
    def remember
    (2)self.remember_token = User.new_token
    (3)update_attribute(:remember_digest, User.digest(remember_token))
    end
    end
    
  31. cookie method:是一個hash,分兩個部分:一個是值,一個是到期日。
    A cookie consists of two pieces of information, a value and an optional expires date. For example, we could make a persistent session by creating a cookie with value equal to the remember token that expires 20 years from now:

    cookies[:remember_token] = { value:   remember_token,
                             expires: 20.years.from_now.utc }
    
  32. 20年可以寫成permanent
    permanent method to implement it, so that we can simply writecookies.permanent[:remember_token] = remember_token

  33. cookies[:user_id] = user.id需要加密成:cookies.signed[:user_id] = user.id。(cookie本身可能沒有加密的功能,所以還要加上signed才能加密)再加上時間會變成:cookies.permanent.signed[:user_id] = user.id
    Because it places the id as plain text, this method exposes the form of the application’s cookies and makes it easier for an attacker to compromise user accounts. To avoid this problem, we’ll use a signed cookie, which securely encrypts the cookie before placing it on the browser:

  34. User.find_by(id: cookies.signed[:user_id])之後的頁面就可以用這個來叫出user。
    After the cookies are set, on subsequent page views we can retrieve the user with code like User.find_by(id: cookies.signed[:user_id])where cookies.signed[:user_id] automatically decrypts the user id cookie. We can then use bcrypt to verify that cookies[:remember_token] matches the remember_digest generated in Listing 8.32. (In case you’re wondering why we don’t just use the signed user id, without the remember token, this would allow an attacker with possession of the encrypted id to log in as the user in perpetuity. In the present design, an attacker with both cookies can log in as the user only until the user logs out.)

  35. secure password source code裡面:, BCrypt::Password.new(password_digest) == unencrypted_password,我們可以轉換成BCrypt::Password.new(remember_digest) == remember_token
    If you think about it, this code is really strange: it appears to be comparing a bcrypt password digest directly with a token, which would imply decrypting the digest in order to compare using ==. But the whole point of using bcrypt is for hashing to be irreversible, so this can’t be right.

  36. bcrypt gem 裡面的 ==已經被重新定義成BCrypt::Password.new(remember_digest).is_password?(remeing redef
    Indeed, digging into the source code of the bcrypt gem verifies that the comparison operator == is being redefined, and under the hood the comparison above is equivalent to the following: BCrypt::Password.new(remember_digest).is_password?(remember_token)Instead of ==, this uses the boolean method is_password? to perform the comparison. Because its meaning is a little clearer, we’ll prefer this second comparison form in the application code.

  37. user.rb加入authenticated?(remember_token)之後,就可以remember user放到sessions_controller.rb裡面去了。

    app/models/user.rb
    def authenticated?(remember_token)
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
    end
    end
    
    app/controllers/sessions_controller.rb
    class SessionsController < ApplicationController
    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      log_in user
      remember user
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
    end
    end
    
  38. 整個過程好像是:先到sessions_controller.rb,create時會遇到remember user,然後會跑到sessions_helper.rbremember(user),第一個步驟是回到user.rbdef remember去產生self.remember_token = User.new_tokenupdate_attribute(:remember_digest, User.digest(remember_token)),把資料庫內的:remember_digest產生出來後,就會繼續第二個步驟去產生cookies,也就是cookies.permanent.signed[:user_id] = user.idcookies.permanent[:remember_token] = user.remember_token

  39. 如果有session[:user_id]就用session[:user_id]登入。不然,就去找cookies[:user_id],改用cookies[:user_id]登入。
    In the case of persistent sessions, we want to retrieve the user from the temporary session if session[:user_id] exists, but otherwise we should look for cookies[:user_id] to retrieve (and log in) the user corresponding to the persistent session. We can accomplish this as follows:

    app/helpers/sessions_helper.rb
    if session[:user_id]
    @current_user ||= User.find_by(id: session[:user_id])
    elsif cookies.signed[:user_id]
    user = User.find_by(id: cookies.signed[:user_id])
    if user && user.authenticated?(cookies[:remember_token])
    log_in user
    @current_user = user
    end
    end
    
  40. 登入之後,cookies要等20年才會失效的解決方法是:先在user.rb設定,將:remember_digest更新為nil,然後在sessions_helper.rb裡面就可以用forget去登出。

    app/models/user.rb
    def forget
    update_attribute(:remember_digest, nil)
    end
    
    module SessionsHelper
    # Logs in the given user.
    def log_in(user)
    session[:user_id] = user.id
    end
    # Forgets a persistent session.
    def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
    end
    # Logs out the current user.
    def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
    end
    end
    
  41. 兩個問題待解:
    開多個視窗的時候,在其中一個登出,然後到另一個視窗要登出的時候,就會出現錯誤訊息 => 必須去檢查有沒有current_user。
    登出第一個視窗(Firefox),可是還沒登出第二個視窗(Chrome),就把第二個視窗關掉又重開:第一個視窗"登出"後,會把資料庫裡面的remember digest刪除。當把第二個視窗關掉的時候,因為只是關掉,不是登出,所以只會刪除session[:user_id],而cookies.signed[:user_id]還會存在,可是因為remember digest已經被刪掉了,所以會拋出錯誤訊息。 => To fix this, we want authenticated? to return false instead.

    app/controllers/sessions_controller.rb
    class SessionsController < ApplicationController
    def destroy
    log_out if logged_in?
    redirect_to root_url
    end
    end
    
    app/models/user.rb
    class User < ActiveRecord::Base
    # Returns true if the given token matches the digest.
    
    def authenticated?(remember_token)
    return false if remember_digest.nil?
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
    end
    def forget
    update_attribute(:remember_digest, nil)
    end
    end
    
  42. return false可以直接跳過後面要執行的code。
    This uses the return keyword to return immediately if the remember digest is nil, which is a common way to emphasize that the rest of the method gets ignored in that case. The equivalent code

    if remember_digest.nil?
    false
    else
    BCrypt::Password.new(remember_digest).is_password?(remember_token)
    end
    
  43. view作好之後,當送出form的時候,會送出去:params[:session][:remember_me],因此可以寫成:

    if params[:session][:remember_me] == '1'
    remember(user)
    else
    forget(user)
    end
    

    或:params[:session][:remember_me] == '1' ? remember(user) : forget(user)
    更新到這邊:

    app/controllers/sessions_controller.rb
    class SessionsController < ApplicationController
    def new
    end
    def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
    log_in user
    params[:session][:remember_me] == '1' ? remember(user) : forget(user)
    redirect_to user
    else
    flash.now[:danger] = 'Invalid email/password combination'
    render 'new'
    end
    end
    def destroy
    log_out if logged_in?
    redirect_to root_url
    end
    end
    
  44. 這邊的運作流程是:使用者填寫登入表單後,會將params送到app/controllers/sessions_controller.rb,驗證user && user.authenticate(params[:session][:password])後之,就會把使用者登入,如果有勾選remember的checkbox的時候,瀏覽器會送出:paparams[:session][:remember_me] == '1',然後就會有remember(user),然後會由app/helpers/sessions_helper.rb去產生user.remembercookies.permanent.signed[:user_id] = user.idcookies.permanent[:remember_token] = user.remember_token,其中的user.remember會去執行app/models/user.rbremember method,去產生update_attribute(:remember_digest, User.digest(remember_token))

  45. 我猜的:如果沒有勾remember me,會執行forget(user),就會變成update_attribute(:remember_digest, nil),所以使用者就不能夠使用資料庫裡面的:remember_digest去作永久性的登入。

← RWD 水平置中 待解的問題 →