Web 安全是一个可怕的主题。所有的你的程序都依靠密码学,代码超出了你的控制。
尽管如此,你还是可以控制部分网页安全 --所有的logins和access checks和injection errors。
本章聚焦在user logins, roles, 和使用测试来确保基本的用户验证。
- User Authentication and Authorization 用户验证和授权✅
- Adding Users and Roles✅
- Restricting Access✅
- More Access-Control Testing ✅
- Using Roles✅
- Protection Against Form Modification 防止表格修改✅
- Mass Assignment Testing✅
- Other Security Resources❌没看
User Authentication and Authorization
安装gem 'devise'
根据提示:
设置config/environments/development.rb (粘贴过来就行)
加a root route
在视图上加flash提示。如果用手脚架建立了一个model,自动会有flash提示。
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
然后生成User model: rails g devise User , 因为用了RSpec和Factory_Bot所以有了?
create spec/factories/users.rb
route devise_for :users #打包了一个全部的登入登出的路径
User model里面默认有一些Devise模块:
还有一些被注释掉的是可以选择的。
这些符号传给devise method能够使用Devise的功能features和假定了一系列的数据列columns,具体的list可以在migration file文件中看到。如(部分):
然后增加Devise的测试helper:
config.include Devise::Test::IntegrationHelpers, type: :feature
#应该可以不写具体类型
Adding Users and Roles
建立一个系统测试user_and_role_spec.rb
直接在测试中写一个方法,log_in_as,用于注册。
然后写测试。
⚠️至少在你的测试中写这么一次模仿用户登陆的测试,这样更直观,但会降低速度。
⚠️以后可以使用Devise自带的方法。
#因为,没有加before_action :authenticate_user!这个验证钩子方法。
Prescription:
Always do security testing in pair: the blocked logic and the okay logic.
再测试通过,不过之前的测试好多失败了,需要修改,才能通过。
Prescription:
当一个单独的变化打破了多个测试, 考虑你的测试策略是否有瑕疵
因此再factories/users.rb中建立一个user。
Devise提供了Test helpers来登陆,以前是ControllerHelpers,现在是:
Devise::Test::IntegrationHelpers 可用于integration, request, system tests
然后就可以使用sign_in(), sign_out()方法了。
before do
sign_in(FactoryBot.create(:user))
end
Restricting Access
有各种约束的情况,需要进行?限制。本例:
约束进入权。用户只能使用自己看自己所在的project组中的project.
这个测试会失败❌,因为project.roles不存在。
思考:
现在,需要设计一个用户和一个工程的结合。如何设计这个数据,是扩展一个新的data还是建立一个新的structure(新model关联)。一般在测试前,就应当决定了。
教材是建立一个新的关系结构多对多。建立一个role model来关联project和user
rails g model role user:refenences project:references role_name:string
rake db:migrate
然后在model层建立双方的关联:
user.rb
project.rb
再次测试:第二个测试会失败,因为没有写相关的逻辑。
?进入单元测试环境,测试的是model层的逻辑。从集成测试的失败点看到,这里也应该让非关联用完无法看到project.
有2个独立的职责来判断一个用户。1是决定是否一个用户能够看到一个project。2是如果核查失败,则返回。
进入model/user_spec.rb文件:
can_view?是自定义的方法,写在模块层的user.rb中:
❌想法:(我觉得这个方法只用于测试,实在没必要在user.rb中写。而且in?方法也不常用,不如直接在测试中使用include?()方法。单元测试其实测的是你的多对多的关联是否建立好。)
✅思考:集成测试到模块的单元测试再到集成测试,这是一个通过测试来-设计交互-发现逻辑缺陷-model层补充底层数据的逻辑-控制器层补充(mvc)的逻辑-完成设计的过程。
集成测试还需要改,测试这个逻辑: 如果用户没有使用权,控制器会成功挡住page.
需要在projects_conroller.rb中修改show action.
测试通过。
⚠️之前的集成测试task也需要修改,增加Role.create()把user和project建立上关联。
总结:
- 先思考需要做什么,然后写一个集成测试,
- 集成测试不通过的问题是什么?
- 然后写针对的单元测试。
- 再增加集成测试通过的逻辑。
- 在这个集成测试通过后,还要看增加的逻辑对之前的测试是否有负面影响,并改正。
More Access-Control Testing
分开责任和测试各自的控制器和模块相关的逻辑的好处是,让你在增加其他需求时自己会更清楚。
下面增加两个需求:
1.添加一个主管用户可以看到所有project。
2.公开的project可以被任何user看到,包括未注册的游客。
rails g migration add_public_fields
然后给Project增加publics属性,User增加admin 属性。
在spec/models/user_spec.rb加入测试,然后修改user.rb中的can_view?方法。通过测试。
这里因为无需添加集成测试,因为集成测试的逻辑无需改变,增加的功能和用户的操作无关。
Using Roles
- project index list,只有用户可以看到它权限内的projects
- 新tasks form。也有限制(这个不看了)
先加一个集成测试案例。
最后一个无法通过,因为控制器还是返回所有的projects。
然后写单元测试在user_spec.rb
match_array是一个RSpec中定义的匹配器。
用到了visible_projects,需要在模块User中定义。
然后单元测试通过,但控制器还要修改@projects = current_user.visible_projects
最后,集成测试通过。
⚠️ Project.where(id: project_ids).or(Project.where(public: true))
不明白。明天看看查询。看看是否有替代的。
6月4日周一,回顾了几个sql知识点。
1. ?昨天标记的project_ids, 可以得到关联的projects的id的数组形式。
has_many关联的方法,⚠️,?,没有详细的看知道,这个就是collection_singular_ids方法
返回一个对象集合的id的数组:
@book_ids
=
@author
.book_ids
|
SELECT "projects"."id" FROM "projects" INNER JOIN "roles" ON "projects"."id" = "roles"."project_id" WHERE "roles"."user_id" = ? [["user_id", 2]]
而: user.projects.map(&:id)
是链式方法调用。所以和数据库交互的是user.projects对应的sql语法,然后再使用map方法。
SELECT "projects".* FROM "projects" INNER JOIN "roles" ON "projects"."id" = "roles"."project_id" WHERE "roles"."user_id" = ? [["user_id", 2]]
Role create INSERT INTO "roles" ("user_id", "project_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["user_id", 1], ["project_id", 14], ["created_at", "2018-06-04 01:38:43.790421"], ["updated_at", "2018-06-04 01:38:43.790421"]]
Project Load SELECT "projects".* FROM "projects" INNER JOIN "roles" ON "projects"."id" = "roles"."project_id" WHERE "roles"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
解释:
开始事物,然后发现user.没有关联这个project,所以创建一个role建立多对多关联。然后新增一条记录:
insert into "table name"(属性,属性) values(?, ?) [[key, value], [key, value]]
提交事物:
使用inner join
user.visible_projects #此时含一条project数据
Project.where(id: project_ids).or(Project.all_public)
#<ActiveRecord::Relation [#<Project id: 14, name: "Project_1", due_date: nil, created_at: "2018-06-04 01:37:45", updated_at: "2018-06-04 01:37:45", public: false>]>
解释:
Project.where(id: user.project_ids).or(Project.where(public: true))
这条语法使用了关联语法:collection_singular_ids和查询方法:or(other)
or方法是模拟的数据库语法 where...or...
user.projects #此时有2条数据。
SELECT "projects".* FROM "projects" INNER JOIN "roles" ON "projects"."id" = "roles"."project_id" WHERE "roles"."user_id" = ? LIMIT ? [["user_id", 1], ["LIMIT", 11]]
=>返回一个关联对象,这个对象是数组,数组里面又包含对象。
#<ActiveRecord::Associations::CollectionProxy [#<Project id: 14, name: "Project_1", due_date: nil, created_at: "2018-06-04 01:37:45", updated_at: "2018-06-04 01:37:45", public: false>, #<Project id: 16, name: "Project_3", due_date: nil, created_at: "2018-06-04 02:12:50", updated_at: "2018-06-04 02:12:50", public: false>]>
再次测试user_spec.rb发现
it "allows an user to view a public project" 没有通过测试,
这是英文project.public = true 更新了,但没有保存到数据库,而之后的方法会从数据库调用project数据。所以❌。
改正就是,project.save。最简单。
重构,因为把visble_projects和can_view?方法合并所以可以重构测试。
Prescription
增加用户验证,很可能会让早期的测试产生错误。尽早尝试解决。
Protection Against Form Modification
(本章用到请求测试,相关博客:点击)
思路:一个user创建了project ,然后才会创建tasks。他创建的tasks只能自己curd,别的user没有存取的权利。
目前的开发没有对此进行现在,正常的页面操作自然不会出现问题,但如果一个恶意的用户使用HTTP request来创建不属于他的project下的task, 就可以绕过user的验证去篡改数据库了。
因此需要对TasksCojtroller#create和其他更新或什么方法进行验证。
例子:
验证task必须由user自身的project创建才会成功。
这里的设计问题是在哪里进行存取的核查(access check), 和由此带来的问题哪里写test?
第一个问题:在TasksController中的方法内限制。
第二个问题:
因为恶意request来自不是标准的用户交互(regular UI)的外部, 所以不能够使用Capybara写集成测试。 只能写请求测试
请求测试是当浏览器发送request后,服务器响应用户请求的过程。
request用到 HTTP 动 词:get、post、delete 和 patch。
given: a user, a project than belongs to, a project the user doesn't belong to.
when: the creation of the task
then: 是否task创建成功。
解释:
post()方法: 属于ActionController::TestCase::Behavior, 模仿POST request (携带参数)
这是get()方法的例子:
get :show, #也可以是相对路径如 tasks_path或者 /tasks/new
params: { id: 7 },
session: { user_id: 1 },
flash: { notice: 'This is flash message' }
然后修改想应的create action.加上判断语法:
Mass Assignment Testing
对传入的request参数进行限制。permit()方法,以防止恶意塞入hash对儿数据。
或者params.require([:task, :work]).permit(:project_id, :title, :size, :name...)
require接受一个hash key 或一个array hash key
还有一种方法。只接受明确指定的参数。
。。。
因为Capybara的集成测试 不能测试出这类潜在的问题,所以请求测试在这里发挥作用