반응형

build.gradle.kts

dependencies {
    // ...
    implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
    implementation("io.jsonwebtoken:jjwt:0.9.1")
    implementation("javax.xml.bind:jaxb-api:2.3.1")
}

UserToken

data class UserToken(
    val id: String,
    val name: String,
    val expireAt: LocalDateTime,
)

IssueToken

class IssueToken {
    data class Request(
        val id: String,
        val name: String,
    )

    data class Response(
        val accessToken: String,
        val tokenType: String,
        val expireAt: LocalDateTime,
    )
}

UserTokenService

@Service
class UserTokenService {
    private val log = LoggerFactory.getLogger(this::class.java)

    fun createToken(userToken: UserToken): String {
        return Jwts.builder()
            .setSubject("user")
            .claim("id", userToken.id)
            .claim("name", userToken.name)
            .setExpiration(userToken.expireAt.toDate())
            .signWith(SignatureAlgorithm.HS512, SIGNING_KEY.toByteArray())
            .compact()
    }

    fun parseToken(token: String): UserToken? {
        return try {
            val claims = Jwts.parser()
                .setSigningKey(SIGNING_KEY.toByteArray())
                .parseClaimsJws(token)
                .body

            UserToken(
                id = claims.get("id", String::class.java),
                name = claims.get("name", String::class.java),
                expireAt = claims.expiration.toLocalDateTime(),
            )
        } catch (e: Exception) {
            log.error("Jwt Parsing Error", e)
            null
        }
    }

    private fun LocalDateTime.toDate(): Date {
        return Date.from(this.atZone(ZoneId.systemDefault()).toInstant())
    }

    private fun Date.toLocalDateTime(): LocalDateTime {
        return this.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime()
    }

    companion object {
        private const val SIGNING_KEY = "test-signing-key"
    }
}

UserTokenAuthenticationFilter

@Component
class UserTokenAuthenticationFilter(
    private val userTokenService: UserTokenService,
) : OncePerRequestFilter() {
    private val bearerTokenResolver: BearerTokenResolver = DefaultBearerTokenResolver()

    override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
        val accessToken = bearerTokenResolver.resolve(request)

        if (accessToken != null) {
            val userToken = userTokenService.parseToken(accessToken)

            val authentication = object : AbstractAuthenticationToken(listOf()) {
                override fun getPrincipal() = userToken
                override fun getCredentials() = accessToken
                override fun isAuthenticated() = userToken != null
            }

            SecurityContextHolder.getContext().authentication = authentication
        }

        filterChain.doFilter(request, response)
    }
}

SecurityConfig

@Configuration
@EnableWebSecurity(debug = true)
class SecurityConfig {
    @Bean
    fun securityFilterChain(
        http: HttpSecurity,
        userTokenAuthenticationFilter: UserTokenAuthenticationFilter,
    ): SecurityFilterChain {
        return http
            .authorizeHttpRequests { authorize ->
                authorize.requestMatchers("/api/v*/auth/token").permitAll()
                authorize.anyRequest().authenticated()
            }
            .addFilterAfter(userTokenAuthenticationFilter, LogoutFilter::class.java)
            .csrf { it.disable() }
            .build()
    }
}

AuthController

@RestController
class AuthController(
    private val userTokenService: UserTokenService,
) {
    @PostMapping("/api/v1/auth/token")
    fun issueToken(@RequestBody request: IssueToken.Request): IssueToken.Response {
        val userToken = UserToken(request.id, request.name, LocalDateTime.now().plusMinutes(10))
        val token = userTokenService.createToken(userToken)
        return IssueToken.Response(
            accessToken = token,
            tokenType = "Bearer",
            expireAt = userToken.expireAt,
        )
    }

    @GetMapping("/api/v1/auth/user")
    fun getUser(@AuthenticationPrincipal userToken: UserToken): UserToken {
        return userToken
    }
}

토큰 발급

% curl -X POST "http://localhost:8080/api/v1/auth/token" \
    -d '{"id": "123", "name": "tyler"}' \
    -H 'Content-Type: application/json'
{"accessToken":"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwiaWQiOiIxMjMiLCJuYW1lIjoidHlsZXIiLCJleHAiOjE3MzM0MTU3NzN9.mxJEtzkCvslMJ2KyG5yfyfGPIS5aUTapuunHqvzmZ4-lu3vKAm5PjtYLzvxP3Yb3jbLadfNpc6Axa1Z0ff4ysg","tokenType":"Bearer","expireAt":"2024-12-06T01:22:53.506523"}

리소스 요청

% curl -X GET "http://localhost:8080/api/v1/auth/user" \
    -H 'Content-Type: application/json' \
    -H 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwiaWQiOiIxMjMiLCJuYW1lIjoidHlsZXIiLCJleHAiOjE3MzM0MTU3NzN9.mxJEtzkCvslMJ2KyG5yfyfGPIS5aUTapuunHqvzmZ4-lu3vKAm5PjtYLzvxP3Yb3jbLadfNpc6Axa1Z0ff4ysg'
{"id":"123","name":"tyler","expireAt":"2024-12-06T01:22:53"}
반응형

+ Recent posts