重温注册与登录--学习笔记

最近把Ruby on Rails 教程重温了一遍,学习到了一些之前没注意到的知识。因此以笔记的形式记录下来,目的是为了梳理实作用户系统的步骤(大标题)与方法,以及一些豆知识,具体细节可以参考Ruby on Rails 教程

一. 注册

豆知识:查看含有user的路径的命令 — rake routes | grep user

1. 建立模型,存储用户信息

(1)模型user是单数,表示单个用户;表名users为复数,代表数据库存储了很多的用户信息。
(2)rails c --sandbox rails的“沙盒”模式,退出后会撤销所做的所有操作。
(3)创建与保存 user = User.new user.save 两步合成一步相当于user = User.create(..)
(4)更新: user = User.first user.name = "jonathan" user.save 合成一步 相当于 user.update(:name => "jonathan")
关于更新用户属性值的写法有以下几种:

1
2
3
第一种写法. user.update(:name => "joanthan")
第二种写法. user.update(name: "joanthan")
第三种写法. user.update_attribute(:name, "joanthan")

update 与 update_attribute 的区别:
update会把user模型中的update_at栏位更新为当前时间;
update_attribute 只更新要求更新的字段值。

2. 数据验证

建立好user的模型,我们需要添加必要的数据验证。有存在性、长度(姓名长度等)、格式(邮箱格式)、唯一性。我们需要用到Active Record中的validate方法,以及正则表达式等来完成这些设置。

对于唯一性来讲,Active Record的设置后的数据唯一性无法保证数据库层的唯一性,比如用户用同一个账号同时注册的情况,解决方法是: 在数据层为需要进行唯一性设置的加上索引,并进行唯一性约束。

3.安全密码

分为两步:
(1)设置密码,并进行二次确认;
(2)验证身份:获取用户提交的密码,哈希加密,然后与数据库存储的密码进行比较。
(3)使用SSL进行加密通信防止数据被恶意用户拦截,确保传输层安全。

实现过程:使用has_secure_password方法
实现条件:user模型中须有 password_digest字段
具体细节:在user模型中调用has_secure_password方法时,会添加如下功能。
A. password_digest存储密码的哈希值;
B. 获得一对虚拟属性,password与password_confirmation;虚拟属性的意思为在model中有这两个属性,但是数据库中没有对应的列。
C. 获得authenticate 方法,如果输入的密码值哈希后与数据库存储的密码哈希相同,返回对应的对象,否则返回true。

豆知识:
(1)使用bcrypt计算密码哈希值
(2)!!会把结果转化成相应的boolean。例如!!user.authenticate("123456")
(3)健壮参数:user_params
params.require(:user).permit(:name, :email)

二.登录

1. 会话

(1)session是两台电脑之间的半永久性连接,使得用户在切换网页时能够记住用户状态;
(2)Users资源使用数据库存储数据,session使用cookie;
(3)Rails中提供session来实现临时会话;
(4)会话不是模型,不能创建类型@user的实例变量。form_for(@user)会让表单向/users发起post请求;这里我们可以使用form_for(:session, url: login_path)

2. 验证用户身份

豆知识:
params[:session][:email] 是一个嵌套散列。

1
2
3
4
5
6
user = User.find_by(email: params[:session][:email].downcase)
user && user.authenticate(params[:session][:password])
意思为当且仅当通过邮箱查到一个用户且提交的密码和数据库的密码匹配

有时习惯写成
user.present? user.authenticate(params[:session][:password])

3. 实现登录

(1)临时会话会让用户登录,关闭浏览器后会关闭会话;
(2)include SessionHelper引入模块;
(3)session[:user_id] = user.id ,将session视为一个散列,user_id为键,user.id为值;
(4)redirect_to user 返回的是 user_url(user)

三. 高级登录

使用cookie有会话劫持的风险,有四种途径盗取cookie信息
(1)使用包嗅探接活不安全网络中传输的cookie 。 => 使用SSL解决

(2)获取包含记忆令牌的数据库。=> 数据库中不直接存储记忆令牌,而是哈希。

(3)使用跨站脚本攻击。=> Rails会自动转移视图模板的内容。

(4)获取已登录用户的访问权。=> 尽量降低其影响,通过加密存储。

SecureRandom.urlsafe_base64 会返回长度为22的随机字符串,每一个字符串有64种可能。

1. 持久会话

实现步骤:
(1)生成随机字符串,用户记忆令牌(remember_token);
(2)把令牌存入浏览器的cookie,并设置过期时间;
(3)在数据库存储令牌摘要(remember_digest);
(4)在浏览器的cookie存储加密后的用户ID;
(5)如果cookie中有用户ID,那么就用这个ID查找数据库的用户,并检查cookie中的记忆令牌和数据中的哈希摘要是否匹配。

创建有效令牌和摘要:
(1)使用 User.new_token 创建一个新记忆令牌;
(2)使用 User.digest生成摘要;
(3)更新数据库中的摘要。

豆知识: self.remember_token = User.new_token,会把值赋给用户的remember_token属性;如果没有self,remember_token只是一个局部变量。

相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 返回指定字符串的哈希摘要
def digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST :
BCrypt::Engine.cost
BCrypt::Password.create(string, cost: cost)
end


#返回一个随机令牌(用于remember_token)
def new_token
SecureRandom.urlsafe_base64
end
end

#为了持久保持会话,在数据库中记住用户
def remember
self.remember_token = User.new_token
update_attribute(:remember_digest, User.digest(remember_token))
end

2. 登录时记住登录状态

(1)cookies方法和session方法一样,也将它视为一个散列;

1
2
cookies[:remember_token] = { value: remember_token, expires: 20.years.from_now.utc}
=> cookies.permanent[:remember_token] = remember_token

(2)持久存储用户ID

1
2
3
cookies[:user_id] = user.id #已纯文本的格式存入cookie,这样不安全
cookies.signed[:user_id] = user.id # 对cookie签名,存取浏览器前安全加密cookie
cookies.permanent.signed[:user_id] = user.id # 持久保存用户ID

(3)存储cookie后,用下列代码搜索用户

1
user = User.find_by(id: cookies.signed[:user_id])

A. cookies.signed[:user_id] 会自动解密用户ID;
B. 使用bcrypt确定cookies[:remember_token]remember_digest是否匹配 .

1
Bcrypt::Password.new(remember_digest).is_password?(remember_token)

(4)知识补充:三元运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
例子1
if boolean?
do_one_thing
else
do_other_thing
end

=> boolean? ? do_one_thing : do_other_thing

例子2
if boolean?
var = foo
else
var = bar
end

=> var = boolean? ? foo : bar

例子3
def foo
do_stuff
boolean? ? "bar" : "baz"
end
Ruby 函数的默认返回值是定义体中的最有一个表打式,所以foo方法的返回值会根据boolean?的结果而不同,不是“bar” 就是“baz”

我们写一个简单的脚本来做一个小试验:

1
2
3
4
5
6
7
print "请输入x的值:"
x = gets.to_i

print "请输入y的值:"
y = gets.to_i

puts "x > y ? 1 : 2"

四. 权限系统

1. 必须先登录

2. 只能编辑自己的资料

3. 友好转向: 登录以后重定向到登录前的页面。

4. 用户管理: 加入新字段(如admin)