Rails 我的权限管理简单实现 (非 gem)

flowerwrong · 2014年05月28日 · 最后由 lilijreey 回复于 2016年10月27日 · 6674 次阅读

写在前面:

ruby-version: 2.1.1
rails-version: 4.1.1

一.model

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.text :password
      t.integer :role_id

      t.timestamps
    end
  end
end
class CreateRoles < ActiveRecord::Migration
  def change
    create_table :roles do |t|
      t.string :name
      t.text :desc

      t.timestamps
    end
  end
end

二.login logout

class SessionsController < ApplicationController
  layout "user", :only => :new

  skip_before_action :require_login, only: [:new, :create]

  def new
    @session = User.new
  end

  def create
    @user = User.authentication(login_params["netid"], login_params["password"])
    if @user
      session[:user_id] = @user.id
      flash[:notice] = "welcome #{@user.name}"
      redirect_to users_path
    else
      flash[:notice] = "pass or netid incorrect"
      redirect_to login_path
    end
  end

  def destroy
    session[:user_id] = nil
    redirect_to login_path
  end

  private
  def login_params
    params.require(:user).permit(:netid, :password)
  end
end

userModel 里面的两个方法

class User < ActiveRecord::Base
  belongs_to :role
  def self.hash_password(pass, name)
    salt = name
    Digest::SHA256.hexdigest(pass + salt)
  end

  def self.authentication(netid, pass)
    user = User.find_by(netid: netid)
    if user && Digest::SHA256.hexdigest(pass + user.name) == user.password
      return user
    end
    false
  end
end

三。实现 current_user,我用的是 help_method

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  private
  def current_user
    @current_user = User.find_by id: session[:user_id] if session[:user_id]
  end
  helper_method :current_user
end

四。实现某些控制器需要登陆

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :require_login

  private
  def current_user
    @current_user = User.find_by id: session[:user_id] if session[:user_id]
  end

  def require_login
    unless current_user
      flash[:notice] = "please login"
      redirect_to login_path
    end
  end
  helper_method :current_user
end

不需要的请使用 skip_before_action

class SessionsController < ApplicationController
  layout "user", :only => :new

  skip_before_action :require_login, only: [:new, :create]

  def new
  end

  def create
  end

  def destroy
  end

  private
  def login_params
    params.require(:user).permit(:netid, :password)
  end
end

五。实现权限控制,同样是作为 help_method

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  before_action :require_login

  private
  def current_user
    @current_user = User.find_by id: session[:user_id] if session[:user_id]
  end

  def require_login
    unless current_user
      flash[:notice] = "please login"
      redirect_to login_path
    end
  end

  def required_role(need_role)
    role = current_user.role
    role_name = [role.name]

    unless (need_role & role_name) == role_name
      flash[:error] = "u r not permission"
      redirect_to login_path
    end
  end

  helper_method :current_user

  helper_method :required_role
end

在控制器中这样写

class RolesController < ApplicationController
  before_action :set_role, only: [:show, :edit, :update, :destroy]

  before_action  do
    required_role(["admin"])
  end
end

收工。

PS:原本不需要 role 表,但是方便以后扩展处 permission 表,还是留下了。

那如何在 view 中实现 cancan 那种功能:

<% if can? :update, Article %>
  <%= link_to "Edit", edit_article_path(@article) %>
<% end %>

<% if can? :update, Comment %>
  <%= link_to "Edit", edit_comment_path(@comment) %>
<% end %>

你可以让用户点击过去后提示说没有权限,但如果能在 view 中隐藏没有权限的链接那才是最完美的

#1 楼 @Peter required_role 是 help_method,暂时可以这样干 <% if required_role(["admin"])%> ...... <% end %> 如果需要细化,还需要实现 permission 表

如果你的 app 只需要是否登录这个权限,这样写足够好了。如果需要更多一点,helpers 慢慢地就会满天飞得招架不过来了。

#3 楼 @billy 哈哈,所以叫做简单实现 目前的 app 只有 5 个固定的 group 组,开放的只有一个登陆,其他都是需要登陆的, 所以不同的 controller 里面写一个 before_action required_role(["group_name1", "group_name2]) 这样即可

可以的。实际一点没什么不好的。需求来了再改。

最近在使用 Pundit, 独立出来的 policy classes 很容易管理,感觉不错!

#6 楼 @gonglexin Cancancan 还算折腾明白了,会用能用,不想再折腾 Pundit 了

想问一下,相对 Cancancan,Pundit 能很精细的控制各个权限吗?比如各种 role,每个 role 有不同权限

#7 楼 @Peter cantango, 看过点介绍,好像没维护了。

#8 楼 @chenge 现在不是用 cancancan 吗

#9 楼 @Peter cancancan 这个名气大,不过还有别的吧。正好前不久涉及到权限,了解了一下。

#7 楼 @Peter 没使用过 Cancan, 刚刚看了下 Cancan 的介绍,感觉 Cancan 是从 user 的角度出发去检查他的各种 ablity,貌似管理权限都是在一个叫 Ablity 的文件里面。而 Pundit 给我的感觉是从 model (resources) 出发,精细到每一个 action 操作定义他的权限。每一个 model 都可以定义一个权限管理的 policy class, 针对 Controller 里面的 new, create, update ... 在 policy class 里面定义对应的 new?, create?, update? 规则。对于 "各种 role, 每个 role 都有不同权限" 完全可以把这些逻辑写到对应的细粒度的 action rule (new?, create?, update? ...) 里面。

Cancan 应该也很容易做到这些,我选择 Pundit 的原因是 pundit 更容易维护,文档更清晰,不喜欢 Cancancan 这个名字。。。

Pundit+1 #7 楼 @Peter 你得自己提供 role 的区分方式……Pundit 只是提供权限的集中管理……

我昨天看了 rolify+cancan+devise,但是看起来头疼,所以还是决定自己弄个简单的,其实也没有多复杂。主要是我的 app 需求比较简单。5 个固定的用户组,只有一个开放 uri,也就是 login。目前这样我觉得是最简单的了。

CanCan 的理念在于把 user 的权限判断全部放在 ability 中,集中化管理。不需要把逻辑散布在 controller, helper, model 各层。但一个不好的地方也很明显,每次处理 request 都会加载整个 ability 文件。比如有时候只需要 update resource1,结果一加载 ability 把关于 resource2, resource3 ... 的权限都计算了一遍。想避免这种情况只能大量的用 block 方式定义每条权限。

不知道 CanCanCan 现在是不是还是这一套模式。

@flowerwrong LZ 也可以看看 Pundit,本质上它就是用 Ruby Object 来封装权限逻辑的一种实现,没做任何 magic 的事情。这个 是他们公司的一篇 blog,讲怎么样实现简单的权限管理的。然后他们把代码稍微封装了一下做成了 Pundit。

Pundit 已經很簡潔了

我用 admin? 靠谱吗

#15 楼 @PlayMonkey 用的人挺多的,看来真的不错。

query cache ?

你的权限控制策略都写死到代码上了,无法动态更改,可用性不大。

需要 登录 后方可回复, 如果你还没有账号请 注册新账号