3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Spring SecurityにJWT(Json Web Token)を統合して、ユーザーのログインと認証を実現する

Posted at

0. 背景

Json Web Token(JWT)は、Jsonに基づく公開仕様[RFC 7519](https://tools.ietf.org/html/rfc7519)です。
通常はクライアントとサーバーの間に、安全且つ確実にユーザー識別情報を転送するために使用されます。
それに、Token情報には、ユーザー識別情報以外、業務データも転送することができます。

JWTの利点は以下の通りです。

  1. サーバー側でユーザー情報を保存する必要はありません。
    サーバーが分散してデプロイされている場合、
    sessionのようにユーザー情報を各サーバーに同期する必要はなく、
    各サーバーはJWT Tokenを解析することでユーザー情報を取得できます。
  2. クロスドメインアクセスをサポートします。
  3. クライアントはCookieをサポートしていなくても使用できます。
    クライアントがさまざまなデバイスやアプリケーションのアクセスをサポートする必要がある場合は、JWT Token認証を使用した方は互換性が良いです。

1. 依存ライブラリを追加する

implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")

2. 実現方法

2.1 JwtTokenProviderクラスをカプセル化して、JWT Tokenの生成と復号化を実現する

package com.example.demo.utils

import io.jsonwebtoken.JwtBuilder
import io.jsonwebtoken.JwtParser
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.userdetails.User
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*

class JwtTokenProvider {
    private val jwtParser: JwtParser
    private val jwtBuilder: JwtBuilder
    init {
        val base64SecretKey = "secretkeysecretkeysecretkeysecretkeysecretkeysecre" +
                "tkeysecretkeysecretkeysecretkeysecretkeysecretkeysecretkey"
        val keyBytes = Base64.getDecoder().decode(base64SecretKey)
        val key = Keys.hmacShaKeyFor(keyBytes)
        jwtParser = Jwts.parserBuilder()
            .setSigningKey(key)
            .build()
        jwtBuilder = Jwts.builder()
            .signWith(key, SignatureAlgorithm.HS256)
    }

    /**
     * JWT Tokenの生成
     * @param authentication ユーザー情報
     * @param expiredAt token有効期限
     * @return token文字列
     */
    fun createToken(authentication: Authentication, expiredAt: LocalDateTime): String {
        val zoneId = ZoneId.systemDefault()
        val zdt = expiredAt.atZone(zoneId)
        val expiredDateAt = Date.from(zdt.toInstant())
        return jwtBuilder
            .claim("username", authentication.name)
            .setSubject(authentication.name)
            .setExpiration(expiredDateAt)
            .compact()
    }
    /**
     * tokenにより、ユーザー情報を解析する
     * @param token ユーザーがリクエストする時に、Headerに持っているtoken
     * @return ユーザー認証情報
     */
    fun getAuthentication(token: String): Authentication {
        val claims = jwtParser.parseClaimsJws(token).body
        val principal = User(claims.subject, "", listOf())
        return UsernamePasswordAuthenticationToken(principal, token, listOf())
    }
}

2.2 JwtToken解析用のinterceptorを提供する

package com.example.demo.interceptor

import com.example.demo.utils.JwtTokenProvider
import org.slf4j.LoggerFactory
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest

class JwtTokenFilter(private val jwtTokenProvider: JwtTokenProvider) : GenericFilterBean() {
    private val log = LoggerFactory.getLogger(javaClass)
    override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
        val servletRequest = request as HttpServletRequest
        val token = resolveToken(servletRequest)
        if (token == null) {
            chain.doFilter(request, response)
            return
        }
        try {
            SecurityContextHolder.getContext().authentication =
                jwtTokenProvider.getAuthentication(token)
        } catch (ex: Exception) {
            if (log.isWarnEnabled) {
                log.warn("parse jwt token error, token: $token", ex)
            }
        }
        chain.doFilter(request, response)
    }

    private fun resolveToken(request: HttpServletRequest): String? {
        return request.getHeader("Authorization")?.let {
            if (it.startsWith("Bearer ")) {
                it.removePrefix("Bearer ")
            } else {
                null
            }
        }
    }

}

2.3 Spring Securityを配置する

UsernamePasswordAuthenticationFilterの前にJwtTokenFilterを追加し、認証を必要としないように/loginを設定する。

package com.example.demo.configuration

import com.example.demo.interceptor.JwtTokenFilter
import com.example.demo.utils.JwtTokenProvider
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.WebSecurityConfigurerAdapter
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http.csrf().disable()
            .addFilterBefore(JwtTokenFilter(tokenProvider()), UsernamePasswordAuthenticationFilter::class.java)
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/login").permitAll()
            .anyRequest().authenticated()
    }

    @Bean
    fun tokenProvider() = JwtTokenProvider()
}

2.4 二つのインターフェイスを作成する:一つはログインインタフェースであり、一つは認証してからアクセスできるインターフェースである

package com.example.demo.controller

import com.example.demo.service.dto.TestQueryDto
import com.example.demo.utils.JwtTokenProvider
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.time.LocalDateTime

@RestController
class TestController(private val tokenProvider: JwtTokenProvider) {
    @GetMapping("/login")
    fun get(dto: TestQueryDto): Map<String, Any> {
        val authentication = UsernamePasswordAuthenticationToken("user01", "***")
        val token = tokenProvider.createToken(authentication, LocalDateTime.now().plusDays(100))
        return mapOf("name" to "user01", "token" to token)
    }

    @GetMapping("/hello")
    fun getUser(): String {
        val authentication = SecurityContextHolder.getContext().authentication
        return authentication.name + " hello!"
    }
}

3. 評価

  1. client側はtokenなしでアクセスする時、403エラーが表示される。

    curl "http://localhost:8080/hello"
    # {"timestamp":"2021-04-01T05:22:28.167+00:00","status":403,"error":"Forbidden","message":"","path":"/hello"}
    
  2. 先に loginを呼び出してログインし、その後loginによって返された tokenをHeaderに入れ、再び/helloを呼び出すと、データは正常に返される。

    curl "http://localhost:8080/login"
    # {"name":"user01","token":"eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIwMSIsInN1YiI6InVzZXIwMSIsImV4cCI6MTYyNTg5NDcwNH0.yxdtwaXJ9i7F-JZeFWijWLR0CAFTDUkrCqcfmv65zWs"}
    
    curl "http://localhost:8080/hello" -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InVzZXIwMSIsInN1YiI6InVzZXIwMSIsImV4cCI6MTYyNTg5NDcwNH0.yxdtwaXJ9i7F-JZeFWijWLR0CAFTDUkrCqcfmv65zWs"
    # user01 hello!
    
3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?