2018年2月22日木曜日

ELBの最適なタイムアウト設定

ELBのクラッシックロードバランサのタイムアウトの話
デフォルトで60秒のアイドルタイムアウトが設定されています。

アイドルタイムアウトは、1バイトも送受信が行われないコネクションを何秒で切断するかの設定です。

長いアップロードなどでで継続的にデータが行われる場合は問題ないです。
送受信が行われないときのタイムアウトです。
例えば、サーバーがレスポンスなくタイムアウト値の時間を超えると切断する。

apacheの設定でのタイムアウトの最適化の話

/etc/httpd/conf/httpd.conf

apacheのタイムアウト > ELBのアイドルタイムアウト
基本的にこちらのタイムアウトは、ELBのタイムアウトより大きくするのが良い。

普通は、長い接続がパフォーマンスを悪くするので、
短いタイムアウトを設定するが
ロードバランサーがそこは面倒をみてくれるので短くなくてよい。

むしろロードバランサーはサーバーとの接続を維持したほうが、サーバーとのハンドシェイクなどのコストが減りパフォーマンスが良くなる可能性が高い。

KeepAliveとTimeoutをELBのアイドルタイムアウトの+60sや2倍などにするとよい。

"Timeout"
おすすめの値:120
ELBのidletimeoutが60sの場合
idletimeout+60sとか2倍とかかな

"KeepAlive"
おすすめの値:On

"KeepAliveTimeout"
おすすめの値:120
ELBのidletimeoutが60sの場合
idletimeout+60sとか2倍とかかな

"MaxKeepAliveTimeout"
おすすめの値:100

参考
https://aws.amazon.com/premiumsupport/knowledge-center/apache-backend-elb/

2018年2月21日水曜日

iOSアプリ 端末時間をズラしてるとCookieの有効期限が...!

クッキーってこんな感じやん
 
Set-Cookie: AccessToken=a3fWa039adnaknd; Expires=Wed, 11 Oct 2018 07:28:00 GMT; Secure; HttpOnly

で、有効期限ってWed, 11 Oct 2018 07:28:00 GMTみたいに日付で指定されてるんだけど、この日時ってどこの時間をみてると思う?

この日時ってiOSアプリでは端末の時間で見てるみたいなんだよ。

つまりよ、クッキーの有効期限を10分後にしてて、iPhoneの端末時間を10分以上進めてるとするよ。

そうすると....

一瞬でクッキーが溶けていくーーー(´°̥̥̥̥̥̥̥̥ω°̥̥̥̥̥̥̥̥`)

ちなみに、Androidだとサーバーの時間との相対的な日時で見てくれてるみたいで、クッキーは溶けず、問題にならない。

クライアントによって解釈が違うのよね。
PCのブラウザも解釈がそれぞれ違う模様。

ということでiOSでクッキーを受け取ったら、なんとかしてサーバーとの相対時間で有効期限を設定して保持したい。

iOSでクッキーを管理してるのは、HTTPCookieStorage
HTTPCookieStorageを改造してクッキー保存するときに有効期限を相対的な値に書き換える作戦で行ってみる

CustomHTTPCookieStorage
class CustomHTTPCookieStorage : HTTPCookieStorage {
    
    private let source: HTTPCookieStorage
    
    init(source: HTTPCookieStorage) {
        self.source = source
    }
    
    
    /*!
     @abstract Get all the cookies
     @result An NSArray of NSHTTPCookies
     */
    override var cookies: [HTTPCookie]? {
        get {
            print("CustomHTTPCookieStorage.cookies -> \(self.source.cookies?.count.description ?? "nil")")
            return self.source.cookies
        }
    }
    
    
    /*!
     @method setCookie:
     @abstract Set a cookie
     @discussion The cookie will override an existing cookie with the
     same name, domain and path, if any.
     */
    override func setCookie(_ cookie: HTTPCookie) {
        print("CustomHTTPCookieStorage.setCookie", cookie)
        //super.setCookie(cookie)
        self.source.setCookie(cookie)
    }
    
    
    /*!
     @method deleteCookie:
     @abstract Delete the specified cookie
     */
    override func deleteCookie(_ cookie: HTTPCookie) {
        print("CustomHTTPCookieStorage.deleteCookie", cookie)
        //super.deleteCookie(cookie)
        self.source.deleteCookie(cookie)
    }
    
    
    /*!
     @method removeCookiesSince:
     @abstract Delete all cookies from the cookie storage since the provided date.
     */
    @available(iOS 8.0, *)
    override func removeCookies(since date: Date) {
        print("CustomHTTPCookieStorage.removeCookies", date)
        //super.removeCookies(since: date)
        self.source.removeCookies(since: date)
    }
    
    
    /*!
     @method cookiesForURL:
     @abstract Returns an array of cookies to send to the given URL.
     @param URL The URL for which to get cookies.
     @result an NSArray of NSHTTPCookie objects.
     @discussion The cookie manager examines the cookies it stores and
     includes those which should be sent to the given URL. You can use
     <tt>+[NSCookie requestHeaderFieldsWithCookies:]</tt> to turn this array
     into a set of header fields to add to a request.
     */
    override func cookies(for URL: URL) -> [HTTPCookie]? {
        print("CustomHTTPCookieStorage.cookies for URL \(URL) -> \(self.source.cookies?.count.description ?? "nil")")
        return self.source.cookies(for: URL)
        //return super.cookies(for: URL)
    }
    
    
    /*!
     @method setCookies:forURL:mainDocumentURL:
     @abstract Adds an array cookies to the cookie store, following the
     cookie accept policy.
     @param cookies The cookies to set.
     @param URL The URL from which the cookies were sent.
     @param mainDocumentURL The main document URL to be used as a base for the "same
     domain as main document" policy.
     @discussion For mainDocumentURL, the caller should pass the URL for
     an appropriate main document, if known. For example, when loading
     a web page, the URL of the main html document for the top-level
     frame should be passed. To save cookies based on a set of response
     headers, you can use <tt>+[NSCookie
     cookiesWithResponseHeaderFields:forURL:]</tt> on a header field
     dictionary and then use this method to store the resulting cookies
     in accordance with policy settings.
     */
    override func setCookies(_ cookies: [HTTPCookie], for URL: URL?, mainDocumentURL: URL?) {
        print("CustomHTTPCookieStorage.setCookies cookies for URL mainDocumentURL", cookies, URL ?? "nil", mainDocumentURL ?? "nil")
        //super.setCookies(cookies, for: URL, mainDocumentURL: mainDocumentURL)
        self.source.setCookies(cookies, for: URL, mainDocumentURL: mainDocumentURL)
    }
    
    
    /*!
     @abstract The cookie accept policy preference of the
     receiver.
     */
    override var cookieAcceptPolicy: HTTPCookie.AcceptPolicy {
        get {
            return self.source.cookieAcceptPolicy
        }
        set {
            self.source.cookieAcceptPolicy = newValue
        }
    }
    
    /*!
     @method sortedCookiesUsingDescriptors:
     @abstract Returns an array of all cookies in the store, sorted according to the key value and sorting direction of the NSSortDescriptors specified in the parameter.
     @param sortOrder an array of NSSortDescriptors which represent the preferred sort order of the resulting array.
     @discussion proper sorting of cookies may require extensive string conversion, which can be avoided by allowing the system to perform the sorting.  This API is to be preferred over the more generic -[NSHTTPCookieStorage cookies] API, if sorting is going to be performed.
     */
    @available(iOS 5.0, *)
    override func sortedCookies(using sortOrder: [NSSortDescriptor]) -> [HTTPCookie] {
        print("CustomHTTPCookieStorage.sortedCookies", sortOrder)
        return self.source.sortedCookies(using: sortOrder)
        //return super.sortedCookies(using: sortOrder)
    }
    
}

はい、やってみましたが、
HTTPCookieStorageを差し替えた途端、呼ばれなくなりました笑

次の作戦。
クッキーを受け取ったのを監視して、受け取った瞬間に有効期限を書き換える。

NotificationCenter.default.addObserver(
    self, selector: #selector(handleCookieManagerCookiesChanged(_:)),
       name: NSNotification.Name.NSHTTPCookieManagerCookiesChanged,
       object: nil)

@objc private func handleCookieManagerCookiesChanged (_ notification: Notification) {
        print("handleCookieManagerCookiesChanged", notification)
        guard let cs: HTTPCookieStorage = notification.object as? HTTPCookieStorage else {
            return
        }
        guard cs === self.manager.session.configuration.httpCookieStorage else {
            print("Different httpCookieStorage")
            return
        }
        self.adjustCookieExpires()
    }
extension A {
    func adjustCookieExpires () {
        guard let cs: HTTPCookieStorage = self.manager.session.configuration.httpCookieStorage else {
            return
        }
        adjustCookieExpires(cs: cs)
    }

    private func adjustCookieExpires (cs: HTTPCookieStorage) {
        print("adjustCookieExpires")
        for cookie : HTTPCookie in cs.cookies ?? [] {
            guard var properties: [HTTPCookiePropertyKey: Any] = cookie.properties else {
                print("cookie properties is nil")
                continue
            }
            var modifiled: Bool = false
            print("cookie properties = \(properties)")
            let cookieComment: String? = properties[HTTPCookiePropertyKey.comment] as? String
            print("cookie cookieComment = \(cookieComment ?? "nil")")
            if let expires: Date = properties[HTTPCookiePropertyKey.expires] as? Date {
                print("cookie expires = \(expires)")
                let adjusted: Bool = cookieComment?.contains(cookieExpiresAdjestedComment) ?? false
                if !adjusted {
                    properties[HTTPCookiePropertyKey.expires] = expires.addingTimeInterval(60 * 60 * 24 * 30)
                    let newComment: String
                    if let oldComment: String = properties[HTTPCookiePropertyKey.comment] as? String {
                        newComment = oldComment + " " + cookieExpiresAdjestedComment
                    } else {
                        newComment = cookieExpiresAdjestedComment
                    }
                    properties[HTTPCookiePropertyKey.comment] = newComment
                    modifiled = true
                }
            }
            guard modifiled else {
                print("cookie not modifiled")
                continue
            }
            guard let newCookie: HTTPCookie = HTTPCookie(properties: properties) else {
                print("cookie newCookie is nil")
                continue
            }
            print("newCookie = \(newCookie)")
            cs.setCookie(newCookie)
        }
    }
}

あ、、、動かしてみてから気づく。

HTTPCookieStorageに入った瞬間消えてるやん....

あきらめて、サーバー側でクッキーの有効期限をそもそも伸ばしました。

2018年2月17日土曜日

GAE x Spring Boot で Spring Security が上手く動かぬ

Google App Engine Java8 Standard x Spring Boot x Spring Security

同じ現象で困ってる方の助けになれば幸いでございます!
と、ほかの解決方法探し中...!

構成
  • Google App Engine Java 8 Standard 
  • Spring Boot 1.5.9 
  • Spring Security 
  • Kotlin
  • Gradle 

 Devローカルサーバーならしっかり動いている。

 本番環境で問題が発生する。

 デプロイしてちゃんと起動はする。
 しかし、Spring Securityを使ったフォームのログインで問題。

 ログインボタンを押した後、
なにごとも無かったかのように、
ログイン画面に戻る笑

どうやらGAEのセッションハンドラを使うと上手く動かぬもよう。

GAEのセッションハンドラを使わず、
GAE内のmemcacheもしくはDatastoreを使って
自前のセッションハンドラにします。

DatastoreSessionRepository
package com.hometest.libs.appengine.session;

import com.google.appengine.api.datastore.*
import org.springframework.session.MapSession
import org.springframework.session.SessionRepository
import org.springframework.stereotype.Component
import java.io.*
import java.util.logging.Logger

@Component
class DatastoreSessionRepository(private val datastoreService: DatastoreService) : SessionRepository<MapSession> {
    private val logger = Logger.getLogger(javaClass.simpleName)
    private val maxInactiveIntervalInSeconds: Int = 3600
    private val kind = "Session"
    private val propertyName = "serialized"

    override fun createSession(): MapSession = MapSession().also { session ->
        session.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds
        logger.info { "createSession() = ${session.id}" }
    }

    override fun save(session: MapSession) {
        logger.info { "save(${session.id}) with expiration ${session.maxInactiveIntervalInSeconds}" }

        ByteArrayOutputStream().use { byteArray ->
            ObjectOutputStream(byteArray).use { outStream ->
                outStream.writeObject(session)
            }
            datastoreService.put(Entity(kind, session.id).apply {
                // TODO: Check byteArray size. This byte array can be no bigger than 1MB.
                // https://cloud.google.com/appengine/docs/standard/java/javadoc/com/google/appengine/api/datastore/Blob
                setProperty(propertyName, Blob(byteArray.toByteArray()))
            })
        }
    }

    override fun getSession(id: String): MapSession? {
        val blob: Blob? = try {
            datastoreService.get(KeyFactory.createKey(kind, id))
                    .getProperty(propertyName) as? Blob
        } catch (_: EntityNotFoundException) {
            null
        }
        val session: MapSession? = blob?.let {
            ByteArrayInputStream(it.bytes).use { byteArray ->
                ObjectInputStream(byteArray).use { inStream ->
                    (inStream.readObject() as MapSession).also { session ->
                        session.lastAccessedTime = System.currentTimeMillis()
                    }
                }
            }
        }
        logger.info { "getSession($id) = ${session?.id}" }
        return session
    }

    override fun delete(id: String) {
        logger.info { "delete($id)" }
        datastoreService.delete(KeyFactory.createKey(kind, id))
    }
}
MemcacheSessionRepository
package com.hometest.libs.appengine.session;

import com.google.appengine.api.memcache.Expiration
import com.google.appengine.api.memcache.MemcacheService
import org.springframework.session.MapSession
import org.springframework.session.SessionRepository
import org.springframework.stereotype.Component
import java.util.logging.Logger

@Component
class MemcacheSessionRepository(private val memcacheService: MemcacheService) : SessionRepository<MapSession> {
    private val logger = Logger.getLogger(javaClass.simpleName)
    private val maxInactiveIntervalInSeconds: Int = 3600

    override fun createSession() = MapSession().also { session ->
        session.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds
        logger.info { "createSession() = ${session.id}" }
    }

    override fun save(session: MapSession) {
        logger.info { "save(${session.id}) with expiration ${session.maxInactiveIntervalInSeconds}" }
        memcacheService.put(session.id, session, Expiration.byDeltaSeconds(session.maxInactiveIntervalInSeconds))
    }

    override fun getSession(id: String): MapSession? =
            (memcacheService.get(id) as? MapSession)?.also { session ->
                session.lastAccessedTime = System.currentTimeMillis()
            }.also { session ->
                        logger.info { "getSession($id) = ${session?.id}" }
                    }

    override fun delete(id: String) {
        logger.info { "delete($id)" }
        memcacheService.delete(id)
    }
}
AppengineConfiguration
package com.hometest.config;

import com.google.appengine.api.datastore.DatastoreService
import com.google.appengine.api.datastore.DatastoreServiceFactory
import com.google.appengine.api.memcache.MemcacheService
import com.google.appengine.api.memcache.MemcacheServiceFactory
import com.hometest.libs.appengine.session.MemcacheSessionRepository
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.session.MapSession
import org.springframework.session.web.http.SessionRepositoryFilter

@Configuration
class AppengineConfiguration {

    // AppEngine Session
    @Bean
    fun memcacheService(): MemcacheService {
        return MemcacheServiceFactory.getMemcacheService()
    }

    @Bean
    fun datastoreService(): DatastoreService {
        return DatastoreServiceFactory.getDatastoreService()
    }

    @Bean
    fun springSessionRepositoryFilter(sessionRepository: MemcacheSessionRepository): SessionRepositoryFilter<MapSession> {
        return SessionRepositoryFilter(sessionRepository)
    }
//    @Bean
//    fun springSessionRepositoryFilter(sessionRepository: DatastoreSessionRepository): SessionRepositoryFilter<MapSession> {
//        return SessionRepositoryFilter(sessionRepository)
//    }

}

しかし、今度はローカルサーバーで認証をかけてないページでセッションクッキーが吐かれなくなった(;´Д`)

こちらは未解決。

一旦本番では問題なくクッキー履かれていいるのでまた今度。

もし解決方法見つけたら、く、ください !

参考