技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

SPA アプリそろそろ作って見たいんじゃい(3)

とりあえず SpringBootSecurity を設定してみた回

まずは build.gradle に依存を追加する。

dependencirs{
    compile('org.springframework.boot:spring-boot-starter-security')
    // あとはおすきに

そしてログインに使用する DB からデータを取得するためのインターフェースを追加する。
基本的に DB にアカウントとパスワードを格納しているので、それを取得する。

interface AccountRepository : JpaRepository<Account, Long> {
    fun findOneByName(name: String): Optional<Account>
}

次に、Spring がログインの際に使用するユーザ情報のデータ定義を作る。

package net.white_azalea.todo_demo.service.secure

import net.white_azalea.todo_demo.repositories.Account
import org.springframework.security.core.authority.AuthorityUtils
import org.springframework.security.core.userdetails.User

class LoginAccount(account: Account) : User(account.name, account.password, AuthorityUtils.createAuthorityList("LOGIN_ROLE")) {

}

中身はあってもなくてもいい。
要するにアカウント ID と、ハッシュ化されたパスワードがあり、その時の権限を用意する。

次は Spring がアカウント ID を受け付けたときにログイン情報を検索するロジックの実装。

package net.white_azalea.todo_demo.service.secure

import net.white_azalea.todo_demo.repositories.AccountRepository
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component

@Component
class AccountDetailService(
        val accountRepository: AccountRepository
) : UserDetailsService {
    override fun loadUserByUsername(username: String?): UserDetails {
        val acc = accountRepository.findOneByName(username.orEmpty())

        if (acc.isPresent) {
            return LoginAccount(acc.get())
        } else {
            throw UsernameNotFoundException("Account does not exists.")
        }
    }
}

パスワードのエンクリプトと、チェックを行うためのクラスも作成。

package net.white_azalea.todo_demo.service.secure

import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.stereotype.Component
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

@Component
class Crypto : PasswordEncoder {

    override fun encode(rawPassword: CharSequence?): String {
        if (rawPassword == null) {
            return ""
        }
        return this.sign("SecretKey", rawPassword.toString())
    }

    /**
     * Check password matching.
     */
    override fun matches(rawPassword: CharSequence?, encodedPassword: String?): Boolean {
        if (rawPassword == null || encodedPassword == null) {
            return false
        }
        return this.sign("SecretKey", rawPassword.toString()).equals(encodedPassword)
    }

    /**
     * Sign with key and value.
     *
     * Not good to use password hash.
     * TODO: Have to use bcrypt to password.
     * https://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/crypto/bcrypt/BCrypt.html
     */
    fun sign(key: String, value: String): String {
        val secretKey = SecretKeySpec(key.toByteArray(), "HmacSHA1")
        val mac = Mac.getInstance("HmacSHA1")
        mac.init(secretKey)
        val bytes = mac.doFinal(value.toByteArray())
        return Base64.getEncoder().encodeToString(bytes)
    }
}

そしてこれらを使ってログインをするための設定クラス

package net.white_azalea.todo_demo.service.secure

import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.authentication.configurers.GlobalAuthenticationConfigurerAdapter
import org.springframework.security.core.userdetails.UserDetailsService

@Configuration
class AuthenticationConfiguration(
        val userDetailsService: UserDetailsService,
        val crypto: Crypto
) : GlobalAuthenticationConfigurerAdapter() {

    override fun init(auth: AuthenticationManagerBuilder?) {
        auth?.userDetailsService(userDetailsService)
                ?.passwordEncoder(crypto)
    }
}

最後に設定これがあれば /loginaccount, password を POST すれば認証処理が走る。

package net.white_azalea.todo_demo

import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.builders.WebSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.web.authentication.AuthenticationFailureHandler
import org.springframework.security.web.util.matcher.AntPathRequestMatcher

@Configuration
@EnableWebSecurity
class SecurityConfig : WebSecurityConfigurerAdapter(false) {

    override fun configure(web: WebSecurity?) {
        web?.ignoring()?.antMatchers(
            "/images/**",
            "/css/**",
            "/javascript/**",
            "/webjars/**")
    }

    fun authFailure(): AuthenticationFailureHandler {
        return AuthenticationFailureHandler { request, response, exception ->
            response.status = 200
            response.sendRedirect("/?error=Failed to access.")
        }
    }

    override fun configure(http: HttpSecurity?) {

        // 認可設定
        http?.authorizeRequests()
                ?.antMatchers("/", "/login")?.permitAll() // index は全ユーザアクセス許可
                ?.anyRequest()?.authenticated() // それ以外は認証要求

        // ログイン認定
        http?.formLogin()
                ?.loginProcessingUrl("/login") // login url
                ?.loginPage("/") // ログインフォームのパス
                ?.failureHandler(authFailure()) // ログイン失敗時のハンドラ
                ?.defaultSuccessUrl("/menu") // ログイン結果
                ?.usernameParameter("account") // ユーザ名パラメータ
                ?.passwordParameter("password")  // パスワードのパラメータ名
                ?.and()

        // ログアウト設定
        http?.logout()
                ?.logoutRequestMatcher(AntPathRequestMatcher("/logout**")) // ログアウト処理のパス
                ?.logoutSuccessUrl("/") // ログアウト完了時のパス
    }
}

実際試して見たが、Cookie 嬢の JSESSION_ID なんかもきちんと更新されていたし、セッションハイジャック対策もそこそこされているのだろう。
送信フォームもこんなカンジ

<!DOCTYPE HTML>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>top page</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
    <h1>Login form!</h1>
    <form action="/login" method="post" th:action="@{'/login'}">
        <div>Account:</div>
        <input type="text" name="account" />
        <div>Pass:</div>
        <input type="password" name="password" />
        <button type="submit">Do login</button>
    </form>
</body>
</html>

気をつけなければならないのは @{'/login'} の部分。
これで作成しないと、URL に CSRF ヘッダも生成されない。つまり POST が蹴られる。

やっぱ大分考慮されてるのねーってカンジである。

SPA アプリそろそろ作って見たいんじゃい(2)

次に DB 接続と Migration してみる。
SpringBoot では公式に Flyway 書いてあるので、そっちを使う。

  • build.gradlecompile("org.flywaydb:flyway-core") 追加。
  • application.properties に下記を記載
   spring.datasource.url=jdbc:h2:./db/example;MODE=MySQL
   spring.jpa.hibernate.ddl-auto=validate
   flyway.locations=db/migration
   flyway.schemas=PUBLIC
  • src/main/resources 配下に db/migration ディレクトリを作って、SQL ツッコミ
    注意点は、flyway はスキーマバージョン管理テーブルを作るのに、デフォルトスキーマ「PUBLIC」にアクセスするべき所を、小文字で「public」アクセスしようとして失敗する∑(゚Д゚)
-- see: http://qiita.com/niwashun/items/dc71dfba4cbb9e9eef98
CREATE SCHEMA IF NOT EXISTS "public";

CREATE TABLE `accounts` (
    `id` BIGINT AUTO_INCREMENT NOT NULL PRIMARY KEY,
    `name`     VARCHAR(255) NOT NULL,
    `password` VARCHAR(127) NOT NULL
);

-- account and password.
INSERT INTO `accounts` (`name`, `password`) VALUES ('account', '8RSglSZ8nqBtmR07hEM3E0Hdslo=');

ここまで来たら、あとは Entity とか作るだけ。

package net.white_azalea.todo_demo.repositories

import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id

@Entity(name = "accounts")
data class Account(
        @Id @GeneratedValue val id: Long,
        val name: String,
        val password: String
) {
    /**
     * for Spring Data JPA
     */
    constructor() : this(0, "", "") {
        // Nothing to do.
    }
}
package net.white_azalea.todo_demo.repositories

import org.springframework.data.jpa.repository.JpaRepository
import java.util.*

interface AccountRepository : JpaRepository<Account, Long> {
    fun findOneByName(name: String): Optional<Account>
}

SPA アプリそろそろ作って見たいんじゃい(1)

話としてはこれだけ。
仕様としては、ログインと TODO だけで、特にセキュリティも考えない。

SPAの構成は、バックエンドに SpringBootWeb + Thymeleaf + SpringDataJPA + SQLite3 構成。
フロントに riot + riot-control でもしようかなと考えてる。

まずは SpringBoot を落としてくる。コレ自体は、サイト で設定してダウンロードすればよい。

f:id:white-azalea:20170701182657p:plain

選択したのは Web + JPA + Thymeleaf ここまではテンプレ作ってくれる。

で、とりあえずエンティティ作って、HelloWorld まで書いたものをぽとり。

github.com

正味 3 時間くらいかかった。
正直たかがこれだけでと思わなくない。

  1. Kotlin ほとんど書いてないから戸惑った
  2. Java と違って、build.gradlespringBoot とかいうセクションを追加しないと、ComponentScan がうまく走らないという状況に気づかなかった

いやー知らないものを詰め込みすぎた感満載。
とはいえ、Kotlin でとりあえず書けるようにはなってきたかな。

JavaScala 足して 2 で割った感。

「面倒なことは Python にやらせよう」の実習課題(3章以降)

課題図書はこれ

gist ではじめてみた。

これも1日1課題以上かなー

gist.github.com

プログラム脳を鍛える数学パズル on python3

追記: とりま gist に置いてみた (2017/06/27

お題はこれで、目標は1日1個以上。

…ダレ無いといいけど…。

続きを読む

超久々に Python 弄ってた

AI 関連が実質 Python 一択で、数学系ライブラリもその表示も Python だとかなり揃ってたので、久々にやってみようとしてどハマりした記録。
何をしようとしたかというと、下記を Python で実装すればいいかなーとかとか漠然と考えてどハマりした。

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

プログラマ脳を鍛える数学パズル シンプルで高速なコードが書けるようになる70問

どこにはまったかというと問題2。

問題内容は書籍を買うか、下記を見て欲しい。

white-azalea.hatenablog.jp

  1. Python だし、eval あるよねー使い方調べるついでに使って見るかー」
    → 9009 に * 突っ込むとしても 90*09 とかになるとダウンする。09 ってなんぞやー
  2. 「諦めて、* が必要な箇所で文字列区切って掛け算するか」
    → 変数タイポで 1h どハマり(実行時評価だから実行するまでわからないので単純なミスが見つからない)
  3. 「計算できたし正解を比較するか」
    → 久々にやってて問題文間違って覚えてて、「計算の前後で出た四桁数字が順不同で含まれてることじゃね?」と勘違いして実装。
    → 片方の文字列を1時づつ取り出して、もう片方に含まれるか?という判定をするが、122 - 124 でも true になる。
    → なら逆も比較したらええやんと思いきや、122 - 12 で結局OK判定になってしまう。
    → なので、片方の文字列を1時づつ取り出して、もう片方から1字づつ削除(immutable じゃなくて背中が痒く…)してみるも、 Java の String.replace 同様の replace メソッドを見つけたが、第3引数に指定がないと replaceAll 同等動作とか知らずにハマる。
    → 突破したら突破したで仕様違いに気づいて脱力…回文かよ…

で、色々やった挙句、こんな単純な問題に 3 時間も悩まされたという。

途中途中で Syntax error に悩まされたのもでかい。
TextMate2 を使っているが、やっぱ IDE なんかでリアルタイムに syntax チェックしてないとわからん。
加えて言えば、実行字型の為に、動かしてから死んでデバッグの後戻りがひどかった…

慣れの問題なのだろうか、例えば代入時に変数名が一時違った位でも、「新しい変数ができた」と認識されて平然と動いてしまう為、何が悪いか超追いかけづらかった…。

結局最後は全部関数にした…もう…関数型で…イイヨ。
たとえ…Python に… tail call recursive が実装されてなかったとしても…!

positions = [
    [1], [2], [3],
    [2, 1], [3, 2], [3, 1],
    [3, 2, 1]
]

def split(str, pos, stack):
    if len(pos) == 0:
        stack.append(str)
        return stack
    else:
        i = pos[0]
        stack.append(str[i:])
        return split(str[:i], pos[1:], stack)

from functools import reduce

def multiple(list):
    return reduce(lambda a,b: a * b, list)

def is_kaibun(left, right):
    return left == right[::-1]

for cur in range(1000, 9999):
    strCur = str(cur)
    for pos in positions:
        splitted = split(strCur, pos, [])
        result   = multiple(map(lambda v: int(v), splitted))
        if is_kaibun(strCur, str(result)):
            print(str(splitted) + " = " + str(result))
            print("success: " + strCur)

Java8 の Stream が物足りない人たちへ

www.vavr.io

言ってみれば、Scala 並の Collection を Java で提供するライブラリ。
何がいいって、Tuple とか Either 型もあるから、クソッタレな null や throw とおさらばできる。

その昔 javaslang (java.lang があるなら俗語があってもいいよね)と名乗ってた。
個人的にはこっちのセンスが好きだったんだけど…物言いでもはいりましたかね。

まぁケースバイケースで。