ruby on rails使用HTTP认证token实现用户登录验证和会话保持
增加认证(Authentication)
认证的过程是这样的: 用户把用户名和密码通过 HTTP POST 请求发送到我们的 API (在这里我们使用 sessions 端点来处理这个请求), 如果用户名和密码匹配,我们会把 token
发送给用户。 这个 token 就是用来证明用户身份的凭证。然后在以后的每个请求中,我们都通过这个 token 来查找用户,如果没有找到用户则返回 401 错误。
给 Member 模型增加 authentication_token 属性
1 | rails g migration add_authentication_token_to_members |
db/migrate/20180510152021_add_authentication_token_to_members.rb :
1 | class AddAuthenticationTokenToMembers < ActiveRecord::Migration |
$ rake db:migrate
生成 authentication_token
app/models/member.rb,
1 | class Member < ActiveRecord::Base |
这里给 Member 模型增加了一个 reset_auth_token!
方法,这样做的理由主要有以下几点:
- 需要有一个方法帮助用户重置 authentication token, 而不仅仅是在创建用户时生成 authenticeation token;
- 如果用户的 token 被泄漏了,我们可以通过 reset_auth_token! 方法方便地重置用户 token ;
sessions endpoint
生成 sessions 控制器
1 | 我们不需要生成资源文件 |
app/controllers/api/v1/sessions_controller.rb
1 | class Api::V1::SessionsController < Api::V1::BaseController |
实现 api_error 和 current_member 方法
app/controllers/api/v1/base_controller.rb :
1 | class Api::V1::BaseController < ApplicationController |
现在还需要做一些额外工作:
- 给 Member 模型增加密码验证,整理数据库,给数据库中已存在的测试用户增加相关字段;
- 实现 app/views/api/v1/sessions/create.json.jbuilder;
- 配置和 sessions 相关的路由;
给 Member 模型增加密码验证,整理数据库
在 Gemfile 里将 gem ‘bcrypt’ 这一行的注释取消
1 | # Use ActiveModel has_secure_password |
app/models/member.rb :
1 | class Member < ActiveRecord::Base |
给 Member 模型增加 password_digest
属性:
1 | rails g migration add_password_digest_to_members |
db/migrate/20180510153029_add_password_digest_to_members.rb :
1 | class AddPasswordDigestToMembers < ActiveRecord::Migration |
执行:
1 | $ bundle install |
整理之前的用户数据。给数据库中已存在的测试用户增加 password 和 authentication token,这个任务可以在 rails console 下完成。
首先启动 rails console :
$ rails c
然后在 rails console 里执行:
1 | Member.all.each {|member| |
实现 app/views/api/v1/sessions/create.json.jbuilder
app/views/api/v1/sessions/create.json.jbuilder :
1 | if @member |
配置和 sessions 相关的路由
1 | Rails.application.routes.draw do |
现在做一个测试看是否能够顺利地拿到用户的 token, 我们使用下面的用户作为测试用户:
1 | { |
浏览器console执行:
1 | # ajax request |
1 | {"session":{"id":2,"username":"222","nickname":"222333","role":"admin","token":"ji14ZeekYZCtJ0tShU88rgQuRsym/XEOnO+01SjWr94DXYzSlIoKzuBQUYmvnxrcHNGgNuqX+ey/1jKkgx0jrg=="}} |
顺利地拿到了 token。我们再做一个验证失败的测试,使用一个错误的密码: fakepwd
1 | curl -i -X POST -d "member[username]=222&member[password]=fakepwd" http://localhost:3000/api/v1/sessions.json |
1 | # ajax request |
1 | HTTP/1.1 401 Unauthorized |
此时服务器返回了 401 Unauthorized
Authenticate Member
在前面的测试中,我们已经成功地拿到了用户的 token, 那么现在我们把 token 和 username 发给 API,看能否成功识别出用户。
首先在 Api::V1::BaseController
里实现 authenticate_member!
方法:
app/controllers/api/v1/base_controller.rb,
1 | class Api::V1::BaseController < ApplicationController |
ActionController::HttpAuthentication::Token
是 rails 自带的方法,可以参考 rails 文档 了解其详情。
这里通过 member_username
拿到 member
,然后通过 ActiveSupport::SecurityUtils.secure_compare
对 member.authentication_token
和从请求头里取到的 token 进行比较,如果匹配则认证成功,否则返回 unauthenticated!
。这里使用了 secure_compare
对字符串进行比较,是为了防止时序攻击(timing attack)
我们构造一个测试用例, 这个测试用例包括以下一些步骤:
- 用户登录成功, 服务端返回其 username, token 等数据
- 用户请求 API 更新其 nickname, 用户发送的 token 合法, 更新成功
- 用户请求 API 更新其 nickname, 用户发送的 token 非法, 更新失败
为了让用户能够更新其 nickname, 我们需要实现 member update
API, 并且加入用户验证 before_action
app/controllers/api/v1/members_controller.rb,
1 | class Api::V1::MembersController < Api::V1::BaseController |
app/views/api/v1/members/update.json.jbuilder,
1 | json.member do |
现在我们进行测试, 测试用户是:
1 | { |
1 | $ curl -i -X PUT -d "member[nickname]=new-member" \ |
我们看到 member nickname 已经成功更新。
请注意: 你们自己测试时需要将 token 换为你们自己生成的 token。
我们使用一个非法的 token 去请求 API, 看看会发生什么状况。
1 | $ curl -i -X PUT -d "member[nickname]=new-member" \ |
服务器返回 401 Unauthorized, 并且 member nickname 没有被更新。