Play2の認証ライブラリとしてSecureSocialを使う
PlayFrameworkで認証周りを実装するときSecureSocialというライブラリが便利です。(もともとPlay1.xに対応する認証関連ライブラリとしてPlayの公式サイトにも掲載されてますがPlay2.xにも対応済みで便利に使えます。)
私はNode.jsでexpressを使うときはpassportというライブラリを使うのですが、それと似たような使用感です。
以下、SecureSocialのドキュメントにある通りやったもの。
build.sbt
build.sbtのresolverとlibraryDependenciesに以下のように追加。
resolvers += Resolver.url("sbt-plugin-releases", url("http://repo.scala-sbt.org/scalasbt/sbt-plugin-releases/"))(Resolver.ivyStylePatterns) libraryDependencies ++= Seq( (略) "securesocial" %% "securesocial" % "2.1.2"
このようにリゾルバを追加するくらいだったらBuild.scalaにするまでもなくbuild.sbtで出来るようです。
conf/
conf/にsecuresocial.confファイルを作ってapplication.confから以下のようにincludeするのが定石みたい。
# application.conf (略) include "securesocial.conf"
とりあえずTwitter認証と単純なユーザ/パスワード認証の2本立てでいく場合
# securesocial.conf smtp { host=smtp.gmail.com #port=25 ssl=true user="xxx@gmail.com" password=xxxxx from="xxx@gmail.com" } securesocial { onLoginGoTo=/ onLogoutGoTo=/login ssl=false twitter { requestTokenUrl="https://api.twitter.com/oauth/request_token" accessTokenUrl="https://api.twitter.com/oauth/access_token" authorizationUrl="https://api.twitter.com/oauth/authorize" consumerKey=xxxxx consumerSecret=xxxxx } }
として、TwitterDeveloperサイトでconsumerKeyとconsumerSecretを発行してコピペ。(なお、TwitterDeveloperサイトでコールバックURLを指定するのを忘れてはならない。アプリケーションがコールバックURLを指定していることをチェックしたければ指定しないほうがいい的な文言が添えられてるが、指定しないと認証できないので注意。私はこれにハマって丸一日つぶした。)
Facebook認証など他の認証手段も追加したいときはtwitterブロックの下に設定を追加していく。対応してるプロバイダ一覧はこちら。
smtpのブロックのところにはユーザ/パスワード認証で使われるユーザ登録確認メールを送るためのメール送信用アカウントを設定する。サービス用のgmailアドレスを取得して指定するのがよいでしょう。
続いて同じくconf/にplay.pluginsというファイルを作って以下を書く。
1500:com.typesafe.plugin.CommonsMailerPlugin 9994:securesocial.core.DefaultAuthenticatorStore 9995:securesocial.core.DefaultIdGenerator 9996:securesocial.core.providers.utils.DefaultPasswordValidator 9997:securesocial.controllers.DefaultTemplatesPlugin 9998:service.MyUserService 9999:securesocial.core.providers.utils.BCryptPasswordHasher 10000:securesocial.core.providers.TwitterProvider 10004:securesocial.core.providers.UsernamePasswordProvider
とりあえずTwitterProviderとUsernamePasswordProviderを使う場合はこのようにし、9998のMyUserServiceのところには以降に書く各認証手段のハンドリング用のクラスを指定する。
続いてconf/にあるroutesファイルに以下を追加。
# Login page GET /login securesocial.controllers.LoginPage.login GET /logout securesocial.controllers.LoginPage.logout
ログインページはsecuresocialがbootstrapバリバリのページを用意してくれてるので、必要に応じてそこのデザインを自分のサービスのものに置き換えて使う。コントローラはライブラリが適用してくれるものをそのまま使えばOK。logout後のloginページへのリダイレクトとかちゃんとやってくれる。
認証のハンドリング用クラス
app.serviceというパッケージにMyUserService.scalaのようなコードを置くのが定石みたい。
package service import play.api.{Logger, Application} import securesocial.core._ import securesocial.core.providers.Token import securesocial.core.IdentityId import models.User; case class MySocialUser(localUser: User, identityId: IdentityId, firstName: String, lastName: String, fullName: String, email: Option[String], avatarUrl: Option[String], authMethod: AuthenticationMethod, oAuth1Info: Option[OAuth1Info] = None, oAuth2Info: Option[OAuth2Info] = None, passwordInfo: Option[PasswordInfo] = None) extends Identity object MySocialUser { def apply(user: User): MySocialUser = { MySocialUser(user, IdentityId(user.name,user.provider), "","","", None, None, AuthenticationMethod("dummy"), None, None, None) } } class MyUserService(application: Application) extends UserServicePlugin(application) { private var tokens = Map[String, Token]() def find(id: IdentityId): Option[Identity] = { val user = User.findByName(id.userId,id.providerId); if (user.isEmpty) None else Some(MySocialUser(user.get)); } def findByEmailAndProvider(email: String, providerId: String): Option[Identity] = { None // not implemented yet } def save(i: Identity) = { val ii=i.identityId var user = User.findByName(ii.userId,ii.providerId); if (user.isEmpty) user = User.insert(ii.userId, ii.providerId) MySocialUser(user.get) } def save(token: Token) { tokens += (token.uuid -> token) } def findToken(token: String): Option[Token] = { tokens.get(token) } def deleteToken(uuid: String) { tokens -= uuid } def deleteTokens() { tokens = Map() } def deleteExpiredTokens() { tokens = tokens.filter(!_._2.isExpired) } }
Identityトレイトを実装したオブジェクトが、コントローラからrequestオブジェクトのメンバーとして参照可能になる。大抵はユーザ情報をDBで管理すると思うので、上記コードのようにユーザクラス"User"のようなものを作ってIdentityトレイトを継承したクラス(MySocialUser)のメンバーに入れておくのが良いでしょう。
Dao周りの設定が済んでから認証周りを実装したほうがスムーズになりそうです。(私は逆にやったので途中ごちゃっとしたりした)
これでコントローラからは
(略) import securesocial.core.{Identity, Authorization} import models._ import service._ object Test extends Controller with securesocial.core.SecureSocial { def test = SecuredAction { implicit request => request.user match { case u: MySocialUser => Ok(views.html.home(u.localUser.id.toString())); case _ => Results.Unauthorized } }
という風に書ける
まとめ
だいぶハマったのですが、こうブログ記事としてまとめて書きおえてみるとドキュメントに書いてある通りやっただけという感じですが、参考になれば幸いです。