TL;DR: 今回の投稿では、Java の世界を蝕んでいる成長中のプログラミング言語、Kotlin で RESTful API を開発する方法を学んでいきます。CRUD 操作を処理する小規模な Spring Boot RESTful API を作成することから始めます。その後、Auth0 でこの API をセキュア にし、それによって 多要素認証、ソーシャルプロバイダーとの統合などたくさんのセキュリティ機能を提供します。最終的には、JWT を自分たちで管理する方法も学び、Auth0 と、独自のトークンを発行する社内ソリューションとを置換します。
Kotlin とは何か
Kotlin は JetBrains が開発したプログラミング言語で、Java 仮想マシン(JVM)で実行し、JavaScript にもコンパイルされます。このプログラミング言語は静的型付けされ、変数、関数、式はコンパイル時間の確認ができるタイプの事前定義された値セットを使用します。
その主要目的のひとつは Java に伴って起きる問題を解決することです。例えば、Java と比較して Kotlin で書かれたソフトウェアはコード行がおよそ 40% 少ないと想定されていますが、Java に使用できる充実したライブラリのセットと相互運用できます。
Kotlin と Java はどのように違うか
まず最初に、構文です。Kotlin の構文は Java のものと若干似ていますが、違いもたくさんあります。JetBrains は、Java 開発者が Kotlin に移動するときになめらかな学習曲線になると説明しています。これは真実ですが、Kotlin 開発者になることや、新しい言語で慣用的コードを書くことはそんなに容易ではありません。
Kotlin の特化について学習すれば、Kotlin は独自のグロッサリがある高度なプログラミング言語であることに気づかれると思います。例えば、Kotlin には データクラス、シールクラス、インライン関数、その他多数の機能があります。これら機能のほとんどは Java でミラー化できますが、かなり詳細なコードを書かずにはできませんから、真に慣用的な Kotlin ソースコードは JetBrains が言うように簡単ではありません。
しかし、ご安心ください。JetBrains は開発者が Java ソースコードを Kotlin に移動できるようなツールを開発しました。Kotlin の Web サイト には Java から変換 というボタンがあります。このボタンは、Java コードを貼り付けして Kotlin のバージョンにすることができます。そのほかに、IntelliJ IDEA にも開発者がこれら変換を実行する機能があります。
Kotlin を学習する
これまで Kotlin を使用したことがなければ、このブログ記事に従ってもいいですが、その前にこの言語について学習しても、特に害になるものではありません。次のリストは Kotlin について学ぶことができるリソースを上げています。
- Kotlin リファレンス:Kotlin の構文が詳しく説明されています
- トライ Kotlin: Kotlin について実践的に学ぶことができます
- Kotlin イン・アクション:この新しい言語を深く掘り下げたい方はご利用ください
Kotlin についてある程度経験がある方、あるいはシンプルな RESTful API を開発することは簡単か疑問に思われている方は続けてお読みください。
Spring Boot Kotlin アプリケーションを始める
Spring Initializr は Spring Boot アプリケーションを開始するにはうってつけの方法です。えり抜かれたプログラミング言語のひとつのオプションとして Kotlin を加えました(本書の執筆時点では、Java、Kotlin、Groovy の 3 つのオプションがありました)。この Web サイトは、ユーザーがアプリケーションのライブラリを容易に選択できるようにしますが、シンプルにするため、本書のために用意した この GitHub レポジトリ を複製することから始め、そこから展開していきます。
git clone https://github.com/auth0-blog/kotlin-spring-boot/ cd kotlin-spring-boot
このスタートアップ プロジェクトにはすでに Spring Data JPA と HSQLDB が搭載されています。これら機能は、API がユーザーによる管理を可能にする 1 セットの顧客を保留します。私たちの仕事は顧客を表す
Customer
Entity Model、永続レイヤーを処理する CustomerRepository
インターフェイス、 RESTful エンドポイントを定義する CustomerController
クラスを作成することです。Kotlin データクラスを作成する
すでに述べましたが、Kotlin の最良の機能のひとつは非常に簡潔化されたプログラム言語だということです。Java 開発者たちが使い慣れている定型句コードのほとんどは getters、setters、equals、hashCode のように簡潔化された構文を支持しています。実際は、 dropped という用語はここでは適切ではありません。equals や hashCode のようなメソッドはコンパイラによって自動的に派生されますが、必要であれば、明確に定義できます。
RESTful API のアイディアはユーザーが顧客セットを管理できるようにすることで、Kotlin データクラス は
Customer
と言います。model
という新しいディレクトリを src/main/kotlin/com/auth0/samples/kotlinspringboot/
ディレクトリに作成してから、 Customer.kt
というファイルに次のソースコードを追加しましょう。package com.auth0.samples.kotlinspringboot.model import javax.persistence.Entity import javax.persistence.Id import javax.persistence.GenerationType import javax.persistence.GeneratedValue @Entity class Customer( @Id @GeneratedValue(strategy = GenerationType.AUTO) var id: Long = 0, var firstName: String = "", var lastName: String = "" )
Java とは異なりますが、クラスの宣言の後に
Customer
クラスの基本的なプロパティを括弧を利用して定義することに、注意してください。 Kotlin では、これをプライマリ コンストラクターと言います。クラスの本文でこれらプロパティを定義したり、他のコンストラクターを定義したりできますが、このケースではこれで十分です。また、@Id
と @GeneratedValue
の注釈を id
プロパティに加えましたので、ご注意ください。この構文は Java の構文と同じです。顧客用レポジトリを作成する
これから作成する
CustomerRepository
インターフェイスは正規の Java Spring Boot アプリケーションですることと非常に似ています。まず、整理するために、persistence
というディレクトリを src/main/kotlin/com/auth0/samples/kotlinspringboot/
ディレクトリに作成しましょう。この新しいディレクトリに、CustomerRepository.kt
というファイルを作成し、次のコードを追加します。package com.auth0.samples.kotlinspringboot.persistence import com.auth0.samples.kotlinspringboot.model.Customer import org.springframework.data.repository.CrudRepository interface CustomerRepository : CrudRepository<Customer, Long>
このインターフェイスにはこのプロジェクトにある HSQLDB インメモリデータベースとインタラクトするために必要なすべてがあります。それで
save
、delete
、findAll
、その他多数 が可能になります。拡張したばかりの CrudRepository
インターフェイスについての詳細情報が必要な場合は、このリソースをご覧ください。顧客 RESTful エンドポイントを定義する
ユーザーの要求を処理する RESTful エンドポイントは Java の対応部分と同様で、もう少し簡潔ですが、Java 開発者にとってはかなり似ているものです。重要なステートメントは変わっていないことに気づかれると思います。かなり詳細ですが、私にとっては良い事ですそのため、依存関係の原因を簡単に特定できます。
クラスを作成するために、
controller
ディレクトリを src/main/kotlin/com/auth0/samples/kotlinspringboot/
ディレクトリに作成することから始めましょう。その後、CustomerController.kt
というファイルを新しいディレクトリに作成し、次のコードを追加します。package com.auth0.samples.kotlinspringboot.controller import com.auth0.samples.kotlinspringboot.model.Customer import com.auth0.samples.kotlinspringboot.persistence.CustomerRepository import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/customers") class CustomerController(val repository: CustomerRepository) { @GetMapping fun findAll() = repository.findAll() @PostMapping fun addCustomer(@RequestBody customer: Customer) = repository.save(customer) @PutMapping("/{id}") fun updateCustomer(@PathVariable id: Long, @RequestBody customer: Customer) { assert(customer.id == id) repository.save(customer) } @DeleteMapping("/{id}") fun removeCustomer(@PathVariable id: Long) = repository.delete(id) @GetMapping("/{id}") fun getById(@PathVariable id: Long) = repository.findOne(id) }
このクラスのソースコードはすぐにわかりますが、完全を期すために、その説明は次のとおりです。
注釈はこのクラスのすべてのエンドポイントには@RequestMapping("/customers")
プレフィクスがあることを宣言しています。/customers
注釈は@GetMapping
への HTTP GET 要求を処理するメソッドとして/customer
を定義します。findAll
注釈は@PostMapping
への HTTP POST 要求を処理するメソッドとして/customers
を定義します。また、このメソッドは顧客の JSON バージョンを受理し、addCustomer
クラスに自動的に逆シリアル化します。Customer
注釈は@PutMapping("/{id}")
への HTTP PUT 要求を処理するメソッドとして/customers
を定義します。このメソッドも要求の本文としてupdateCustomer
を受理します。PUT メソッドと POST メソッドの違いは PUT メソッドは要求パスが顧客のCustomer
を更新することを求めることです。{id}
注釈は@DeleteMapping("/{id}")
への HTTP DELETE 要求を処理するメソッドとして/customers
を定義します。{id} の場合、消去される顧客の ID を定義します。removeCustomer
注釈は@GetMapping("/{id}")
への HTTP GET 要求を処理するメソッドとして/customer/{id}
を定義し、getById
は応答としてシリアル化される顧客を定義します。{id}
それだけです。これで Spring Boot によって支援される最初の Kotlin RESTful API ができました。遊んでみたければ、アプリケーションのルートディレクトリに
mvn spring-boot:run
とタイプすると、Spring Boot が起動します。その後、次のコマンドを使って API とインタラクトします。# adds a new customer curl -H "Content-Type: application/json" -X POST -d '{ "firstName": "Bruno", "lastName": "Krebs" }' http://localhost:8080/customers # retrieves all customers curl http://localhost:8080/customers # updates customer with id 1 curl -H "Content-Type: application/json" -X PUT -d '{ "id": 1, "firstName": "Bruno", "lastName": "Simões Krebs" }' http://localhost:8080/customers/1 # deletes customer with id 1 curl -X DELETE http://localhost:8080/customers/1
何か問題が発生したら、GitHub レポジトリの顧客ブランチ でソースコードを比較してください。
Kotlin RESTful API を Auth0 でセキュアにする
ご覧のように、API を Auth0 でセキュアにすることは非常に簡単で、たくさんの機能を提供します。Auth0 では、シングルサインオン、ユーザー管理、ソーシャル ID プロバイダー(Facebook、GitHub、Twitterなど)のサポート、エンタープライズ(Active Directory、LDAP、SAMLなど)、独自のユーザーデータベース を含む、確かな アイデンティティ管理ソリューション を取得するコード行を数行書かなければなりません。
まだこのようなことをしたことがない初心者には、無料 Auth0 アカウント の登録をお勧めします。Auth0 アカウントをお持ちの方は、まず ダッシュボードに API を作成 してください。API は外部リソースを表すエンティティで、クライアントが要求する保護されているリソースの受理と応答を可能にします。この API は、構築したばかりの Kotlin アプリの正確な機能です。
先進認証を始めるために Auth0 は寛大な無料レベルを提供しています。
API を作成するとき、新しいAPI のフレンドリ名の
Name
、access_token
を要求するときに使用する String
の Identifier
、この API が access_token
を登録するために 対称 または 非対称アルゴリズム を使用する場合に定義する Signing Algorithm
の 3 つの分野を定義しなければなりません。この場合、これらの分野をそれぞれ Kotlin RESTful API
、kotlin-jwts
、RS256
(非対称アルゴリズムを使用します)にします。Auth0 は異なる OAuth 2.0 はアクセストークンを要求するためにフローします をサポートします。この場合、例をシンプルにするために、 API & 信頼するクライアントのフロー を使用します。このフローは実装が最もシンプルですが、クライアントアプリケーションが 絶対的に信頼される 場合 だけ 使用すべきであることに留意してください。ほとんどの場合別のフローが必要で、Auth0 の 「どの OAuth 2.0 フローを使用すべきか?」 の記事はニーズに合った適切なアプローチを選ぶときに役立ちます。
API & 信頼されるクライアント フローを使用するには、まず Auth0 アカウントで
Default Directory
プロパティを構成しなければなりません。そのためには、アカウント設定 ページに移動し、 Default Directory
プロパティの値として Username-Password-Authentication
を追加します。この値は Auth0 アカウントで既定で表示される データベース接続 の名前です。また、クライアント で
Password
付与タイプを有効にする必要もあります。上記で説明したように API を作成したら、Auth0 は Kotlin RESTful API (Test Client)
というクライアントを自動的に作成します。その設定にアクセスして Show Advanced Settings
オプションをクリックし、Grant Types
タブの Password
をチェックし、その変更を保存します。コードを変更する
./src/main/resources
の下に application.properties
というファイルがあります。このファイルを Auth0 アカウントのデータで読み込む必要があります。新規アカウントを作成しているときに既定で、この場合に使用できる「既定のアプリ」が表示されます。これらはその構成で重要な要素なので、その値と次のアプリケーションの値を置換することを忘れないでください。# this is the identifier of the API that we just created auth0.audience=kotlin-jwts # replace YOUR-DOMAIN to get something like https://bkrebs.auth0.com/ auth0.issuer=https://YOUR-DOMAIN.auth0.com/
コードに進む前に、次のように Maven 構成に 3 つの依存関係を追加する必要があります。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>auth0</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>auth0-spring-security-api</artifactId> <version>1.0.0-rc.2</version> </dependency>
これが終わったら、
WebSecurityConfig.kt
というファイルを src/main/kotlin/com/auth0/samples/kotlinspringboot/
ディレクトリに次のソースコードで作成します。package com.auth0.samples.kotlinspringboot.security import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import com.auth0.spring.security.api.JwtWebSecurityConfigurer import org.springframework.beans.factory.annotation.Value @Configuration @EnableWebSecurity open class WebSecurity : WebSecurityConfigurerAdapter() { @Value("\${auth0.audience}") private val audience: String? = null @Value("\${auth0.issuer}") private val issuer: String? = null @Throws(Exception::class) override fun configure(http: HttpSecurity) { http.authorizeRequests() .anyRequest().authenticated() JwtWebSecurityConfigurer .forRS256(audience, issuer!!) .configure(http) } }
それだけです。Auth0 を Kotlin Spring Boot RESTful API で使用するために必要なことはこれだけです。アプリケーションのセキュリティをテストするために、アプリケーションを再度実行しましょう。
mvn spring-boot:run
要求を API に発行するアクセストークンを取得する前に、まず Auth0 の新規ユーザーを作成する必要があります。そのためには、
/dbconnections/signup
エンドポイントに POST
要求を発行する必要があります。この要求は次の JSON 本文の後に Content-Type
ヘッダーと application/json
が必要です。curl -H "Content-Type: application/json" -X POST -d '{ "client_id": "hfs2Au7Zka9XYbXs0CRpdmaL33IKy4mA", "email": "user@test.com", "password": "123123", "connection": "Username-Password-Authentication" }' https://bkrebs.auth0.com/dbconnections/signup # response: # {"_id":"xxx","email_verified":false,"email":"user123@test.com"}
その後、
POST
要求を https://YOUR-DOMAIN.auth0.com/oauth/token
に発行して access_token
を取得します。この要求にも次のように本文と Content-Type
ヘッダーに JSON オブジェクトを含む必要があります。curl -H "Content-Type: application/json" -X POST -d '{ "grant_type":"password", "username": "user@test.com", "password": "123123", "audience": "kotlin-jwts", "client_id": "hfs2Au7Zka9XYbXs0CRpdmaL33IKy4mA", "client_secret": "Hx4eFNAT8TI2TUVDXhxWDJ8vWpZxt79DQYUl7e178Uw0ASfc7eY42zPf2H-Gv1n1" }' https://bkrebs.auth0.com/oauth/token # response: # {"access_token":"xxx.yyy.zzz","expires_in":86400,"token_type":"Bearer"}
両方の要求の
client_id
および client_secret
プロパティは状況に応じて 変更されなければならない ことに注意してください。それらの値は Auth0 が作成した Kotlin RESTful API (Test Client) クライアントで見つけられます。それらの値を取得するには クライアントページ に移動してください。この最後の要求を発行すると、
access_token
を取得できます。これからは、Kotlin API に送信する要求のヘッダーでこのトークンを使用しますので、この access_token
でエンドポイントをクエリすると、顧客のセットを再度管理できるようになります。# no token = no access: curl http://localhost:8080/customers # token = access curl -H "Authorization: Bearer xxx.yyy.zzz" http://localhost:8080/customers
“Kotlin RESTful API を Auth0 で簡単にセキュアにする”
これをツイートする
Kotlin を独自のソリューションでセキュアにする
何らかの理由で Auth0 でセキュアになる RESTful API を希望しない場合は、本章で説明するステップに従ってください。まず、
pom.xml
から Auth0 の依存関係を削除します。<dependency> <groupId>com.auth0</groupId> <artifactId>auth0</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>auth0-spring-security-api</artifactId> <version>1.0.0-rc.2</version> </dependency>
その後に、
application.properties
ファイルに追加した 2 つのプロパティは使用しませんので削除します。それから、JWT を発行・検証するには、次の Maven 依存関係を追加します。<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
ユーザーを処理する
API の複数のユーザーをサポートするには、まず
ApplicationUser.kt
、ApplicationUserRepository.kt
、SignUpController.kt
の 3 つのクラスを作成します。これらクラスは顧客の管理をサポートするほとんどクラスのように動作します。最初のクラス、ApplicationUser.kt
クラスは model
パッケージに作成され、次のコードを含みます。package com.auth0.samples.kotlinspringboot.model import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id @Entity class ApplicationUser( @Id @GeneratedValue(strategy = GenerationType.AUTO) var id: Long = 0, var username: String = "", var password: String = "" )
新しいものは何もありません。ユーザーのプロパティを保留する別のデータクラスだけです。その後、
ApplicationUserRepository.kt
クラスを persistence
パッケージに次のコードで作成します。package com.auth0.samples.kotlinspringboot.persistence import com.auth0.samples.kotlinspringboot.model.ApplicationUser import org.springframework.data.repository.CrudRepository interface ApplicationUserRepository : CrudRepository<ApplicationUser, Long> { fun findByUsername(username: String): ApplicationUser? }
この場合の
CustomerRepository
と比較した場合の唯一の違いは findByUsername
というメソッドを定義したことです。このメソッドは、ユーザーがユーザー名を見つけるために独自のソリューションで使用されます。ここで、最後のクラス SignUpController.kt
は次のコードで controller
パッケージに作成します。package com.auth0.samples.kotlinspringboot.controller import com.auth0.samples.kotlinspringboot.model.ApplicationUser import com.auth0.samples.kotlinspringboot.persistence.ApplicationUserRepository import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/sign-up") class SignUpController(val applicationUserRepository: ApplicationUserRepository, val bCryptPasswordEncoder: BCryptPasswordEncoder) { @PostMapping fun signUp(@RequestBody applicationUser: ApplicationUser) { applicationUser.password = bCryptPasswordEncoder.encode(applicationUser.password) applicationUserRepository.save(applicationUser) } }
このコントローラーで定義された唯一のエンドポイントは
signUp
で、新規ユーザーによるアプリケーションの登録を可能にします。サインイン プロセスとトークンの検証は後ほど説明しますが、別の領域で処理されます。最終的なデータ漏洩でさえもユーザーのパスワードをセキュアにするには、Spring Security に付いてくる BCryptPasswordEncoder
クラスを使用してすべてのパスワードをエンコードすることにご留意ください。JWT を Kotlin で発行・検証する
これで
User
データクラスをマップし、エンドポイントによって新規ユーザーが自分で登録できるので、API とインタラクトできるようにする前にこれらユーザーがサインインし、JWT を検証する必要があります。これを実現するには、JWTAuthenticationFilter
、JWTAuthorizationFilter
、UserDetailsServiceImpl
の 2 つのフィルタと 1 つのクラスを作成します。サインイン機能を担当する最初のフィルタは JWTAuthenticationFilter.kt
と呼ばれる新規ディレクトリファイルに WebSecurity
クラスとして同じパッケージに作成されます。このファイルには次のソースコードがあります。package com.auth0.samples.kotlinspringboot import com.auth0.samples.kotlinspringboot.model.ApplicationUser import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication import org.springframework.security.core.AuthenticationException import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.userdetails.User import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import java.io.IOException import java.util.Date import javax.servlet.FilterChain import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse class JWTAuthenticationFilter(authManager: AuthenticationManager) : UsernamePasswordAuthenticationFilter() { init { authenticationManager = authManager } @Throws(AuthenticationException::class, IOException::class, ServletException::class) override fun attemptAuthentication( req: HttpServletRequest, res: HttpServletResponse): Authentication { val creds = ObjectMapper() .readValue(req.inputStream, ApplicationUser::class.java) return authenticationManager.authenticate( UsernamePasswordAuthenticationToken( creds.username, creds.password, emptyList<GrantedAuthority>() ) ) } @Throws(IOException::class, ServletException::class) override fun successfulAuthentication( req: HttpServletRequest, res: HttpServletResponse, chain: FilterChain?, auth: Authentication) { val JWT = Jwts.builder() .setSubject((auth.principal as User).username) .setExpiration(Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET) .compact() res.addHeader(HEADER_STRING, TOKEN_PREFIX + " " + JWT) } }
このフィルタは、
- ユーザーから資格情報を解析し、それらを認証する
、attemptAuthentication
- ユーザーの認証が成功したときに JWT を作成する
の 2 つの機能を定義します。successfulAuthentication
これら両方のフィルタは
SECRET
や EXPIRATION_TIME
のように一部未定義の定数を使用することにご留意ください。これら定数を定義するには、次のコードで SecurityConstants.kt
というファイルを同じディレクトリに作成します。package com.auth0.samples.kotlinspringboot val SIGN_UP_URL = "/sign-up" val SECRET = "SecretKeyToGenJWTs" val TOKEN_PREFIX = "Bearer " val HEADER_STRING = "Authorization" val EXPIRATION_TIME: Long = 864_000_000 // 10 days
上記のフィルタが作成するトークンを検証するには、2つめのフィルタ
JWTAuthorizationFilter
が必要です。このフィルタは次のコードで同じディレクトリ内に作成されます。package com.auth0.samples.kotlinspringboot import io.jsonwebtoken.Jwts import org.springframework.security.authentication.AuthenticationManager import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.context.SecurityContextHolder import org.springframework.security.web.authentication.www.BasicAuthenticationFilter import java.io.IOException import java.util.Collections.emptyList import javax.servlet.FilterChain import javax.servlet.ServletException import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse class JWTAuthorizationFilter(authManager: AuthenticationManager) : BasicAuthenticationFilter(authManager) { @Throws(IOException::class, ServletException::class) override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) { val header = request.getHeader(HEADER_STRING) if (header == null || !header.startsWith(TOKEN_PREFIX)) { chain.doFilter(request, response) return } val authentication = getAuthentication(request) SecurityContextHolder.getContext().authentication = authentication chain.doFilter(request, response) } fun getAuthentication(request: HttpServletRequest): Authentication? { val token = request.getHeader(HEADER_STRING) if (token != null) { // parse the token. val user = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token.replace(TOKEN_PREFIX, "")) .getBody() .getSubject() return if (user != null) UsernamePasswordAuthenticationToken(user, null, emptyList<GrantedAuthority>()) else null } return null } }
このフィルタはセキュリティ保護されたエンドポイントが要求されたときに使用され、
Authorization
ヘッダーにトークンがあればチェックによって開始します。何とかトークンが見つかれば、その検証が行われ、そのユーザーを SecurityContext
に設定します。トークンが見つからなければ、その要求を Spring Security フィルタチェーンに応じて移動させ、それからこの要求は 401(承認されていません)状態コードの応答を受けます。作成する必要がある最後のクラスは
UserDetailsServiceImpl
です。このクラスは Spring Security から UserDetailsService
クラスを拡張し、データベースでユーザーを見つける担当なので、Spring Security がその資格情報をチェックできます。このクラスはメイン kotlinspringboot
ディレクトリに作成され、次のソースコードを格納します。package com.auth0.samples.kotlinspringboot import com.auth0.samples.kotlinspringboot.model.ApplicationUser import com.auth0.samples.kotlinspringboot.persistence.ApplicationUserRepository import org.springframework.security.core.userdetails.User 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.Service import org.springframework.transaction.annotation.Transactional @Service open class UserDetailsServiceImpl(val userRepository: ApplicationUserRepository) : UserDetailsService { @Transactional(readOnly = true) @Throws(UsernameNotFoundException::class) override fun loadUserByUsername(username: String): UserDetails { val user = userRepository.findByUsername(username) ?: throw UsernameNotFoundException(username) return User(user.username, user.password, emptyList()) } fun save(user: ApplicationUser) { userRepository.save(user) } }
この独自のソリューションをまとめるために、
WebSecurity
クラスのコンテンツと次を置換します。package com.auth0.samples.kotlinspringboot import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpMethod import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder @Configuration @EnableWebSecurity open class WebSecurity(val userDetailsService: UserDetailsService) : WebSecurityConfigurerAdapter() { @Bean fun bCryptPasswordEncoder(): BCryptPasswordEncoder { return BCryptPasswordEncoder() } override fun configure(http: HttpSecurity) { http.csrf().disable().authorizeRequests() .antMatchers(HttpMethod.POST, SIGN_UP_URL).permitAll() .anyRequest().authenticated() .and() .addFilter(JWTAuthenticationFilter(authenticationManager())) .addFilter(JWTAuthorizationFilter(authenticationManager())) } override fun configure(auth: AuthenticationManagerBuilder?) { auth!!.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()) } }
これらを変更したら、再度 API とインタラクトできるようになり、次のように JWT を適切に作成・検証しているかをチェックします。
# run Kotlin app again mvn spring-boot:run # register a new user curl -H "Content-Type: application/json" -X POST -d '{ "username": "admin", "password": "password" }' http://localhost:8080/sign-up # login to get the JWT (in the Authorization header) curl -i -H "Content-Type: application/json" -X POST -d '{ "username": "admin", "password": "password" }' http://localhost:8080/login # get customers passing the JWT contained by the Authorization header curl -H "Authorization: Bearer xxx.yyy.zzz" http://localhost:8080/customers
ご覧のように、JWT で独自のセキュリティソリューションを作成することはそんなに難しくはありません。ただし、Auth0 で統合するために実行した作業よりもずっと多くの作業を要します。多要素認証、ソーシャル ID プロバイダー、エンタープライズ接続(Active Directory、LDAP、SAMLなど)のようなさらに高度なトピックには対処しませんでした。このようなケースを処理するにはさらに多くの作業が必要になります。これら機能をなんとか素早く実現したとしても、Auth0 を使ったときと同じようなセキュリティ対策を講じることはできません。
まとめ
Java 開発者にとって Kotlin でコードを書くことは、注意すべき危険がそんなにないので、そんなに難しいことではありません。しかし、言語のフルパワーと最高の機能を使って真の Kotlin 開発者になるには容易ではなく、数時間の学習と開発が必要です。Kotlin と既存の Java ライブラリとの統合は Spring Boot を使用できるので非常に良いことで、開発したコードは非常に完結で読みやすいものでした。Kotlin の機能についてあまり取り扱いませんでしたが、その適用性を最後に検証しました。
“バックエンドの Kotlin アプリケーションを開発することは全く問題がなく簡単です。”
これをツイートする
いかがですか?Kotlin を支持して Java を断念しませんか?
About the author
Bruno Krebs
R&D Content Architect