SecureSocialでSNSを使ったログイン

Socialと付くだけあって、ソーシャルなサービスでのログインができる。

ユーザー名/パスワードを使った(外部サービスを使わない)認証に加えて

での認証ができるようになってる。 認証時に得た情報はBasicProfileに格納されるが、取得できない情報や間違って設定されているところがあるようなので、メモっておく。

  • Instagram
    • 名前がFirst name/Last nameに分かれていない
    • メールアドレスが取得できない
  • Github
    • First name, Last nameが取得できない
  • LinkedIn
    • メールアドレスを取得するには権限の設定がLinkedIn側で必要(r_emailaddressを追加)
  • Twitter
    • 名前がFirst name/Last nameに分かれていない
    • メールアドレスが取得できない
  • Dropbox
    • FirstName, LastNameが取得できない
    • 姓名が逆
    • メールアドレスが取得できない
  • Foursquare
  • Xing
    • First nameにdisplay_name(フルネーム)が設定されてしまう。
    • Last nameにfirst_nameが設定されてしまう。
    • Full nameにlast_nameが設定されてしまう。
    • これもProviderを継承して自作するしかない。

SecureSocialで独自の認証画面を作る。

環境は

  • Scala : 2.11
  • Play : 2.3
  • SecureSocial : 3.0-M1

SecureSocialで独自の認証画面を使う方法を書く。 前提として、以下の方法でsecuresocialを導入しているものとする。

Global.scala

RuntimeEnvironmentのサブクラスを実装する。 ほぼ、前回と同じだが、カスタムの画面を使用する場合はviewTemplatesをオーバーライドする。

object ApplicationRuntimeEnvironment extends RuntimeEnvironment.Default[User] {
    protected override def include(p: IdentityProvider) = p.id ->   p
    protected override def oauth1ClientFor(provider: String) = new OAuth1Client.Default(ServiceInfoHelper.forProvider(provider), httpService)
    protected override def oauth2ClientFor(provider: String) = new OAuth2Client.Default(httpService, OAuth2Settings.forProvider(provider))

    override lazy val userService: UserService[User] = new UserServiceImpl
    override lazy val providers = ListMap(
        include(new FacebookProvider(routes, cacheService, oauth2ClientFor(FacebookProvider.Facebook))),
        include(new GoogleProvider(routes, cacheService,oauth2ClientFor(GoogleProvider.Google))),
        include(new UsernamePasswordProvider[User](userService, avatarService, viewTemplates, passwordHashers))
    )

    override lazy val viewTemplates 
        = new plugins.CustomTemplates(this) /// <====追加
}

plugins/CustomTemplates.scala

ViewTemplatesを継承したクラスを作成。 独自に実装したい画面に対応するメソッドをオーバーライドする。FacebookなどのSNSでのログインのみであれば、getLoginPage、ID/パスワードの認証であれば、その他のメソッドもオーバーライドする必要がある

  • getLoginPage
  • getNotAuthorizedPage
  • getSignUpPage
  • getStartSignUpPage
  • getResetPasswordPage
  • getStartResetPasswordPage
  • getPasswordChangePage

import play.api.data.Form
import play.api.i18n.Lang
import play.api.mvc.RequestHeader
import play.twirl.api.Html

import securesocial.controllers._
import securesocial.core.RuntimeEnvironment

class CustomTemplates(env: RuntimeEnvironment[_]) extends ViewTemplates {
    implicit val implicitEnv = env

    override def getLoginPage(form: Form[(String, String)],
            msg: Option[String] = None)(implicit request: RequestHeader, lang: Lang): Html = {
        views.html.login(form, msg)
    }
    ...(略)... 
}

あとはテンプレートを書けばいいだけなので、特に問題となるようなところは無いはず。 わからなければ、ココらへんに転がってるものを見ればいいと思う。

https://github.com/jaliss/securesocial/tree/master/module-code/app/securesocial/views

views.login.scala.html

@(loginForm: Form[(String,String)], errorMsg: Option[String] = None)(implicit request: RequestHeader, env:securesocial.core.RuntimeEnvironment[_])

@import java.util._
@import java.text.DateFormat

@import securesocial.core.providers.UsernamePasswordProvider.UsernamePassword

@main {
<div class="form-box" id="login-box">
    <div class="header">Sign In</div>
    @env.providers.get(UsernamePassword).map { up =>
        @views.html.provider("userpass", Some(loginForm))
    }
    <div class="margin text-center">
        @defining( env.providers.values.filter( _.id != UsernamePassword) ) { externalProviders =>
            @if( externalProviders.size > 0 ) {
                <div class="clearfix">
                    <p>@Messages("securesocial.login.instructions")</p>
                    <p>
                        @for(p <- externalProviders) {
                            @views.html.provider(p.id)
                        }
                    </p>
                </div>
            }
        }

    </div>
</div>

}{
}{
}

views.provider.scala.html

@(providerId: String, loginForm: Option[Form[(String, String)]] = None)(implicit request: RequestHeader, lang: Lang, env:     securesocial.core.RuntimeEnvironment[_])

@import securesocial.core.providers.UsernamePasswordProvider
@import securesocial.core.AuthenticationMethod._
@import play.api.Logger
@import helper._
@import play.api.Play

@implicitFieldConstructor = @{ FieldConstructor(securesocial.views.html.inputFieldConstructor.f) }

@env.providers.get(providerId).map { provider =>
@if( provider.authMethod == OAuth1 || provider.authMethod == OAuth2 ) {
    @defining( "images/providers/%s.png".format(provider.id) ) { imageUrl =>
        <a href="@env.routes.authenticationUrl(provider.id)"> <img src="@controllers.routes.Assets.at(imageUrl)"/></a>
    }
}

@if( provider.authMethod == UserPassword ) {
    <form action="@env.routes.authenticationUrl("userpass")" method="post">
    <div class="body bg-gray">
        @if(loginForm.get.hasErrors) {
        <div class="alert alert-danger alert-dismissable">
            <i class="fa fa-ban"></i>
            <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
            User ID or password is invalid!
        </div>
        }
        @if( UsernamePasswordProvider.withUserNameSupport ) {
            @helper.inputText(
                loginForm.get("username"),
                '_label -> Messages("securesocial.signup.username"),
                'class -> "form-control",
                'placeholder -> "User ID"
            )
        } else {
            @helper.inputText(
                loginForm.get("username"),
                '_label -> Messages("securesocial.signup.email1"),
                'class -> "form-control",
                'placeholder -> "User ID"
            )
        }

        @helper.inputPassword(
            loginForm.get("password"),
            '_label -> Messages("securesocial.signup.password1"),
            'class -> "form-control",
            'placeholder -> "Password"
        )
        <div class="form-group">
            <input type="checkbox" name="remember_me"/> Remember me
        </div>
    </div>
    <div class="footer">
        <button type="submit" class="btn bg-olive btn-block">Sign me in</button>

        <p><a href="@env.routes.resetPasswordUrl">I forgot my password</a></p>

        @if(Play.current.configuration.getBoolean("securesocial.registrationEnabled").getOrElse(true) ){
            <a href="@env.routes.signUpUrl" class="text-center">Register a new membership</a>
        }
    </div>
    </form>
    }
}.getOrElse {
    @*****************************************
    * Todo: throw a runtime exception? this might need improvement
    *****************************************@
    @Logger.error("[securesocial] unknown provider '%s'. Can't render it.".format(providerId))
    { throw new RuntimeException("Unknown provider '%s') }
}

実行

あとは、実行するだけ。簡単にログイン画面のカスタマイズができた。

f:id:H_Yamaguchi:20150216193241p:plain

最後に

  • securesocialのバージョンが3.0になって変わったのは、play.pluginsを使わなくなった(かわりにRuntimeEnvironmentを使う)だけっぽい。
  • 認証時のメールのテンプレートなどもカスタマイズできる。その場合は、RuntimeEnvironmentのmailTemplatesをオーバーライドすればいい。

UITableViewControllerにUIActivityIndicatorViewを表示させる

ビューコントローラ内で普通に

self.view.addSubview(indicator)

などとやってると、self.viewがUITableViewなので、テーブルの中にインジケーターが埋め込まれたように見えてしまう。 表示位置によってはインジケーターがセクションの下に潜り込んでしまってイケてない感じ。

テーブルの上に表示するにはself.viewではなく、self.navigationController?.viewに対して追加してやると良い。

var indicator:UIActivityIndicatorView?

override func viewDidLoad() {
    super.viewDidLoad()
    ...
    let indicator = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.Gray)
    let screen = self.view.bounds.size
    //中央に表示
    indicator.center = CGPointMake(screen.width / 2, screen.height / 2 )
    self.indicator = indicator
    self.navigationController?.view.addSubview(indicator)
    ....
}
func hoge() {
    self.indicator?.startAnimating()
    self.view.userInteractionEnabled = false //処理中はテーブルをタップできないようにする。
    ...(長い処理)...
    self?.indicator?.stopAnimating()
    self?.view.userInteractionEnabled = true    
}

SourceKitServiceが400%で稼働する。

今、swiftでアプリ作成中なんだけど、ソースコードを編集中に度々CPUが唸りだす。

環境は

アクティビティモニターで見てみると、SourceKitServiceがCPU使用率400%で頑張っていた。 新しい暖房機器としては使えるが、XCodeが応答無しになったりして開発効率は悪い。

「困ったときのグーグル先生」ということで色々探していると、それっぽいのを見つけた。

Xcode6、Swiftでコード補完が始まるとCPU大回転! - eggkimi web studio

この記事を参考に50行くらいあるメソッドを細かく分割したところ、症状は改善した(ような気がする)

securesocialをscala2.11/Play2.3.4で使う

securesocialの(安定)最新版は2.1.4だが、scala2.11では使えない(かどうかわからないけど、sbtではダウンロードできない) ので、scala2.11で使いたい場合は3系(3.0-M1)に上げる必要がある。

"ws.securesocial"   %% "securesocial" % "2.1.2",

"ws.securesocial"   %% "securesocial" % "3.0-M1",

それに伴って色々変更すべき点があるみたい。

play.plugins

いままでは、

9994:securesocial.core.DefaultAuthenticatorStore
9995:securesocial.core.DefaultIdGenerator
9996:securesocial.core.providers.utils.DefaultPasswordValidator
9997:securesocial.controllers.DefaultTemplatesPlugin
...
10000:securesocial.core.providers.TwitterProvider
10001:securesocial.core.providers.FacebookProvider
10002:securesocial.core.providers.GoogleProvider
10003:securesocial.core.providers.LinkedInProvider
...

のように書く必要があったけど、一切不要になった。

Global.scala

ここに、アプリケーション独自のRuntimeEnvironmentの派生クラスを記述する必要がある。

/**
 * The runtime environment for this sample app.
 */
object ApplicationRuntimeEnvironment extends RuntimeEnvironment.Default[User] {
  protected override def include(p: IdentityProvider) = p.id ->   p
  protected override def oauth1ClientFor(provider: String) = new OAuth1Client.Default(ServiceInfoHelper.forProvider(provider), httpService)
  protected override def oauth2ClientFor(provider: String) = new OAuth2Client.Default(httpService, OAuth2Settings.forProvider(provider))

  override lazy val userService: UserService[User] = new UserServiceImpl
  override lazy val providers = ListMap(
    // oauth 2 client providers
    include(new FacebookProvider(routes, cacheService, oauth2ClientFor(FacebookProvider.Facebook))),
    include(new GoogleProvider(routes, cacheService,oauth2ClientFor(GoogleProvider.Google))),
    // username password
    include(new UsernamePasswordProvider[User](userService, avatarService, viewTemplates, passwordHashers))
  )
}

/**
 * An implementation that checks if the controller expects a RuntimeEnvironment and
 * passes the instance to it if required.
 *
 * This can be replaced by any DI framework to inject it differently.
 *
 * @param controllerClass
 * @tparam A
 * @return
 */
override def getControllerInstance[A](controllerClass: Class[A]): A = {
  val instance  = controllerClass.getConstructors.find { c =>
    val params = c.getParameterTypes
    params.length == 1 && params(0) == classOf[RuntimeEnvironment[User]]
  }.map {
        _.asInstanceOf[Constructor[A]].newInstance(ApplicationRuntimeEnvironment)
  }
  instance.getOrElse(super.getControllerInstance(controllerClass))
}

いままで、play.pluginsに使用するサービスを書いていたが、 今後は、RuntimeEnvironment.providersに記述する。 今回新たにgetControllerInstanceをoverrideしているのは、単にRuntimeEnvironmentをコントローラーに渡すためだと思う。

上記のRuntimeEnvironmentはUserServiceとprovidersの定義を上書きしているだけだが、他にもカスタマイズが入る場合(例えば、独自のテンプレートを使うとか)はここで独自に作成したサービスを指定することになると思う。

コントローラー

いままでは、objectとして定義していたけど、classで定義するようになる。

object UserController extends SecureSocial {
...
}

class UserController(override implicit val env:RuntimeEnvironment[User]) extends SecureSocial[User] {
...
}

routes

いままでは、使用するアクションをすべて記述していた(と思う)

GET     /login securesocial.controllers.LoginPage.login
GET     /logout securesocial.controllers.LoginPage.logout
GET     /signup                     securesocial.controllers.Registration.startSignUp
...(この後たくさん)

今後は

->      /auth   securesocial.Routes

だけでOK。

モデル、UserService

IdentityIdとかIdentityがなくなってる。その代わり(?)にBasicProfileというのができてる。 BasicProfileはIdentityとほぼ同じなので、書き換えは必要だけど、難しい話じゃない。

views

2系では標準のテンプレートがあって、bootstrapなログイン画面とか提供されていたけど、 3系にしたところcssやjsがNotFoundになってしまった。 ただ、public/securesocial/javascsriptsとかpublic/securesocial/bootstrap/cssなどにファイルを置けば読み込んでくれるので、特に問題にはならないと思う。(普通は独自のテンプレート定義するはずだし)

Deviseで生成したパスワードを使ってPHPで認証する

<?php
//暗号化されたパスワード
$database_record = '$2a$10$xxxxxxxxxxxxxxxxxxxxxxxxx';

//ポストされてきたパスワード
$user_input = 'yyyyyyyyyy';

if (crypt($user_input, $database_record) == $database_record) {
   echo '成功';
} else {
    echo '失敗:';
}
?>

参考

http://stackoverflow.com/questions/9321075/ruby-bcrypt-password-retrieval-in-php http://stackoverflow.com/questions/11544540/php-bcrypt-for-ruby-on-rails-devise-passwords

マーカーの画像を綺麗に表示する。

以前、マーカーの画像を変更する方法を書いたが、その時、Retinaで見ると画像がぼやけて見える。と書いた。 これの解決方法を最近知ったのでメモ。

CLLocationCoordinate2D position = CLLocationCoordinate2DMake(51.5, -0.127);
GMSMarker *london = [GMSMarker markerWithPosition:position];
london.title = @"London";
london.icon = [UIImage imageNamed:@"house"];
london.map = mapView_;

こう書いてたのを以下のように修正する。

CLLocationCoordinate2D position = CLLocationCoordinate2DMake(51.5, -0.127);
GMSMarker *london = [GMSMarker markerWithPosition:position];
london.title = @"London";
UIImage *img = [UIImage imageNamed:@"house"];

CGRect rect = CGRectMake(0,0,40,40* img.size.height / img.size.width);
UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0.0);
[img drawInRect:rect];
img = UIGraphicsGetImageFromCurrentImageContext();

UIGraphicsEndImageContext();

london.icon = img;
london.map = mapView_;

UIGraphicsBeginImageContextWithOptionsを使うところがポイントと思う。 コードは激しく長くなるけど、こんな感じでOK。