LoginSignup
9
8

More than 5 years have passed since last update.

Scala+Play+SecureSocialでTwitterにログイン認証してユーザ名とアバターを表示するメモ

Last updated at Posted at 2014-10-09

使用したバージョン

Scala 2.11.1
Play 2.2.1
SecureSocial 2.1.4

Playプロジェクトの作成

新規プロジェクトを作成。

shell
$ play new play221ss214SNSLogin

       _
 _ __ | | __ _ _  _
| '_ \| |/ _' | || |
|  __/|_|\____|\__ /
|_|            |__/

play 2.2.1 built with Scala 2.10.2 (running Java 1.7.0_67), http://www.playframework.com

The new application will be created in /Users/suzukijun/Documents/Homepage/Chatamp/play221ss214SNSLogin

What is the application name? [play221ss214SNSLogin]
> 

Which template do you want to use for this new application? 

  1             - Create a simple Scala application
  2             - Create a simple Java application

> 1
OK, application play221ss214SNSLogin is created.

Have fun!

$ cd play221ss214SNSLogin/

各種設定

plugins.sbtの修正

warファイルを作成できるようにplay2warプラグインを追加しておきます。

project/plugins.sbt
// Comment to get more information during initialization
logLevel := Level.Warn

// The Typesafe repository
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

// Use the Play sbt plugin for Play projects
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.2.1")

// Use the Play sbt plugin for Play projects
addSbtPlugin("com.github.play2war" % "play2-war-plugin" % "1.2")

build.sbtの修正

mysqlとSecureSocialプラグインを追加。

build.sbt
import com.github.play2war.plugin._


name := "play221ss214SNSLogin"

version := "1.0-SNAPSHOT"

scalaVersion := "2.11.1"

libraryDependencies ++= Seq(
  jdbc,
  anorm,
  cache,
  "mysql" % "mysql-connector-java" % "5.1.18",
  "ws.securesocial" %% "securesocial" % "2.1.4"
)     

play.Project.playScalaSettings

Play2WarPlugin.play2WarSettings

Play2WarKeys.servletVersion := "3.0"

play.pluginsファイルの追加

conf/play.plugins
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

routesファイルの修正

# Home page
GET     /                           controllers.Application.index

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.at(path="/public", file)


# Login page
GET     /login                      securesocial.controllers.LoginPage.login
GET     /logout                     securesocial.controllers.LoginPage.logout

# User Registration and password handling 
GET     /signup                     securesocial.controllers.Registration.startSignUp
POST    /signup                     securesocial.controllers.Registration.handleStartSignUp
GET     /signup/:token              securesocial.controllers.Registration.signUp(token)
POST    /signup/:token              securesocial.controllers.Registration.handleSignUp(token)
GET     /reset                      securesocial.controllers.Registration.startResetPassword
POST    /reset                      securesocial.controllers.Registration.handleStartResetPassword
GET     /reset/:token               securesocial.controllers.Registration.resetPassword(token)
POST    /reset/:token               securesocial.controllers.Registration.handleResetPassword(token)
GET     /password                   securesocial.controllers.PasswordChange.page
POST    /password                   securesocial.controllers.PasswordChange.handlePasswordChange

# Providers entry points
GET     /authenticate/:provider     securesocial.controllers.ProviderController.authenticate(provider)
POST    /authenticate/:provider     securesocial.controllers.ProviderController.authenticateByPost(provider)
GET     /not-authorized             securesocial.controllers.ProviderController.notAuthorized

application.confファイルの修正

1行目に以下の行を追加

conf/application.conf
include "securesocial.conf"

MySQLの設定

ebdbとなっているところは、データベース名をいれてください。

conf/application.conf
db.ebdb.driver="com.mysql.jdbc.Driver"
db.ebdb.url="jdbc:mysql://localhost/ebdb"
db.ebdb.user="[データベースのユーザー名]"
db.ebdb.pass="[データベースのパスワード]"
#実行されたSQLを出力するように指定
db.ebdb.logStatements=true

securesocial.confファイルを追加

Consumer KeyとConsumer SecretはTwitterにアプリを登録して取得しておきます。

conf/securesocial.conf
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="[Your Twitter Consumer Key]"
        consumerSecret="[Your Twitter Consumer Secret]"
    }
}

マイグレーションの設定:1.sql

ebdbとなっているところはご自分の環境にあわせて変更してくださ。

conf/evolutions/ の後に続く部分はdatabase名になります。

conf/evolutions/ebdb/1.sql
# Ups以下はマイグレート時に実行されるSQL
# --- !Ups                                                                                                                                                                          

    CREATE TABLE user
    (
        id INT(12) UNSIGNED NOT NULL UNIQUE AUTO_INCREMENT,
        name VARCHAR(200) NOT NULL UNIQUE,
        first_name VARCHAR(200),
        last_name VARCHAR(200),
        full_name VARCHAR(200),
        password VARCHAR(200),
        avator_url VARCHAR(200),
        email VARCHAR(200),
        provider enum ('twitter', 'facebook', 'google', 'github', 'userpass', 'linkedin', 'foursquare', 'dropbox', 'xing', 'instagram', 'vk', 'weibo') NOT NULL,
        auth_method VARCHAR(50),
        oauth1token VARCHAR(200),
        oauth1secret VARCHAR(200),
        oauth2access_token VARCHAR(200),
        oAuth2TokenType VARCHAR(200),
        oAtuh2ExpiresIn INT,
        oAuth2RefreshToken VARCHAR(200),
        sex enum ('male', 'female', 'other') default 'other',
        description TEXT,

        created DATETIME NOT NULL,
        modified DATETIME NOT NULL,

        PRIMARY KEY (id)
    ) CHARSET utf8 COMMENT = 'User Accounts';

    /* Create Indexes */
    CREATE UNIQUE INDEX USER_IDX ON user (name ASC);
    CREATE INDEX ACCOUNT_IDX ON user (id ASC);


# Downs以下はマイグレート差し戻し時に実行されるSQL
# --- !Downs

    DROP INDEX USER_IDX ON user;
    DROP INDEX ACCOUNT_IDX ON user;

    DROP TABLE user;

Controllerの修正

Application.scalaの修正

app/controllers/Application.scala
package controllers

import play.api._
import play.api.mvc._
import securesocial.core._

object Application extends securesocial.core.SecureSocial {

  def index = SecuredAction { implicit request =>
    // ここにくる時点でfullNameやアクセストークンは取得済み                                                                           
    val name = request.user.fullName
    val provider = request.user.identityId.providerId

    // ためしにアクセストークンとアクセスシークレットを取得してみる。                                                     
    var token = ""
    val oAuth1Info:Option[OAuth1Info] = request.user.oAuth1Info
    oAuth1Info match {
      case Some(info) => {
        token = info.token
        val secret = info.secret
      }
      case None => ()
    }

    Ok(views.html.index(name, provider, request.user.avatarUrl, token))
  }

  def page = UserAwareAction { implicit request =>
    val userName = request.user match {
      case Some(user) => user.fullName
      case _ => "guest"
    }
    Ok("Hello %s".format(userName))
  }

}

Modelの作成

Userクラスの作成

app/models/User.scala
package models

import java.io.Serializable
import securesocial.core._
import securesocial.core.providers._
import anorm._ 
import anorm.SqlParser._

import play.api.db._
import play.api.Play.current// DB.withConnectionで必要


case class User(
    id:Pk[Long],
    name:String,
    provider:String,
    email:Option[String],
    passwordInfo:Option[PasswordInfo],
    first_name:Option[String],
    last_name:Option[String],
    full_name:Option[String],
    avatarUrl:Option[String],
    authMethod: securesocial.core.AuthenticationMethod,
    oAuth1Info: Option[securesocial.core.OAuth1Info],
    oAuth2Info: Option[securesocial.core.OAuth2Info]) extends Identity
{

  def isEmpty : Boolean = {
      id != NotAssigned
      }

  def identityId: securesocial.core.IdentityId = {
      IdentityId(name, provider)
  }

  def firstName:String = {
      if(first_name == null) return ""

      first_name match {
        case Some(s) => s
          case None => ""
      }
  }

  def lastName:String = {
      if(last_name == null) return ""

      last_name match {
        case Some(s) => s
          case None => ""
      }
  }

  def fullName: String = {
      if(full_name == null){
        ""
      }else{
        full_name match {
                case Some(s) => s
              case None => ""
        }
      }
  }

}

object User {

   val parser: RowParser[User] = {
    get[Pk[Long]]("user.id") ~
    get[String]("user.name") ~
    get[String]("user.provider") ~
    get[Option[String]]("user.email") ~
    get[Option[String]]("user.password") ~
    get[Option[String]]("user.first_name") ~
    get[Option[String]]("user.last_name") ~
    get[Option[String]]("user.full_name") ~
    get[Option[String]]("user.avator_url") ~
    get[String]("user.auth_method") ~
    get[String]("user.oauth1token") ~
    get[String]("user.oauth1secret") ~
    get[String]("user.oauth2access_token") ~
    get[Option[String]]("user.oAuth2TokenType") ~
    get[Option[Int]]("user.oAtuh2ExpiresIn") ~
    get[Option[String]]("user.oAuth2RefreshToken") map {
      case id ~ name ~ provider ~ email ~ password ~ first_name ~ last_name ~ full_name ~ 
           avatorUrl ~ auth_method ~ oauth1token ~ oauth1secret ~ oauth2access_token ~ tokenType ~ expiresIn ~ oAuth2RefreshToken => 
        val pass = password match {
          case Some(s) => {
            id match {
              case NotAssigned => None
              case Id(num) => Some(new PasswordInfo(num.toString(), s))
            }
          }
          case None => None
        }

        val authMethod = auth_method match {
          case "OAuth1" => AuthenticationMethod.OAuth1
          case _        => AuthenticationMethod.OAuth2
        }
        val oAuth1Info = if(oauth1token == null || oauth1token.length == 0) None else Some(OAuth1Info(oauth1token, oauth1secret))
        val oAuth2Info = if(oauth2access_token == null || oauth2access_token.length == 0)
               None
             else
               Some(OAuth2Info(oauth2access_token, null, null, null))
        User(id, name, provider, email, pass, first_name, last_name, full_name, avatorUrl, authMethod, oAuth1Info, oAuth2Info)
    }
   }


  def findByName(name:String, provider:String) : Option[User] = {
    println("User.findByName. name:%s, provider: %s".format(name, provider))

    provider match {
      case "userpass" => {  // mail & password login
    DB.withConnection("ebdb"){ implicit connection =>
        val user = SQL("select * from user where name = {name}").on('name -> name).as(User.parser.singleOpt)
        println("email.  user = " + user)
        user
    }
      }
      case _ => {  // SNS login
        println("SNS Login: findByName")

    DB.withConnection("ebdb"){ implicit connection =>
        val user = SQL("select * from user where name = {name}").on('name -> name).as(User.parser.singleOpt)
        println("SNS user = " + user)
        user
    }
      }
    }

  }

  def findById(id: Long): Option[User] = {
    println("User.findById")

    DB.withConnection("ebdb") { implicit connection =>
      SQL("select * from user where id = {id}").on('id -> id).as(User.parser.singleOpt)
    }
  }

  def findByEmailAndProvider(email: String, providerId: String): Option[User] = {
    println("User.findByEmailAndName")

    DB.withConnection("ebdb") { implicit connection =>
      SQL("select * from user where email = {email} and provider = {providerId}").on('email -> email, 'providerId -> providerId).as(User.parser.singleOpt)
    }
  }

  def insert(i : Identity) : Option[User] = {
    val provider = i.identityId.providerId
    val name = i.identityId.userId
    val authMethod = i.authMethod
    val avatarUrl = i.avatarUrl
    val firstName = i.firstName
    val fullName = i.fullName
    val lastName = i.lastName
    val oAuth1Info = i.oAuth1Info
    val oAuth2Info = i.oAuth2Info
    val email = i.email
    val passwordOpt = i.passwordInfo
    val password = passwordOpt match {
      case Some(pass) => Some(pass.password)
      case None => None
    }

    oAuth1Info match {
      case Some(info) => println("oAuth1 token" + info.token + ", secret = " + info.secret)
      case None => println("no oAuth1")
    }
    oAuth2Info match {
      case Some(info) => println("oAuth2 accesstoken = " + info.accessToken )
      case None => println("no oAuth2")
    }

    val (token, secret) = oAuth1Info match {
      case Some(info) => (info.token, info.secret)
      case None => ("", "")
    }
    val (accessToken, tokenType, expiresIn, refreshToken):(String, Option[String], Option[Int], Option[String]) = oAuth2Info match {
          case Some(info) => (info.accessToken, info.refreshToken, info.expiresIn, info.tokenType)
          case None => ("", None, None, None)
    }

    insert(name,
           password,
           provider,
           email,
           authMethod,
           avatarUrl,
           firstName,
           lastName,
           fullName,
           token,
           secret,
           accessToken,
           tokenType,
           expiresIn,
           refreshToken)
  }

  def createNewUser(i: Identity) : Option[User] = {
    println("User.createNewUser")

    insert(i)
  }

  def insert(name:String,
      password:Option[String],
      provider:String,
      emailOpt:Option[String],
      authMethod:AuthenticationMethod,
      avatarUrlOpt:Option[String],
      firstNameNullable:String,
      lastNameNullable:String,
      fullNameNullable:String,
      oAuth1TokenNullable:String,
      oAuth1SecretNullable:String,
      oAuth2AccessTokenNullable:String,
      oAuth2TokenType:Option[String],
      oAtuh2ExpiresIn:Option[Int],
      oAuth2RefreshToken:Option[String]
  ) : Option[User] =
  {
    println("User.insert main")

    var user : Option[User] = findByName(name, provider)

    user match {
      case Some(_) => return user
      case None => () // do nothing
    }

    DB.withConnection("ebdb"){ implicit conn =>
      var id:Option[Long] = None

      val email = emailOpt.getOrElse("")
      val avatarUrl = avatarUrlOpt.getOrElse("")
      val firstName = if(firstNameNullable == null) None else Some(firstNameNullable)
      val lastName =  if(lastNameNullable == null) None else Some(lastNameNullable)
      val fullName = if(fullNameNullable == null) None else Some(fullNameNullable)
      val oAuth1Token = if(oAuth1TokenNullable == null) None else Some(oAuth1TokenNullable)
      val oAuth1Secret = if(oAuth1SecretNullable == null) None else Some(oAuth1SecretNullable)
      val oAuth2AccessToken = if(oAuth2AccessTokenNullable == null) None else Some(oAuth2AccessTokenNullable)


      val sqlQuery = SQL("""
                insert into user (name, password, provider, email, first_name, last_name, full_name, avator_url,
                                  auth_method, oauth1token, oauth1secret, oauth2access_token, oAuth2TokenType, oAtuh2ExpiresIn, oAuth2RefreshToken, created, modified)
                values ({name}, {password}, {provider}, {email}, {firstName}, {lastName}, {fullName}, {avatarUrl},
                        {authMethod}, {oAuth1Token}, {oAuth1Secret}, {oAuth2AccessToken}, {oAuth2TokenType}, {oAtuh2ExpiresIn}, {oAuth2RefreshToken}, now(), now());
            """)
      id = sqlQuery.on(
        'name -> name,
        'password -> password,
        'provider -> provider,
        'email -> email,
        'firstName -> firstName,
        'lastName -> lastName,
        'fullName -> fullName,
        'avatarUrl -> avatarUrl,
        'authMethod -> authMethod.toString(),
        'oAuth1Token -> oAuth1Token,
        'oAuth1Secret -> oAuth1Secret,
        'oAuth2AccessToken -> oAuth2AccessToken,
        'oAuth2TokenType -> oAuth2TokenType,
        'oAtuh2ExpiresIn -> oAtuh2ExpiresIn,
        'oAuth2RefreshToken -> oAuth2RefreshToken
      ).executeInsert()

      println("SQL Insert id " + id)

      val oAuth1Info = if(oAuth1TokenNullable == null) None else Some(OAuth1Info(oAuth1TokenNullable, oAuth1SecretNullable))
      val oAuth2Info = if(oAuth2AccessTokenNullable == null) None else Some(OAuth2Info(oAuth2AccessTokenNullable, oAuth2TokenType, oAtuh2ExpiresIn, oAuth2RefreshToken))


      id match {
        case Some(num:Long) => {
          password match {
            case None => user = Some(new User(new Id(num), name, provider, emailOpt, None, firstName, lastName, fullName, avatarUrlOpt, authMethod, oAuth1Info, oAuth2Info))
            case Some(s) => {
              val pass = new PasswordInfo(num.toString(), s) // hasher:このパスワードをhashするのに本人のidを使用している
              user = Some(new User(new Id(num), name, provider, emailOpt, Some(pass), firstName, lastName, fullName, avatarUrlOpt, authMethod, oAuth1Info, oAuth2Info))
            }
          }
        }
        case None => {
          println("db user = None")
          user = None
        }
      }
    } // end of DB.withConnection

    user
  }
}

MyUserService.scalaの作成

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),
        user.firstName,
        user.lastName,
        user.fullName,
        user.email,
        user.avatarUrl,
        user.authMethod,
        user.oAuth1Info,
        user.oAuth2Info,
        user.passwordInfo)
  }
}


class MyUserService(application: Application) extends UserServicePlugin(application) {
  private var tokens = Map[String, Token]()

  def find(id: IdentityId): Option[Identity] = {
    User.findByName(id.userId,id.providerId);
  }

  def findByEmailAndProvider(email: String, providerId: String): Option[Identity] = {
    User.findByEmailAndProvider(email, providerId)
  }

  def save(i: Identity) = {
    val ii=i.identityId
    var user = User.findByName(ii.userId,ii.providerId);
    user match {
      case Some(u) => {
        user = User.insert(u)
        MySocialUser(user.get)
      }
      case None => {
        println("save None Profile")
        user = User.createNewUser(i)
        MySocialUser(user.get)
      }
    }
  }

  def save(token: Token) {
    println("save(token:) 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)
  }
}

カスタムViewの作成

index.scala.htmlの修正

app/views/index.scala.html
@(user: String, provider: String, avatarUrl: Option[String], token: String)(implicit request: RequestHeader, lang: Lang)

@import helper._
@import securesocial.core.Registry
@import securesocial.core.AuthenticationMethod._
@import securesocial.core.providers.UsernamePasswordProvider.UsernamePassword

@main("Welcome") {
  <div class="page-header">
    <h1>Welcome!</h1>
  </div>

  Welcome @(user)@@@(provider)!

  @avatarUrl match {
    case Some(url) => { <image src="@url" /> }
    case None => {}
  }

  <p>
    Your access token is '@token'
  </P>

  <p>
    <a href="/logout">Logout</a>
  </p>
}

実行

ここまでで動作するはずですので、実行してみます。

$ play run

ブラウザで http://localhost:9000/ に接続してうまくツイッターでログイン認証できれば成功です。

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8