SecurityContextとセッションでアクセス制御を行う
【参考サイト】
Jersey
Jersey
【概要】
JAX-RSでHTTPセッションの情報を元に権限チェックする方法です。
javax.ws.rs.core.SecurityContextを使用します。
Google先生で検索するとweb.xmlに権限情報を追加するというのは
かなり引っかかるのですが、なかなか独自でと言うのが引っかかりませんでした。
ログインユーザごとに権限をチェックしたいと言う場合、
一般的にはDBにアクセスしてと言うやり方をすると思うので、web.xmlを使うのは無理があります。
今回はDBで認証した情報をセッションに設定、
そのセッションを元にSecurityContextで権限チェックを行う方法について書きます。
(試行錯誤しながらやった結果なので、他に良いやり方がありそうな気が・・・)
【SecurityContextによる権限チェックとは】
下記のようなアノテーションをつけると
SecurityContextの情報から権限があるかをチェックしてくれるようです。
@PermitAll @RolesAllowed("Admin")
詳細はjavax.annotation.securityパッケージ配下のJavaDocを参照して下さい。
【独自のSecurityContext】
まずは独自のSecurityContextを作成します。
package jp.ne.glory.web.framework.filter; import java.security.Principal; import javax.servlet.http.HttpSession; import javax.ws.rs.core.SecurityContext; public class SessionSecurityContext implements SecurityContext { private final Principal principal; private final String roleName; public SessionSecurityContext(final HttpSession session) { this.principal = (Principal) session.getAttribute("Princpal"); this.roleName = (String) session.getAttribute("Role"); } @Override public Principal getUserPrincipal() { return principal; } @Override public boolean isUserInRole(String role) { return role.equals(roleName); } @Override public boolean isSecure() { return false; } @Override public String getAuthenticationScheme() { return "OriginalForm"; } }
コンストラクタでセッションを受け取り、必要な情報(Principalとロール名)を設定します。
今回はサンプルなので単一のロール名のみですが、通常はListとかで受け取ると思います。
SecurityContextではいくつかのメソッドを実装する必要があります。
getUserPrincipalメソッドではPrincipalのオブジェクトを返します。
今回はセッションに設定した内容を返しています。
(Principalオブジェクトが何をしているか良くわかっていないのですが、認証した情報を保持するオブジェクトっぽいです)
isUserInRoleメソッドが今回重要になる箇所です。
パラメータで渡されたroleを保持しているかを判定するメソッドです。
RolesAllowedで設定したロール名と比較する時に使用されるようです。
isSecureメソッドは通信がセキュアかを判定するものです。
今回はHTTP通信なのでfalseです。
getAuthenticationSchemeメソッドは認証方法のスキーマを返却します。
web.xmlを使用した場合はBASIC、DIGESTなどが返却されます。
今回は独自のものなのでOriginalFormとしてみました。
(ここら辺は何かルールがあったりするかもしれません)
【Filter】
続いて作成したSecurityContextを設定するためのFilterを作成します。
package jp.ne.glory.web.framework.filter; import java.io.IOException; import javax.annotation.Priority; import javax.servlet.http.HttpSession; import javax.ws.rs.Priorities; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; import javax.ws.rs.core.SecurityContext; import jp.ne.glory.infra.certify.CertifyTarget; @Priority(Priorities.AUTHENTICATION) @CertifyTarget public class SessionSecurityFilter implements ContainerRequestFilter { private HttpSession session; public SessionSecurityFilter(final HttpSession session) { this.session = session; } @Override public void filter(final ContainerRequestContext requestContext) throws IOException { final SecurityContext context = new SessionSecurityContext(session); requestContext.setSecurityContext(context); } }
やっていることはContainerRequestContext#setSecurityContextに
先ほど作成したSecurityContextのオブジェクトを設定しているだけです。
PriorityにはPriorities.AUTHENTICATIONを設定します。
Priorities.AUTHORIZATIONより小さい値であれば良いです。
CertifyTargetアノテーションはFilterのNameBinding用のアノテーションです。
package jp.ne.glory.infra.certify; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import javax.ws.rs.NameBinding; @NameBinding @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CertifyTarget { }
【Feature】
FilterにProviderアノテーションをつければ想定どおり動くかなと思ったのですが、
Feature経由ではないと上手く動かなかったのでFeatureを作成します。
(上手くいかなかった件は後述します)
package jp.ne.glory.web.framework.filter; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.servlet.http.HttpSession; import javax.ws.rs.container.DynamicFeature; import javax.ws.rs.container.ResourceInfo; import javax.ws.rs.core.FeatureContext; @ApplicationScoped public class SessionSecurityFeature implements DynamicFeature { @Inject private HttpSession session; @Override public void configure(ResourceInfo resourceInfo, FeatureContext context) { context.register(new SessionSecurityFilter(session)); } }
HttpSessionをFeatureContext#registerでコンポーネントを登録しているだけです。
【Application】
アプリケーションの設定部分です。
今回はJerseyを使っているので、Jerseyのorg.glassfish.jersey.server.ResourceConfigを使います。
package jp.ne.glory.web; import javax.ws.rs.ApplicationPath; import jp.ne.glory.web.framework.filter.SessionSecurityFeature; import jp.ne.glory.web.framework.thymeleaf.ThymeleafViewProcessor; import org.glassfish.jersey.filter.LoggingFilter; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; import org.glassfish.jersey.server.mvc.MvcFeature; @ApplicationPath("/") public class ApplicationSetting extends ResourceConfig { public ApplicationSetting() { // ここの3行はJerseyMVCとThymeleafの設定関連です packages(this.getClass().getPackage().getName()); register(ThymeleafViewProcessor.class); register(MvcFeature.class); register(SessionSecurityFeature.class); register(RolesAllowedDynamicFeature.class); } }
先ほど作成したSessionSecurityFeatureとRolesAllowedDynamicFeatureを登録します。
RolesAllowedDynamicFeatureはJerseyが提供する権限チェック用のFeatureです。
権限チェックを行うFilterを登録してくれます。
権限チェックを行うFilterはRolesAllowedアノテーションがあるかなどを判定し、
アクセス権限がない場合、javax.ws.rs.ForbiddenExceptionをスローします。
登録されるFilterの優先度はPriorities.AUTHORIZATIONになっています。
そのため今回自作したFilterはPriorities.AUTHORIZATIONより小さい値である必要があります。
【ExceptionMapper】
ForbiddenExceptionがスローされると403エラー画面が出るので、
ForbiddenExceptionが起きた場合の処理を実装します。
package jp.ne.glory.web.framework.exception.mapper; import javax.ws.rs.ForbiddenException; import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; @Provider public class ForbiddenExceptionMapper implements ExceptionMapper<ForbiddenException> { @Override public Response toResponse(ForbiddenException exception) { return Response.ok("You have no TestRole authority.").build(); } }
「権限がありません」と言うメッセージを返却します。
(403 Forbiddenを200 OKにするのは良くないですがサンプルコードなので。)
【アクセス制御するリソース】
アクセス制御するリソースは下記のようにします。
package jp.ne.glory.web.admin; import javax.annotation.security.RolesAllowed; import javax.enterprise.context.RequestScoped; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.core.Response; import jp.ne.glory.infra.certify.CertifyTarget; @Path("/admin") @RequestScoped @CertifyTarget @RolesAllowed("TestRole") public class Top { @GET public Response view() { return Response.ok("Welcome! TestRole User!").build(); } }
RolesAllowedを使って「TestRole」を持つ場合にだけアクセスできるようにしています。
RolesAllowedの値は配列で受け渡せますが、今回は単一のロールなのでStringです。
アクセスチェック制御対象なので独自に作成したCertifyTargetもつけます。
【セッション作成部分】
続いてセッションを作成するリソースです。
package jp.ne.glory.web; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import jp.ne.glory.infra.certify.CertifyPrincipal; @Path("/sample") public class SessionSample { @GET public Response createSession(@Context final HttpServletRequest servletRequest) { final HttpSession session = servletRequest.getSession(true); session.setAttribute("Principal", new CertifyPrincipal()); session.setAttribute("Role", "TestRole"); return Response.ok("Session created!").build(); } }
セッションに必要なPrincipalとロール名を設定しています。
Principalは独自に作成したクラスのオブジェクトを設定します。
[Principal]
package jp.ne.glory.infra.certify; import java.security.Principal; public class CertifyPrincipal implements Principal { @Override public String getName() { return "test"; } }
getNameメソッドではログインしたユーザ名やグループ名を返すべきなのだろうなぁと思うのですが、
今回はかなり適当な値を返却しています。
【試してみる】
と言うことで動きを確認してみます。
ブラウザを立ち上げて、
「http://localhost:8080/admin」にアクセスすると「You have no TestRole authority.」と表示されます。
権限がないのでForbiddenExceptionが発生し、ForbiddenExceptionMapperの結果が返却されていることがわかります。
続いて「http://localhost:8080/sample」にアクセスすると「Session created!」と表示されます。
これでセッションに情報が設定されました。
もう一度「http://localhost:8080/admin」にアクセスすると「Welcome! TestRole User!」と表示されます。
Top#viewの処理が実行されたことがわかります。
無事にアクセス制御できていますね。
【まとめ】
セッションとSecurityContextを組み合わせて権限チェックを行う方法を試してみました。
アノテーションをつけるだけで権限チェックが行えるので、
権限の種類が多いと言うアプリケーションの場合には非常に有効だと思います。
今回のサンプルはほとんどJAX-RS仕様になるので、特定のフレームワークへの依存もほとんどないと思います。
RolesAllowedDynamicFeatureを使った箇所はJerseyに依存していますが。
今回はRolesAllowedDynamicFeatureを使用しましたが、
独自のFeatureを作ることもできるのでかなりカスタマイズしやすそうです。
【Featureを作った理由】
本当は書きのようなコードにしたかったのですが、
@ApplicationPath("/") public class ApplicationSetting extends ResourceConfig { /** * コンストラクタ.<br> * アプリケーションの設定を行う。 */ public ApplicationSetting() { packages(this.getClass().getPackage().getName()); register(ThymeleafViewProcessor.class); register(MvcFeature.class); register(LoggingFilter.class); register(SessionSecurityFilter.class); register(RolesAllowedDynamicFeature.class); } }
ResourceConfigから登録したFilterと
Feature経由で登録したFilterで実行される順番が異なったため、
Feature経由で登録するようにしました。
上記のコードでも
RolesAllowedDynamicFeatureのFilter => SessionSecurityFilter
と言う実行順序でした。
原因がわからなかったのですが、Featureでやったらできました。
Feature経由でも標準から外れたやり方ではないと思います。