😂

spring-security v6+KotlinでHelloWorld

2023/01/16に公開

はじめに

本記事ではspring-securityを使ったハンズオンを行います

spring-securityは2022年12月20日にversion 6.0へと変わり、いままでの設定ファイルと大きく変わることになりました。

筆者はKotlinの学習をはじめた2023年1月、ネットにほとんど情報がない中で試行錯誤したので、今回は簡単なチュートリアルという形でv6の設定ファイルについて紹介していきます

spring-securityの学習

spring-securityがそもそもなんなのか、どのように使えるのかなどは設定ファイルが古くはなりますが以下のサイトが参考になります

https://qiita.com/ryotaro76/items/6c8405fada7a6fead7cc

https://qiita.com/rubytomato@github/items/6c6318c948398fa62275

また学習を進める上で以下の書籍は大変参考になりました。まずはこの書籍で概要を掴んでから実装を勧めてみるのがおすすめです

https://www.amazon.co.jp/Kotlin-サーバーサイドプログラミ�%3�グ実践開発-竹端-尚人/dp/4297118599/ref=sr_1_3?__mk_ja_JP=カタカナ&crid=2CDTD7B0SPEVW&keywords=kotlin&qid=1673854296&sprefix=kotlin%2Caps%2C192&sr=8-3

開発環境

  • IntelliJ IDEA Community Edition
  • open jdk version 18

プロジェクト作成

まずはSpringBootのプロジェクト作成です

こちらのサイトにアクセスして以下の設定をします

項目
Project Gradle-Kotlin
Language Kotlin
Artifact security
Name security

そして依存関係を追加します
「ADD DEPENDENCIES」をクリックして、「Spring Web」、「Spring Security」を追加します

「GENERATOR」をクリックしてダウンロードをします
適当な場所に解凍して、IntelliJでフォルダを開きます

APIの準備

今回は2つのAPIを用意します

まずは/src/main/kotlin/com/example/securityToDoController.ktを作成して以下にします

ToDoController.kt
package com.example.security

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class ToDoController() {
    @GetMapping("/hello")
    fun hello(): String {
        return "Hello"
    }

    @GetMapping("/todo")
    fun todo(): String {
        return "todo"
    }
}

注意としては今回はAPIなので@RestControllerでアノテーションしているところです
@Controllerにすると思わぬエラーが出ますので注意しましょう

https://qiita.com/Sicut_study/items/0dc4e0fdac28e54f776f

今回はAPIを以下のように使い分けます

/hello : 認証関係なく誰でもアクセスできる
/todo : ログインしたユーザーのみアクセスできる

spring-securityの設定

次にspring-securityの設定を行います
現在以下のコマンドを叩くと401エラーが発生します

$ curl localhost:8080/hello

これはspring-securityを依存関係に追加しているのでデフォルトですべてのパスはログイン認証をしていないとアクセスできないようになっているからです

まずは/helloのみログイン認証なしでアクセスできるように設定していきましょう

/helloを認証無しでアクセスする設定

/src/main/kotlin/com/example/securitySecurityConfig.ktを作成して以下にします

ここに設定を書いていきます

SecurityConfig.kt
package com.example.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain

@Configuration
@EnableWebSecurity
class SecurityConfig {
    @Bean
    fun configureHttpSecurity(httpSecurity: HttpSecurity): SecurityFilterChain {
        httpSecurity
            .authorizeHttpRequests()
            .requestMatchers("/hello", "/login").permitAll()
            .anyRequest().authenticated()
        return httpSecurity.build()
    }
}

APIを再起動してアクセスしてみます

$ curl localhost:8080/hello

するとHelloと返ってきます

簡単にそれぞれについて解説していきます

@Configuration
@EnableWebSecurity
class SecurityConfig {
    @Bean
    fun configureHttpSecurity(httpSecurity: HttpSecurity): SecurityFilterChain {
		ここに認証に関する設定をかく
    }
}

ここは雛形です。configureHttpSecurityの中にどのアクセスが認証が必要かを設定できます
以前はWebSecurityConfigAdapterを使った実装でしたが変わっています

            .authorizeHttpRequests()
            .requestMatchers("/hello", "/login").permitAll()
            .anyRequest().authenticated()

ここでは/hello/loginのアクセスに対してpermitAll()をすることで認証なしにアクセスできるようにしました

/loginは先ほど作成していませんが、これはspring-securityがログイン用に裏で用意してくれているAPIです

そしてanyRequest().authenticated()でそれ以外のパスのアクセスは認証が必要と設定しました

ためしにcurl http://localhost:8080/todoにアクセスすると401エラーが返ってきます

$ curl -i http://localhost:8080/todo # 401エラー

/todoにアクセスする

では認証を行えるように設定を追加して、/todoにアクセスできるようにします

SecurityConfig.kt
package com.example.security

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.provisioning.InMemoryUserDetailsManager

@Configuration
@EnableWebSecurity
class SecurityConfig {
    @Bean
    fun configureHttpSecurity(httpSecurity: HttpSecurity): SecurityFilterChain {
        httpSecurity
            .authorizeHttpRequests()
            .requestMatchers("/hello", "/login").permitAll()
            .anyRequest().authenticated()
            .and()
            .csrf().disable()
            .formLogin()
            .loginProcessingUrl("/login")
            .usernameParameter("email")
            .passwordParameter("password")
        return httpSecurity.build()
    }

    @Bean
    fun users(): UserDetailsService {
        val user = User.builder()
            .username("user")
            .password(passwordEncoder()?.encode("password"))
            .roles("USER")
            .build()
        return InMemoryUserDetailsManager(user)
    }

    @Bean
    fun passwordEncoder(): PasswordEncoder? {
        return BCryptPasswordEncoder()
    }
}

追加した箇所を説明します

            .csrf().disable()
            .formLogin()
            .loginProcessingUrl("/login")
            .usernameParameter("email")
            .passwordParameter("password")

まずcsrf().disable()でCSRFトークンによる証明を無効にしています
そして、loginProcessingUrlでログインフォームを有効化して、ユーザーのパラメータはemail、パスワードのパラメータはpasswordで受け付けるように設定しました

POSTリクエストでemailpasswordの2つを送り、それらを用いてログイン認証を行うことになります

    @Bean
    fun users(): UserDetailsService {
		# 認証設定を書く
    }

ここで認証の具体的な内容を設定します

https://spring.pleiades.io/spring-security/reference/servlet/authentication/passwords/user-details-service.html#servlet-authentication-userdetailsservice

ここも昔であればconfigureとして実装していたので変わった箇所になります

        val user = User.builder()
            .username("user")
            .password(passwordEncoder()?.encode("password"))
            .roles("USER")
            .build()

ここでは認証ユーザーを作成しています
ユーザー名: user
パスワード: password
というログイン認証があった場合にUSERというロールを付与してログインできるようにしています

このように事前にログインできるアカウントを定義しておくのをインメモリ認証とよびます

https://spring.pleiades.io/spring-security/reference/servlet/authentication/passwords/in-memory.html

    @Bean
    fun passwordEncoder(): PasswordEncoder? {
        return BCryptPasswordEncoder()
    }

APIに渡されたパスワードは自動的に暗号化されるため、比較するパスワードも暗号化しておく必要があるのでこの関数を用意しました

では、実際に認証して/todoにアクセスできるか確かめます

$ curl -i -c cookie.txt -H 'Content-Type:application/x-www-form-urlencoded' -X POST -d 'email=user' -d 'password=password' http://localhost:8080/login

このコマンドでまず/loginで認証を行い、認証情報をcookie.txtとしてファイルに保存しています

$ cat cookie.txt
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost	FALSE	/	FALSE	0	JSESSIONID	BF71008CD281961A6463394AA8422306

このように情報が乗っています
次にこのファイルを利用して/todoにアクセスします

$ curl -b cookie.txt http://localhost:8080/todo

するとtodoというレスポンスが返ってきて認証してアクセスできるようになったことが確認できました

おわりに

私自身KotlinもSpringBootも利用したことがなかったのでここまでやるのにもかなり苦戦しました

このようなハンズオンがあればいいなと思っていろいろ情報を集めていましたが、体系的なものが見つからなかったので作成してみました

参考

GitHubで編集を提案

Discussion