Help us understand the problem. What is going on with this article?

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

renoinn
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away