- 编写用户表相关的迁移文件并使用Rails提供的命令创建用户表;
- 编写用户模型的验证;
- 编写用户模型的单元测试,完成相关测试。
今天我们先从用户模块的开发走起。
我们约定:在我们的系统中,用户使用自己的邮箱和密码进行登录和注册。基于这个前提我们来开发我们的用户模块。今天我们先来进行用户模型相关功能的开发,在下一节我们将完成控制器相关功能及对外提供 API 来允许用户使用 路由直接进行各种操作。
- 用户需要邮箱注册,所以需要
邮箱
字段; - 用户需要密码进行登录,所以需要
密码
字段; - 用户拥有不同的权限,在我们的系统中,权限的分类比较简单,我们可以定义一个
角色
字段来实现; - 每个用户都应该由唯一的id,Rails已经为我们提供了默认的id字段,并且是主键,所以我们在这里不用自定义,直接使用默认即可;
- Rails还会自动帮我们维护两个字段:created_at, updated_at 。
字段 | 类型 | 长度 | 注释 | null | 默认值 |
---|---|---|---|---|---|
id | unsigned integer | 11 | 主键id,自动增长 | 否 | 0 |
string | 100 | 邮箱 | 否 | 空字符串 | |
password_digest | string | 256 | 密码 | 否 | 空字符串 |
role | unsigned tiny_int | 2 | 角色:0-管理员 1-普通用户 2-店家 | 否 | 1 |
created_at | timestamp | --- | 创建时间 | 是 | 当前时间 |
updated_at | timestamp | --- | 修改时间 | 是 | 当前时间 |
- 用户注册和登录:email字段信息必须提供
- 用户注册和登录:password_digest 字段信息必须提供
- 角色只能包含:0-管理员 1-普通用户 2-店家
-
使用命令创建模型
$ rails generate model 单数模型名 字段1:字段类型 字段2:字段类型
该命令会生成迁移文件,模型,以及模型的测试文件。
-
Rails中提供的字段类型和常用数据库字段类型:
*Rails* | *mysql* | *postgresql* | *sqlite* |
---|---|---|---|
:binary | blob | bytea | blob |
:boolean | tinyint(1) | boolean | boolean |
:date | date | date | date |
:datetime | datetime | timestamp | datetime |
:decimal | decimal | decimal | decimal |
:float | float | float | float |
:integer | int(11) | integer | integer |
:string | varchar(255) | * | varchar(255) |
:text | text | text | text |
:time | time | time | datetime |
:timestamp | datetime | timestamp | datetime |
-
创建数据表迁移的编写语法
class CreateUsers < ActiveRecord::Migration[6.1] def change create_table :复数表名 do |t| t.字段类型 :字段名称, limit:100, null:false, default:'' ... ... t.timestamps end end end
-
执行迁移的命令
$ rails db:migrate
-
模型中字段内置验证的基本语法
validates :字段名, 验证对规则:{具体的验证规则, message:"错误信息"}
-
模型创建新对象并保存
模型.new(字段1:值, ...).save 或者 模型.create(字段1:值, ...)
-
模型单元测试的基本语法
test '测试描述:不允许重复' do 模型对象 = 模型.new(字段1:值, ...) # assert表示断言通过验证 assert 模型对象.valid? # assert_not 表示断言没有通过验证 assert_not 模型对象.valid? end
$ git checkout -b chapter04
$ rails generate model User email:string password_digest:string role:integer
Running via Spring preloader in process 5041
invoke active_record
create db/migrate/20210507090011_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
可以看到命令行帮我们生成了 db/migrate/20210507090011_create_users.rb
用户模型的迁移文件。
还帮我们生成了 app/models/user.rb
用户模型文件。
db/migrate/20210507090011_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.1]
def change
create_table :users do |t|
t.string :email, limit:100, null:false, default:''
t.index :email, unique: true
t.string :password_digest, limit:256, null:false, default:''
t.integer :role, default: 1, null: false, unsigned: true
t.timestamps
end
end
end
我们这里创建了users表,为email字段添加了唯一索引
$ rails db:migrate
== 20210507090011 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0154s
== 20210507090011 CreateUsers: migrated (0.0155s) =============================
执行结果显示创建了 users 表,在rails中,我们的模型名使用英语单词单数,生成的表名对应了英语单词的复数形式。
思路分析
我们需要做的验证:
-
email:不能为空,不能重复, 必须符合格式;
-
password_digest:不能为空;
-
role:值必须是在 [0, 1, 2] 中的一个。
app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true,
uniqueness: true,
format: { with: /\w+@\w+\.{1}[a-zA-Z]{2,}/ }
validates :password_digest, presence: true
validates :role, inclusion: { in: [0, 1, 2], message:"role can be only in [0 1 2]" }
end
$ rails console
2.7.2 :001 > u = User.new({email:"testemail", password_digest:'', role:5})
2.7.2 :002 > u.valid?
2.7.2 :003 > u.errors.messages
=> {:email=>["is invalid"], :password_digest=>["can't be blank"], :role=>["role can be only in [0 1 2]"]}
通过错误信息,可知三种验证都未通过
我们使用Rails内置的 Minitest 测试框架来编写以及测试我们的应用。
对于单元测试的编写,我们需要进行两种基本类型场景测试:成功的场景和失败的场景。而模型的验证,在这里主要是针对模型中对字段的相关 validates 的针对性测试!
- 成功的场景
- 使用全部合法的参数(
合法的email,合法的password_digest,合法的role
)创建用户,断言:通过验证
- 使用全部合法的参数(
- 失败的场景
- 使用
非法的email,合法的password_digest,合法的role
创建用户,断言:未通过验证 - 使用
重复的email,合法的password_digest,合法的role
创建用户,断言:未通过验证 - 使用
非法的password_digest,合法的email,合法的role
创建用户,断言:未通过验证 - 使用
非法的role,合法的email,合法的password_digest
创建用户,断言:未通过验证
- 使用
在使用Rails的命令创建模型user时,Rails还帮我们自动创建了测试文件已经与模型对应的测试用预定义数据文件:
test/fixtures/users.yml
我们可以修改这个文件的内容如下:
one:
email: 'user1@demo.com'
password_digest: '123456'
role: 1
two:
email: 'user2@demo.com'
password_digest: '123456'
role: 1
这里定义了两个用户:用户one
和 用户 two
, 然后我们就可以在测试文件中通过 users(:one)
来获取第一个用户的对象, users(:two)
来获取第二个用户的对象信息了。
测试的编写我们首先要确定文件路径:test/models/user_test.rb
, 如果你仔细观察,这个文件也是Rails帮我们自动创建的!现在开始编写相关测试吧!
-
使用全部合法的参数(
合法的email,合法的password_digest,合法的role
)创建用户,断言:通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 使用合法参数 test 'valid: user with all valid things' do user = User.new(email: 'user0@demo.com', password_digest:'123456', role:1) assert user.valid? end #... end
-
使用
非法的email,合法的password_digest,合法的role
创建用户,断言:未通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 使用不符合格式发email test 'invalid: user with invalid email' do user = User.new(email: 'test', password_digest:'123456', role:1) assert_not user.valid? end end
-
使用
重复的email,合法的password_digest,合法的role
创建用户,断言:未通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 重复的邮箱 test 'invalid: user with taken email' do user = User.new(email: users(:one).email, password_digest:'123456', role:1) assert_not user.valid? end end
-
使用
非法的password_digest,合法的email,合法的role
创建用户,断言:未通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 使用不合法的password_digest test 'invalid: user with invalid password_digest' do user = User.new(email: 'test1@test.cn', password_digest:'', role:1) assert_not user.valid? end end
-
使用
非法的role,合法的email,合法的password_digest
创建用户,断言:未通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 使用不合法的role test 'invalid: user with invalid role' do user = User.new(email: 'test2@test.cn', password_digest:'123456', role:5) assert_not user.valid? end end
$ rails test
Running via Spring preloader in process 7692
Run options: --seed 35082
# Running:
.....
Finished in 0.203476s, 24.5729 runs/s, 24.5729 assertions/s.
5 runs, 5 assertions, 0 failures, 0 errors, 0 skips
我们这里顺利通过测试。不过在你自己编写代码的过程中可能会遇到各种各样的问题!别着急,慢慢调试,这种麻烦正是你积累经验的好帮手!
不过到目前为止,我们的密码保存的还都是明文,在实际的工作项目中,这是不允许的。下面我们将利用 Rails 内置的加密功能来实现密码的加密保存!
其实Rails框架中的ActiveModel::SecurePassword::has_secure_password
已经为我们提供了密码加密、验证等一系列功能。我们在这里要使用它,不过我们最好先弄明白原理,所以我们可以尝试阅读源码!为了更方便阅读,我把源码做了小小的整理,下面是我经过我整理过的源码:
module ActiveModel
module SecurePassword
extend ActiveSupport::Concern
module ClassMethods
# 安全密码
def has_secure_password(attribute = :password, validations: true)
# 引入"bcrypt"
require "bcrypt"
# password=
define_method("#{attribute}=") do |unencrypted_password|
# 设定password为未加密的密码
instance_variable_set("@#{attribute}", unencrypted_password)
cost = 12
# 设定password_digest为加密的密码
self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
end
# password_confirmation=
define_method("#{attribute}_confirmation=") do |unencrypted_password|
# 设定password_confirmation为未加密的密码
instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
end
# authenticate_password
define_method("authenticate_#{attribute}") do |unencrypted_password|
attribute_digest = public_send("#{attribute}_digest")
# 验证密码 是否正确
BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
end
# authenticate 别名
alias_method :authenticate, :authenticate_password if attribute == :password
# 默认就是true 需要验证
if validations
include ActiveModel::Validations
validate do |record|
# 创建用户 password 不能为空
record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
end
# password 最长72个字符
validates_length_of attribute, maximum: 72
# 如果存在就验证 password_confirmation == password
validates_confirmation_of attribute, allow_blank: true
end
end
end
end
end
通过以上源码,我们可以确认几条对我们比较重要的信息:
has_secure_password
方法接受的参数中密码的字段默认名称是:password
而不是password_digest
;has_secure_password
方法严重依赖外部的bcrypt
;has_secure_password
方法把加密后的密码保存到了password_digest
,这也是为什么users
表中密码字段名称为password_digest
;has_secure_password
方法添加了以下几条password
字段的验证- 创建用户时
password
不能为空; password
最长不能超过72个字符;- 如果传递了
attribute_confirmation
字段,会进行两次密码相同的比较,没传则不比较。
- 创建用户时
基于以上结论,我们可以确定我们需要完成的步骤:
- 首先引入
bcrypt
。 - 在user模型中调用
has_secure_password
方法。
bcrypt
是一个gem
, 我们可以使用命令 bundle add bcrypt
安装, 不过其实我们的项目已经默认添加了 bcrypt
,只不过是注释状态,我们可以打开 Gemfile
文件,去掉bcrypt
前面的注释:
# Use Active Model has_secure_password
gem 'bcrypt', '~> 3.1.7'
# ...
然后在项目根目录下执行命令安装新增的gem
$ bundle
现在就可以使用 bcrypt
了。
has_secure_password
是 ActiveModel
的类方法, 继承自 ActiveModel
类的子类都可以直接调用,Rails模型都是 ActiveModel
的子类,所以我们可以直接在users
中直接调用即可,参数使用 has_secure_password
默认参数!
app/models/user.rb
class User < ApplicationRecord
# ...
has_secure_password
end
有了 has_secure_password
方法的加持,对于 User
模型,要创建用户我们需要提供的字段包括:email
、password
、role
, 即:
User.new({email:"a@a.com", password:'123456', role:1})
password_digest
字段由has_secure_password
方法来维护,而且password_digest
的值是加密后的密码。
$ rails console
2.7.2 :001 > u = User.new({email:"a@a.com", password:'123456', role:1})
2.7.2 :002 > u.save
TRANSACTION (0.8ms) BEGIN
User Exists? (34.6ms) SELECT 1 AS one FROM `users` WHERE `users`.`email` = 'a@a.com' LIMIT 1
User Create (1.7ms) INSERT INTO `users` (`email`, `password_digest`, `created_at`, `updated_at`) VALUES ('a@a.com', '$2a$12$Bn6pEc/pyeWpmKwM0nfIF.0ZSK41W4H3Qc96aq.Sg/f0XXEQo6P7y', '2021-05-08 01:56:28.110434', '2021-05-08 01:56:28.110434')
TRANSACTION (2.8ms) COMMIT
=> true
2.7.2 :003 > u
=> #<User id: 2, email: "a@a.com", password_digest: [FILTERED], role: 1, created_at: "2021-05-08 01:56:28.110434000 +0000", updated_at: "2021-05-08 01:56:28.110434000 +0000">
可以观察到密码被加密了并且被保存到了 users
表的 password_digest
字段中。
到这里,模型相关的开发工作,可以告一段落了。
-
使用
合法的password,合法的email,合法的role
创建用户,断言:通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 使用合法的password test 'valid: user with valid password' do user = User.new(email: 'test3@test.cn', password:'123456', role:1) assert user.valid? end end
-
使用
非法的password,合法的email,合法的role
创建用户,断言:未通过验证test/models/user_test.rb
class UserTest < ActiveSupport::TestCase # 使用非法的password test 'invalid: user with invalid password' do user = User.new(email: 'test4@test.cn', password:'', role:1) assert_not user.valid? end end
$ rails test
Running via Spring preloader in process 3990
Run options: --seed 8355
# Running:
.......
Finished in 0.307520s, 22.7627 runs/s, 22.7627 assertions/s.
7 runs, 7 assertions, 0 failures, 0 errors, 0 skips
$ git add .
$ git commit -m "set users model with validation and bcrypt"
我们完成了用户模型验证和密码加密功能,下节课我们将开发用户控制器相关功能!一定要跟上脚步,认真练习!