使用したバージョン
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/ に接続してうまくツイッターでログイン認証できれば成功です。