Edited at

SpringBoot(Kotlin)とFreemarkerでログインするサンプル作った

More than 1 year has passed since last update.

お仕事でSpringBootとFreeMarker使う事になったので、簡単なログインページを実装してみた。

また、フロントエンドにVueを使う事になりそうな感じなので、ついでにWebpackとGradle連携させて、Vueを読み込むところまでやってみた。

SpringBootのKotlinはまだ日本語のドキュメントが少ないので、ソースコード載せつつ要点かいつまんで説明していく。

ソースコード

https://github.com/renoinn/springboot-freemarker-vue-sample

dockerとかでDB用意すればそのまま動くはず。


build.gradle

buildscript {

ext {
kotlinVersion = '1.2.51'
springBootVersion = '2.0.5.RELEASE'
}
repositories {
mavenCentral()
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-allopen:${kotlinVersion}")
classpath("org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}")
classpath "com.moowork.gradle:gradle-node-plugin:1.2.0"
}
}

apply plugin: 'kotlin'
apply plugin: 'kotlin-spring'
apply plugin: 'kotlin-allopen'
apply plugin: 'kotlin-jpa'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'com.moowork.node'

group = 'com.sample'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
compileKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
freeCompilerArgs = ["-Xjsr305=strict"]
jvmTarget = "1.8"
}
}

repositories {
mavenCentral()
}

dependencies {
// spring
compile('org.springframework.boot:spring-boot-starter-actuator')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('org.springframework.boot:spring-boot-starter-freemarker')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.session:spring-session-core')
runtime('org.springframework.boot:spring-boot-devtools')
compileOnly "org.springframework.boot:spring-boot-configuration-processor"

// etc
compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("org.jetbrains.kotlin:kotlin-reflect")
compile('com.fasterxml.jackson.module:jackson-module-kotlin')
compile('no.api.freemarker:freemarker-java8:1.2.0')

// mysql
runtime('mysql:mysql-connector-java')

// test
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
}

node {
version = '9.11.2'
npmVersion = '6.4.1'
download = true
}

// Webpackでのビルドを実行するタスク
task webpack(type: NodeTask, dependsOn: 'npmInstall') {
def osName = System.getProperty("os.name").toLowerCase()
if (osName.contains("windows")) {
script = project.file('node_modules/webpack/bin/webpack.js')
} else {
script = project.file('node_modules/.bin/webpack')
}
}

// processResourcesタスクの前に上述のwebpackタスクを実行する
processResources.dependsOn 'webpack'

clean.delete << file('node_modules')

まずはbuild.gradle。kotlin-allopenkotlin-noargkotlin-jpaはkotlinでSpringBoot開発する上で、少し面倒な部分を楽にしてくれる。

あと、webpackと連携させたいのでgradle-node-pluginも入れておく。


WebSecurityConfig.kt

@Configuration

@EnableWebSecurity
class WebSecurityConfig : WebSecurityConfigurerAdapter() {

@Override
@Throws(Exception::class)
override fun configure(web: WebSecurity) {
// これらにマッチするURLへのアクセスは無視する
web.ignoring().antMatchers(
"/images/**",
"/css/**",
"/js/**",
"/webjars/**")
}

@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.csrf().disable()
http.authorizeRequests()
.antMatchers("/").permitAll()
.anyRequest().authenticated()
.and()
.formLogin() // フォームでのログインをする宣言
.loginProcessingUrl("/login") // ログイン処理をするエンドポイント。formのactionでここを指定する
.loginPage("/") // ログインフォームのURL
.defaultSuccessUrl("/home") // ログイン成功後のURL

http.logout()
.logoutRequestMatcher(AntPathRequestMatcher("/logout**")) // マッチするリクエストがきたらログアウトする
.logoutSuccessUrl("/") // ログアウト後のURL
}

@Configuration
class AuthenticationConfiguration : GlobalAuthenticationConfigurerAdapter() {
@Autowired
var userDetailsService: UserDetailsServiceImpl = UserDetailsServiceImpl()

@Throws(Exception::class)
override fun init(auth: AuthenticationManagerBuilder) {
auth.userDetailsService<UserDetailsService>(userDetailsService)
.passwordEncoder(BCryptPasswordEncoder())

}
}
}

次にSpringSecurity用のConfig。WebSecurityConfigurerAdapterconfigureをオーバーライドして設定を追加していく。

あと、GlobalAuthenticationConfigurerAdapterinitをオーバーライドしている部分。AuthenticationManagerBuilderPasswordEncoderを渡してやらないとSpringSecurityが有効にならないので注意。仮にパスワードを平文で保存する場合でもNoOpPasswordEncoderを渡してやらないといけなかったりする。パスワード平文で保存とかほぼやらないと思うけど。

あとSpringSecurityを使う上で、UserDetailsを継承したエンティティクラスとUserDetailsServiceの実装クラスを用意してやる必要がある。


 User.kt

https://github.com/renoinn/springboot-freemarker-vue-sample/blob/master/src/main/kotlin/com/sample/app/domain/entity/User.kt


 UserDetailsServiceImpl.kt

@Service

class UserDetailsServiceImpl : UserDetailsService {
@Autowired
lateinit var userRepository: UserRepository

override fun loadUserByUsername(username: String): UserDetails {
val user = findByUsername(username)
if (!user.isPresent)
throw UsernameNotFoundException("Username $username is not found")
if (!user.get().isAccountNonLocked)
throw UsernameNotFoundException("Username $username is locked")
return user.get().copy(authorities = getAuthorities())
}

private fun getAuthorities() = AuthorityUtils.createAuthorityList("ROLE_USER")

fun findByUsername(username: String): Optional<User> {
val user = userRepository.findByUsername(username)
return user
}
}

UserDetailsエンティティはリンク先を見てもらうとして、UserDetailsServiceImplの方はloadUserByUsernameをオーバーライドしてUserDetailsを返してやらなければならない。この時、返却するUserDetailsauthoritiesの中身の値はROLE_のプレフィックスで始まる文字列じゃないといけないので注意。ROLE_USERとかROLE_ADMINとか。


 webpack.config.js


const webpack = require("webpack");
const ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
entry: './src/main/js/app.js',
output: {
filename: 'js/bundle.js',
path: __dirname + '/build/resources/main/static/'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ExtractTextPlugin.extract({
use: 'css-loader',
fallback: 'vue-style-loader'
})
}
}
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
}
]
},
resolve: {
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
plugins: [
new ExtractTextPlugin("css/styles.css"),
]
}
;

webpackは特に複雑な事はしてないはず。/build/resources/main/static/jsにファイルを置くとhttps://hoge.com/jsでアクセスできるようになるので、outputはそこに設定している。

まだまだ勉強中なので、こうした方が良いみたいなコメントとか、プルリクとか大歓迎です。


 参考URL