认证与授权:实现用户特定功能的技术探索
1. 唤醒功能的集成测试
唤醒功能需要进行测试,目前所有测试都在集成测试的顶层进行,因为我们尚未在 HTTP 环境之外使用这些功能。测试代码位于
tests/controller/sessions_controller_test.rb
中,这里仅解释一个成功路径的测试用例。
class SessionsControllerTest < IntegrationTest
it do
user = Thing::Create.(thing: {
name: "Taz", users: [{"email"=> "fred@taz.de"}]
}).model.users[0]
token = Tyrant::Authenticatable.new(user).confirmation_token
visit "sessions/wake_up_form/#{user.id}/?confirmation_token=#{token}"
page.must_have_content "account, fred@taz.de!"
page.must_have_css "#user_password"
page.must_have_css "#user_confirm_password"
end
end
上述测试的步骤如下:
1.
创建带有隐式用户的事物
:使用
Thing::Create
操作创建一个事物,并获取其中的用户。
2.
获取确认令牌
:使用
Tyrant::Authenticatable
从新创建的用户那里获取确认令牌。
3.
访问唤醒页面
:手动计算唤醒 URL 并打开该页面。
4.
验证页面内容
:确保页面显示激活账户的标题和表单。
接着,输入相同的密码并提交表单:
fill_in "Password", with: "123"
fill_in "Password, again", with: "123"
click_button("Engage")
page.must_have_content "Password changed."
Tyrant::Authenticatable.new(user).confirmed?.must_equal true
page.current_path.must_equal "/sessions/sign_in_form"
操作步骤如下:
1.
输入密码并提交
:在表单中输入相同的密码并提交。
2.
检查提示信息
:验证页面显示密码更改成功的提示信息。
3.
验证用户状态
:使用
Tyrant::Authenticatable
检查用户是否已确认。
4.
验证当前路径
:确保当前页面为登录页面。
最后,使用表单进行登录:
fill_in "Email", with: "fred@taz.de"
fill_in "Password", with: "123"
click_button "Sign in!"
page.must_have_content "Hi, fred@taz.de"
操作步骤为:
1.
输入登录信息
:在表单中输入邮箱和密码。
2.
点击登录按钮
:提交登录表单。
3.
验证欢迎信息
:确保页面显示欢迎信息。
2. 用户特定功能与授权
现在我们可以实现用户登录、退出、注册和唤醒隐式创建的休眠用户等功能,接下来是时候添加更多特定用户的功能了。不同用户类型在应用中的权限和操作如下表所示:
| 用户类型 | 创建事物 | 编辑事物 | 删除事物 | 修改事物名称 |
| ---- | ---- | ---- | ---- | ---- |
| 匿名用户 | 允许 | 不允许 | 不允许 | 不允许 |
| 已登录用户 | 允许 | 仅自己创建的事物 | 仅自己创建的事物 | 不允许 |
| 管理员用户 | 允许 | 所有事物 | 所有事物 | 允许 |
2.1 策略(Policies)
在 Trailblazer 应用中引入特定用户功能的第一步通常是为每个需要访问控制的操作添加策略。策略是一个 Ruby 类,包含代表规则的方法,能够访问当前用户和任意模型,从而计算权限。
以下是添加策略后的
Thing::Create
操作:
class Thing < ActiveRecord::Base
class Create < Trailblazer::Operation
include Policy
policy Thing::Policy, :create?
end
end
操作步骤如下:
1.
引入策略模块
:使用
include Policy
引入 Trailblazer 的策略模块。
2.
激活策略
:引用策略类
Thing::Policy
和需要通过的规则
:create?
。
策略类
Thing::Policy
如下:
class Thing::Policy
attr_reader :model, :user
def initialize(user, model)
@user, @model = user, model
end
def create?
true
end
end
这个策略类的
create?
方法始终返回
true
,意味着任何用户(包括未登录用户)都可以运行
Create
操作。
当运行
Thing::Create
操作时,策略会在
setup!
方法中进行评估:
class Trailblazer::Operation
def setup!(params)
policy = Thing::Policy.new(params[:current_user], model)
policy.create? or raise NotAuthorizedError
# ..
end
end
操作步骤如下:
1.
实例化策略对象
:将当前用户和模型传递给策略构造函数。
2.
调用规则方法
:调用配置的规则方法,如果结果为
false
,则抛出异常。
2.2 操作继承(Operation Inheritance)
虽然这个策略很简单,但添加它是有目的的。一方面,遵循一定的约定,添加一个空策略便于后续修改;另一方面,Trailblazer 中的策略对象不仅可用于限制操作访问,还在构建器中非常有用,能够帮助确定要实例化的具体操作子类。
以下是
Thing::Create
的两个子类:
class Thing < ActiveRecord::Base
class Create < Trailblazer::Operation
# ..
class SignedIn < self
end
class Admin < SignedIn
end
end
end
这里创建了
Create::SignedIn
和
Create::Admin
两个子类,分别用于处理已登录用户和管理员用户的情况。
2.3 多态构建器(Polymorphic Builders)
使用子类和多态的目的是避免在代码中使用大量的
if/else
判断。在
Thing::Create
中添加构建器:
class Create < Trailblazer::Operation
include Resolver
policy Thing::Policy, :create?
model Thing, :create
builds -> (model, policy, params) do
return self::Admin if policy.admin?
return self::SignedIn if policy.signed_in?
end
end
操作步骤如下:
1.
引入解析器模块
:使用
include Resolver
引入解析器模块。
2.
定义构建器块
:在
builds
块中根据策略对象的方法判断用户类型,并返回相应的子类。
为了使构建器正常工作,需要在
Thing::Policy
类中添加
admin?
和
signed_in?
方法:
class Thing::Policy
# ..
def signed_in?
user.present?
end
def admin?
signed_in? and user.email == "admin@trb.org"
end
end
根据不同的用户类型,
Thing::Create
操作会实例化不同的子类:
Thing::Create.().class #=> Thing::Create
Thing::Create.(current_user: User.find(1)) #=> Thing::Create::SignedIn
Thing::Create.(current_user: User.admin) #=> Thing::Create::Admin
当运行
Thing::Create
操作时,其流程如下:
graph TD;
A[调用 Thing::Create(params)] --> B[构建模型 build_model(params)];
B --> C[实例化策略对象 build_policy(params)];
C --> D[运行构建器块 build_operation_class(model, policy, params)];
D --> E[实例化操作类 operation_class.new.(params)];
综上所述,通过上述的测试、策略、操作继承和多态构建器等技术,我们可以实现不同用户类型在应用中的不同功能和权限控制,同时保持代码的简洁和可维护性。后续我们将进一步细化已登录用户和管理员用户的操作,使多态调度更有意义。
3. 用模块细化操作
前面提到,当用户登录时,事物的创建和更新表单应显示“我是作者!”复选框。这意味着我们需要在合约中添加一个布尔字段,并在操作中添加逻辑,以便在勾选该复选框时将当前用户添加到新创建的事物中。
由于这些是仅针对已登录用户的语义,不能与匿名用户的操作混合,因此我们有不同的操作
Thing::Create
来处理匿名用户,而
Thing::Create::SignedIn
则用于处理已注册用户。
为了将此功能应用于已登录用户和管理员用户,以及更新操作,我们将细化代码放入一个操作模块中,该模块位于
app/concepts/thing/signed_in.rb
:
module Thing::SignedIn
include Trailblazer::Operation::Module
contract do
property :is_author, virtual: true, default: "0"
end
callback(:before_save) do
on_change :add_current_user_as_author!, property: :is_author
end
def add_current_user_as_author!(thing)
thing.users << @current_user
end
def setup_params!(params)
@current_user = params[:current_user]
end
end
操作步骤如下:
1.
创建模块
:创建一个名为
Thing::SignedIn
的 Ruby 模块。
2.
引入操作模块
:使用
include Trailblazer::Operation::Module
引入操作 API。
3.
细化合约
:在合约中添加一个虚拟属性
:is_author
,默认值为
"0"
。
4.
添加回调
:使用
callback(:before_save)
和
on_change
事件注册
add_current_user_as_author!
方法,该方法在
is_author
字段更改时调用。
5.
实现回调方法
:在
add_current_user_as_author!
方法中,将当前用户添加到事物的用户列表中。
6.
设置参数
:重写
setup_params!
方法,将当前用户从参数中提取到实例变量中。
为了使这些更改生效,我们需要将模块包含到
SignedIn
操作类中:
class Thing < ActiveRecord::Base
class Create < Trailblazer::Operation
class SignedIn < self
include Thing::SignedIn
end
class Admin < SignedIn
end
end
end
操作步骤如下:
1.
包含模块
:在
Thing::Create::SignedIn
类中使用
include Thing::SignedIn
包含模块。
2.
继承功能
:由于
Admin
类继承自
SignedIn
类,它将自动继承这些更改。
4. 总结与展望
通过上述的一系列操作,我们已经实现了不同用户类型在应用中的不同功能和权限控制。具体如下表所示:
| 用户类型 | 操作类 | 功能特点 |
| ---- | ---- | ---- |
| 匿名用户 |
Thing::Create
| 仅能创建事物,无特殊表单字段和额外逻辑 |
| 已登录用户 |
Thing::Create::SignedIn
| 可看到“我是作者!”复选框,勾选后可将自己添加为作者 |
| 管理员用户 |
Thing::Create::Admin
| 继承已登录用户的功能,可编辑和删除所有事物,还能修改事物名称 |
在实现过程中,我们采用了多种技术:
-
集成测试
:确保唤醒功能和登录功能的正确性。
-
策略
:通过策略类来控制操作的访问权限。
-
操作继承
:创建不同的操作子类来处理不同用户类型。
-
多态构建器
:根据用户类型动态选择操作子类。
-
模块细化
:使用模块来细化操作,避免代码重复。
这些技术的应用使得代码更加简洁、可维护,并且易于扩展。例如,我们可以轻松地添加新的用户类型或功能,只需要在相应的操作类或模块中进行修改。
未来,我们可以进一步探索以下方面:
-
更多的用户功能
:例如,为不同用户类型添加更多的个性化功能,如用户资料管理、消息通知等。
-
性能优化
:随着用户数量的增加,应用的性能可能会受到影响。我们可以通过优化数据库查询、缓存机制等方式来提高性能。
-
安全增强
:加强用户认证和授权的安全性,如使用更复杂的密码策略、多因素认证等。
总之,通过不断地优化和扩展,我们可以使应用更加完善,满足不同用户的需求。
graph LR;
A[匿名用户操作] --> B[Thing::Create];
C[已登录用户操作] --> D[Thing::Create::SignedIn];
E[管理员用户操作] --> F[Thing::Create::Admin];
D --> G[包含 Thing::SignedIn 模块];
F --> G;
G --> H[添加 is_author 字段和逻辑];
通过这个流程图,我们可以更清晰地看到不同用户类型的操作类之间的关系,以及模块细化的作用。不同用户类型的操作类通过继承和模块包含,实现了功能的差异化和代码的复用。
超级会员免费看

被折叠的 条评论
为什么被折叠?



