とりあえず 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) } }
最後に設定これがあれば /login
に account
, 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 が蹴られる。
やっぱ大分考慮されてるのねーってカンジである。