pac4j
javaで作られた認証・認可ライブラリ
多くのサービスとjavaフレームワークに対応している
比較的簡単に実装できる(と思う)
内部でscribejavaを使っている模様
play-pac4jにはDeadbolt2が統合されており、シームレスにこれを使用できる
-
PlayFramework用のモジュール
https://github.com/pac4j/play-pac4j -
PlayFramework(java)+pac4jの公式デモ(実装の参考に)
https://github.com/pac4j/play-pac4j-java-demo
Play + pac4jエントリー一覧
PlayFramework + pac4jでOAuthを使う ①Twitter認可
PlayFramework + pac4jでOAuthを使う ②DB連携
本記事のゴール
OAuthを用いてTwitterユーザーを認可する
全コード
実装
build.sbt
libraryDependenciesにplay-pac4j2.5.0とpac4j-oauth1.9.2を追記
libraryDependencies ++= Seq(
javaJdbc,
cache,
javaWs,
"org.pac4j" % "play-pac4j" % "2.5.0",
"org.pac4j" % "pac4j-oauth" % "1.9.2"
)
app/modules
DemoHttpActionAdapter
全てのレスポンスを改変できるクラス
今回はhttpステータスコードが401か403の場合、それぞれ特殊なページをレンダリングするようにした
また、401(認証エラー)の場合にはログインセッションを削除しておく
削除しないと値が空のpac4j用クッキーが作られる
package modules;
import org.pac4j.core.context.HttpConstants;
import org.pac4j.core.context.Pac4jConstants;
import org.pac4j.play.PlayWebContext;
import org.pac4j.play.http.DefaultHttpActionAdapter;
import play.mvc.Result;
import static play.mvc.Results.*;
public class DemoHttpActionAdapter extends DefaultHttpActionAdapter {
@Override
public Result adapt(int code, PlayWebContext context) {
if (code == HttpConstants.UNAUTHORIZED) {
// セッションを削除
context.getJavaContext().session().remove(Pac4jConstants.SESSION_ID);
return unauthorized("401 UNAUTHORIZED");
} else if (code == HttpConstants.FORBIDDEN) {
return forbidden("403 FORBIDDEN");
} else {
return super.adapt(code, context);
}
}
}
SecurityModule
pac4jの設定を行うクラス
実際に動作させるときは、twIdをAPI Keyに、twSecretをAPI Secretの文字列に置き換える
package modules;
import com.google.inject.AbstractModule;
import org.pac4j.core.authorization.authorizer.RequireAnyRoleAuthorizer;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
import org.pac4j.oauth.client.TwitterClient;
import org.pac4j.play.ApplicationLogoutController;
import org.pac4j.play.CallbackController;
import org.pac4j.play.store.PlayCacheStore;
import org.pac4j.play.store.PlaySessionStore;
import play.Configuration;
import play.Environment;
public class SecurityModule extends AbstractModule {
private final Configuration configuration;
public SecurityModule(final Environment environment, final Configuration configuration) {
this.configuration = configuration;
}
@Override
protected void configure() {
bind(PlaySessionStore.class).to(PlayCacheStore.class);
// TwitterのAPI KeyとAPI Secretを設定
TwitterClient twitterClient = new TwitterClient("twId", "twSecret");
// コールバックURLとクライアントを設定、今回はtwitterのみ
Clients clients = new Clients("http://localhost:9000/callback", twitterClient);
Config config = new Config(clients);
// Authorizer:認証ロジック 複数のAuthorizerをadd可能
config.addAuthorizer("admin", new RequireAnyRoleAuthorizer<>("ROLE_ADMIN"));
// ActionAdapterの設定
config.setHttpActionAdapter(new DemoHttpActionAdapter());
bind(Config.class).toInstance(config);
// callback
CallbackController callbackController = new CallbackController();
callbackController.setDefaultUrl("/");
bind(CallbackController.class).toInstance(callbackController);
// logout
ApplicationLogoutController logoutController = new ApplicationLogoutController();
logoutController.setDefaultUrl("/");
bind(ApplicationLogoutController.class).toInstance(logoutController);
}
}
app/conf/application.conf
enabled += "modules.SecurityModule"を追記してSecurityModuleを有効化する
...
play.modules {
# By default, Play will load any class called Module that is defined
# in the root package (the "app" directory), or you can define them
# explicitly below.
# If there are any built-in modules that you want to disable, you can list them here.
#enabled += my.application.Module
enabled += "modules.SecurityModule"
# If there are any built-in modules that you want to disable, you can list them here.
#disabled += ""
}
...
app/view
index.scala.html
トップページ
org.pac4j.core.context.Pac4jConstants.SESSION_ID(この定数の値は"pac4jSessionId")の有無でログイン状態を判定する
未ログインならTwitterLogin用のリンクを表示、ログインしていればLogoutリンクも表示
@(message: String)
@main("index") {
@if(org.pac4j.core.util.CommonHelper.isBlank(session.get(org.pac4j.core.context.Pac4jConstants.SESSION_ID))) {
<a href="@controllers.routes.LoginController.twitterLogin()">Twitter Login</a>
}else{
<a href="@controllers.routes.LoginController.twitterLogin()">Twitter Login</a>
<br />
<a href="@org.pac4j.play.routes.ApplicationLogoutController.logout()">Logout</a>
}
}
protectedIndex.scala.html
ログインしていないと見れないページ
プロファイル(OAuthで認可したユーザーの情報)を受け取って表示
定数で引っ張れるログインセッションの値もついでに表示
@(profiles: List[org.pac4j.core.profile.CommonProfile])
@main("テストページ") {
<h1>protected area</h1>
<a href="..">Back</a><br />
<br /><br />
profiles: @profiles<br />
@if(org.pac4j.core.util.CommonHelper.isBlank(session.get(org.pac4j.core.context.Pac4jConstants.SESSION_ID))) {
<font color="red">session NOT exist</font>
}else{
<font color="red">@session.get(org.pac4j.core.context.Pac4jConstants.SESSION_ID)</font>
}
}
※ pac4jのセッションについて
playframework用のログインセッションはplay-pac4jにあるPlayCacheStoreによって作られる
https://github.com/pac4j/play-pac4j/blob/master/src/main/java/org/pac4j/play/store/PlayCacheStore.java
getOrCreateSessionIdメソッド内でUUIDを生成し、定数Pac4jConstants.SESSION_IDをキーにしてセッションにputしている
app/controller/LoginController.java
package controllers;
import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.ProfileManager;
import org.pac4j.play.PlayWebContext;
import org.pac4j.play.java.Secure;
import org.pac4j.play.store.PlaySessionStore;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.protectedIndex;
import javax.inject.Inject;
import java.util.List;
public class LoginController extends Controller {
@Inject
protected PlaySessionStore playSessionStore;
private List<CommonProfile> getProfiles() {
final PlayWebContext context = new PlayWebContext(ctx(), playSessionStore);
final ProfileManager<CommonProfile> profileManager = new ProfileManager<>(context);
return profileManager.getAll(true);
}
@Secure(clients = "TwitterClient")
public Result twitterLogin() {
return ok(protectedIndex.render(getProfiles()));
}
}
@Inject protected PlaySessionStore playSessionStore;
セッション操作用のインターフェース
中身にはPlayCacheStoreのインスタンスが入る
private List getProfiles()
OAuthで認可したユーザーのプロファイルをコンテキストから取得する処理
@Secure(clients = "TwitterClient")
Secureアノテーションを付けたメソッドはプロテクトされ、認可、認証済みのユーザーでないと実行できなくなる
未認可状態でアクセスすると認可を求められる
clientsにTwitterClientを記述することで、Twitterアカウントの連携を要求する
app/conf/routes
indexとprotectedIndexの設定
pac4j用にcallbackとlogoutを設定
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# An example controller showing a sample home page
GET / controllers.HomeController.index
# Social
GET /tw controllers.LoginController.twitterLogin
# pac4j
GET /callback org.pac4j.play.CallbackController.callback()
GET /logout org.pac4j.play.ApplicationLogoutController.logout()
# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)
動作確認
protectedIndexにアクセスしてTwitterアカウントに連携を許可し、以下のような画面になれば成功
Next
取得したプロファイル情報をDBに格納し、突合せによるログインログアウトを実装