RESTful APIの認証と承認のメカニズムを扱う多くの質問がここにありますが、アプリケーションレベルで安全なサービスを実装する方法の詳細については、それらのどれも取り上げられていないようです。
たとえば、私のwebapp(Javaを念頭に置いていますが、これは実際にはすべてのバックエンドに適用されます)に、APIのユーザーがユーザー名とパスワードでログインできる安全な認証システムがあるとしましょう。ユーザーがリクエストを行うと、リクエスト処理パイプライン中の任意の時点で、getAuthenticatedUser()
メソッドを呼び出して、ユーザーがログインしていない場合はnullユーザー、またはユーザードメインオブジェクトを返すことができます。ログインしているユーザーを表します。
APIにより、認証されたユーザーはデータにアクセスできます。 _/api/orders/
_へのGETは、そのユーザーの注文リストを返します。同様に、_/api/tasks/{task_id}
_へのGETは、その特定のタスクに関連するデータを返します。
ユーザーのアカウントに関連付けることができるいくつかの異なるドメインオブジェクトがあると仮定しましょう(注文とタスクは2つの例であり、顧客、請求書などもある可能性があります)。認証されたユーザーのみが自分のオブジェクトに関するデータにアクセスできるようにしたいので、ユーザーが_/api/invoices/{invoice_id}
_を呼び出すとき、ユーザーがそのリソースへのアクセスを承認されていることを確認してから、リソースを提供する必要があります。
私の質問は、この承認の問題に対処するためのパターンや戦略は存在するのでしょうか。私が検討しているオプションの1つは、ヘルパーインターフェイス(つまりSecurityUtils.isUserAuthorized(user, object)
)を作成することです。これは、リクエストの処理中に呼び出して、ユーザーがオブジェクトをフェッチすることを許可されていることを確認できます。これは、アプリケーションのエンドポイントコードをこれらの呼び出しの多くで汚染するため、理想的ではありません。
_Object someEndpoint(int objectId) {
if (!SecurityUtils.isUserAuthorized(loggedInUser, objectDAO.get(objectId)) {
throw new UnauthorizedException();
}
...
}
_
...そして、少し面倒かもしれませんが、すべてのドメインタイプにこのメソッドを実装するという問題があります。これが唯一のオプションかもしれませんが、あなたの提案を聞きたいです!
神の愛のためにSecurityUtils
クラスを作成しないでください!
あなたのクラスは、ほんの数ヶ月で10k行のスパゲッティコードになります! Action
タイプ(作成、読み取り、更新、破棄、リストなど)をisUserAuthorized()
メソッドに渡す必要があります。これはすぐに数千行の長さになりますswitch
ステートメントは、単体テストが困難になるほど複雑化するロジックを含んでいます。しないでください。
一般的に、少なくともRuby on Rails)では、各ドメインオブジェクトにpolicy各モデルのクラス。次に、リクエストの現在のユーザーがリソースにアクセスできるかどうかをコントローラーがポリシークラスに尋ねます。Rubyの例を次に示します。そのようなものをJavaで実装することは決してありませんが、アイデアは明確に出くわす必要があります。
_class OrderPolicy
class Scope < Struct.new(:user, :scope)
def resolve
# A user must be logged in to interact with this resource at all
raise NotAuthorizedException unless user
# Admin/moderator can see all orders
if (user.admin? || user.moderator?)
scope.all
else
# Only allow the user to see their own orders
scope.where(orderer_id: user.id)
end
end
end
# Constructor, if you don't know Ruby
def initialize(user, order)
raise NotAuthorizedException unless user
@user = user
@order= order
end
# Whitelist what data can be manipulated by each type of user
def valid_attributes
if @user.admin?
[:probably, :want, :to, :let, :admin, :update, :everything]
elsif @user.moderator?
[:fewer, :attributes, :but, :still, :most]
else
[:regualar, :user, :attributes]
end
end
# Maybe restrict updatable attributes further
def valid_update_attributes
end
# Who can create new orders
def create?
true # anyone, and they would have been authenticated already by #initialize
end
# Read operation
def show?
@user.admin? || @user.moderator? || owns_order
end
# Only superusers can update resources
def update?
@user.admin? || @user.moderator?
end
# Only admins can delete, because it's extremely destructive or whatever
def destroy?
@user.admin?
end
private
# A user 'owns' an order if they were the person who submitted the order
# E.g. superusers can access the order, but they didn't create it
def owns_order
@order.orderer_id == @user.id
end
end
_
ネストされたリソースが複雑な場合でも、一部のリソースはネストされたリソースを「所有」する必要があるため、トップレベルのロジックは本質的にバブルダウンします。ただし、ネストされたリソースは、「親」リソースとは関係なく更新できる場合に、独自のポリシークラスを必要とします。
私の大学の学部向けのアプリケーションでは、everythingはCourse
オブジェクトを中心に展開します。 User
中心のアプリケーションではありません。ただし、User
sはCourse
に登録されているため、次のことを確認できます。
@course.users.include? current_user && (whatever_other_logic_I_need)
ほとんどすべてのリソースはUser
に関連付けられているため、特定のCourse
を変更する必要があるリソースの場合。これは、_owns_whatever
_メソッドのポリシークラスで行われます。
私はそれほど多くのJava=アーキテクチャーを実行していませんが、Policy
インターフェースを作成できるようです。認証が必要なさまざまなリソースがインターフェースを実装する必要があります。次に、ドメインオブジェクトごとに必要になるほど複雑になる可能性のあるすべてのメソッドがあります重要なことは、ロジックをモデルに結び付けることですそれ自体が、同時にそれを別のクラス(単一責任原則(SRP))に保持します。
コントローラのアクションは次のようになります。
_public List<Order> index(OrderQuery query) {
authorize(Order.class)
// you should be auto-rescuing the NotAuthorizedException thrown by
//the policy class at the controller level (or application level)
// if the authorization didn't fail/rescue from exception, just render the resource
List<Order> orders = db.search(query);
return renderJSON(orders);
}
public Order show(int orderId) {
authorize(Order.class)
Order order = db.find(orderId);
return renderJSON(order);
}
_
より便利な解決策は、アノテーションを使用して、何らかの形式の承認が必要なメソッドをマークすることです。これはビジネスコードから際立っており、Spring SecurityまたはカスタムAOPコードで処理できます。エンドポイントではなくビジネスメソッドでこれらのアノテーションを使用すると、権限のないユーザーがエントリポイントに関係なくそれらを呼び出そうとすると、例外が発生する可能性があります。
機能ベースのセキュリティを使用します。
機能は、特定のアクションを実行できるという証拠として機能する偽造できないオブジェクトです。この場合:
これにより、現在のユーザーが許可されていないことを実行することは不可能になります。
そのようにそれは不可能です