개요
OAuth2 기반의 인증 서비스, 각종 인증수단을 중앙에서 관리함으로써 각 연결되는 서비스들이 관리 포인트를 줄일 수 있게 만드는 것이 목적이다
OAuth2
OAuth는 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹사이트 상의 자신들의 정보에 대해 웹사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는, 접근 위임을 위한 개방형 표준이다. 이 매커니즘은 여러 기업들에 의해 사용되는데, 이를테면 아마존, 구글, 페이스북, 마이크로소프트, 트위터가 있으며 사용자들이 타사 애플리케이션이나 웹사이트의 계정에 관한 정보를 공유할 수 있게 허용한다 - Wikipedia
JWT
JSON 웹 토큰(JSON Web Token, JWT)은 선택적 서명 및 선택적 암호화를 사용하여 데이터를 만들기 위한 인터넷 표준으로, 페이로드는 몇몇 클레임(claim) 표명(assert)을 처리하는 JSON을 보관하고 있다. 토큰은 비공개 시크릿 키 또는 공개/비공개 키를 사용하여 서명된다. 이를테면 서버는 "관리자로 로그인됨"이라는 클레임이 있는 토큰을 생성하여 이를 클라이언트에 제공할 수 있다. 그러면 클라이언트는 해당 토큰을 사용하여 관리자로 로그인됨을 증명한다. 이 토큰들은 한쪽 당사자의 비공개 키(일반적으로 서버의 비공개 키)에 의해 서명이 가능하며 이로써 해당 당사자는 최종적으로 토큰이 적법한지를 확인할 수 있다. 일부 적절하고 신뢰할만한 수단을 통해 다른 당사자가 상응하는 공개키를 소유하는 경우 이 경우 또한 토큰의 적법성 확인이 가능하다. 토큰은 크기가 작고 URL 안전으로 설계되어 있으며 특히 웹 브라우저 통합 인증(SSO) 컨텍스트에 유용하다. JWT 클레임은 아이덴티티 제공자와 서비스 제공자 간(또는 비즈니스 프로세스에 필요한 클레임)의 인가된 사용자의 아이덴티티를 전달하기 위해 보통 사용할 수 있다. - Wikipedia
설정
정보
설정 간 필요한 URI 목록은 아래와 같다
{
"issuer" : "http://auth.stable.codes",
"authorization_endpoint" : "http://auth.stable.codes/oauth2/authorize",
"pushed_authorization_request_endpoint" : "http://auth.stable.codes/oauth2/par",
"device_authorization_endpoint" : "http://auth.stable.codes/oauth2/device_authorization",
"token_endpoint" : "http://auth.stable.codes/oauth2/token",
"token_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth" ],
"jwks_uri" : "http://auth.stable.codes/oauth2/jwks",
"response_types_supported" : [ "code" ],
"grant_types_supported" : [ "authorization_code", "client_credentials", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange" ],
"revocation_endpoint" : "http://auth.stable.codes/oauth2/revoke",
"revocation_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth" ],
"introspection_endpoint" : "http://auth.stable.codes/oauth2/introspect",
"introspection_endpoint_auth_methods_supported" : [ "client_secret_basic", "client_secret_post", "client_secret_jwt", "private_key_jwt", "tls_client_auth", "self_signed_tls_client_auth" ],
"code_challenge_methods_supported" : [ "S256" ],
"tls_client_certificate_bound_access_tokens" : true,
"dpop_signing_alg_values_supported" : [ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" ]
}
서버
Spring Security 와 OAuth2 의존성을 추가한다
dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server")
}
OAuth2 관련 서버 설정 값을 추가한다
# 12. Security Properties spring.security.oauth2.resourceserver.jwt.jwk-set-uri=[위 설정 정보 중 jwks_uri]
Spring Security 구성
OAuth2 와 Spring Security 를 연동하기 위한 설정이 필요하다.
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
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
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.authorizeRequests().anyRequest().permitAll() // 일단, 모든 경로에 대해 인증을 풀어버린다
http.oauth2ResourceServer().jwt() // OAuth2(+Jwt) 활성화
}
}
연동 예제
위 설정이 마무리되면 발급 된 토큰을 이용하여 아래의 예제과 같이 연동이 가능하다. 위 설정과 같은 Global Method 인증 방식으로 설명했다
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/test")
class TestController(
private val service: TestService
) {
@GetMapping("/info")
@PreAuthorize("isAuthenticated()")
fun info(
@AuthenticationPrincipal jwt: Jwt
) = service.getVerySensitiveData(
jwt
)
}
import org.slf4j.LoggerFactory
import org.springframework.security.oauth2.jwt.Jwt
import org.springframework.stereotype.Service
@Service
class TestService {
private val logger = LoggerFactory.getLogger(javaClass)
fun getVerySensitiveData(jwt: Jwt): String {
val id = jwt.subject
logger.info("매우 민감한 정보에 접근하는 호출 - $id")
return "인증없이 출력되면 매우 민감한 문장"
}
}
목록
OAuth2
인가 요청
요청 예제
| Parameter | Description |
|---|---|
|
Client Id |
|
Redirect URI |
|
code 로 고정 |
GET /oauth2/authorize?client_id=laboratory&redirect_uri=android-app%3A%2F%2Fcodes.stable.apps.laboratory%2Foauth2%2Fredirect&response_type=code HTTP/1.1
Host: auth.stable.codes
Cookie: SESSION=d7c8f102-27a4-49ed-9c19-1b1c438f0182
응답 예제
HTTP/1.1 302 Found
x-content-type-options: nosniff
x-xss-protection: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
location: android-app://codes.stable.apps.laboratory/oauth2/redirect?code=IsoIRRCrFMscWbjYj0r9clIYGrFFmLiK7qTnA0kJUZPgs2-tXq3YMRzMul_SGhOX594ZP7vln3yc57NBdm27-vRqgFwrIRqpuA7rRkUvX6A1T_Rtvc2kgN1ZLSn0y-5-
x-cloud-trace-context: abfabea9f3ea72e54186c2ecf5225dae
date: Mon, 30 Jun 2025 01:18:40 GMT
content-type: text/html
server: Google Frontend
토큰 발급
요청 예제
| Name | Description |
|---|---|
|
client_id:client_secret 을 Basic Auth 로 변환 한 값 |
| Parameter | Description |
|---|---|
|
authorization_code 로 고정 |
|
Redirect URI |
|
code 값 |
POST /oauth2/token HTTP/1.1
Authorization: Basic bGFib3JhdG9yeTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
Host: auth.stable.codes
Content-Length: 245
grant_type=authorization_code&redirect_uri=android-app%3A%2F%2Fcodes.stable.apps.laboratory%2Foauth2%2Fredirect&code=IsoIRRCrFMscWbjYj0r9clIYGrFFmLiK7qTnA0kJUZPgs2-tXq3YMRzMul_SGhOX594ZP7vln3yc57NBdm27-vRqgFwrIRqpuA7rRkUvX6A1T_Rtvc2kgN1ZLSn0y-5-
응답 예제
HTTP/1.1 200 OK
x-content-type-options: nosniff
x-xss-protection: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
content-type: application/json;charset=UTF-8
date: Mon, 30 Jun 2025 01:18:41 GMT
server: Google Frontend
Transfer-Encoding: chunked
Content-Length: 866
{
"access_token" : "eyJraWQiOiIwOTQzZThlYS00NDZhLTQzOTktOWNjNC1lYjBmYTE5NGYyYTIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoibGFib3JhdG9yeSIsIm5iZiI6MTc1MTI0NjMyMSwiaXNzIjoiaHR0cDovL2F1dGguc3RhYmxlLmNvZGVzIiwiZXhwIjoxNzUxMjQ2NjIxLCJpYXQiOjE3NTEyNDYzMjEsImp0aSI6IjZiMzg2YTBhLTUzYWYtNGI0Ni05ZjM4LTU2N2QwZGRhZDkwNiJ9.aJ0udM1-D39vETmaI4DHABUoIxC05i5oTj-1yOE_lkqlZ9ShfubJrn0GiR3XM_ntAx9PtbwNhzN0Nufmf1M0ST7VBIiYQGv2Osjy6QQ1fk-JWKmA8v4rkxQGEZJdrMpjnG4JNxQ0W4dIVJtMAxR6EQT46VSNoqMYnzrKAChhSmHq622VrBltWBi-FrsYbtCPWo60lif5l-8zx2SAenD5k0MHV8iDKL3yhCss5Z9t9ZT0rP-2GIdA5EDFDTMRtUlf_0EQD-39qVHLaN21HtpPCilWjEcsxXWOUH624FinvyrDrbFTu0Z4BVSij-Wh4yDvd12QurjNVxhl44Z0evsytw",
"refresh_token" : "rhHcP3ihD2U9QYwQvgQ9Huf-ZmVdJkSzyg21Co7FCxpoBCsql29L7e3wDyoK0gAFjWrEzQs8w92WBVO3wUPQCu7letKQgavNJBPfv22Vwq_vBo9muF-yAj4d8qWWz1DY",
"token_type" : "Bearer",
"expires_in" : 299
}
| Path | Type | Description |
|---|---|---|
|
|
Access Token |
|
|
Refresh Token |
|
|
토큰 타입 (예: Bearer) |
|
|
토큰 만료까지 남은 시간 (초) |
토큰 검증
요청 예제
| Name | Description |
|---|---|
|
client_id:client_secret 을 Basic Auth 로 변환 한 값 |
| Parameter | Description |
|---|---|
|
Access Token or Refresh Token |
|
access_token or refresh_token |
POST /oauth2/introspect HTTP/1.1
Authorization: Basic bGFib3JhdG9yeTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
Host: auth.stable.codes
Content-Length: 675
token=eyJraWQiOiIwOTQzZThlYS00NDZhLTQzOTktOWNjNC1lYjBmYTE5NGYyYTIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoibGFib3JhdG9yeSIsIm5iZiI6MTc1MTI0NjMyMSwiaXNzIjoiaHR0cDovL2F1dGguc3RhYmxlLmNvZGVzIiwiZXhwIjoxNzUxMjQ2NjIxLCJpYXQiOjE3NTEyNDYzMjEsImp0aSI6IjZiMzg2YTBhLTUzYWYtNGI0Ni05ZjM4LTU2N2QwZGRhZDkwNiJ9.aJ0udM1-D39vETmaI4DHABUoIxC05i5oTj-1yOE_lkqlZ9ShfubJrn0GiR3XM_ntAx9PtbwNhzN0Nufmf1M0ST7VBIiYQGv2Osjy6QQ1fk-JWKmA8v4rkxQGEZJdrMpjnG4JNxQ0W4dIVJtMAxR6EQT46VSNoqMYnzrKAChhSmHq622VrBltWBi-FrsYbtCPWo60lif5l-8zx2SAenD5k0MHV8iDKL3yhCss5Z9t9ZT0rP-2GIdA5EDFDTMRtUlf_0EQD-39qVHLaN21HtpPCilWjEcsxXWOUH624FinvyrDrbFTu0Z4BVSij-Wh4yDvd12QurjNVxhl44Z0evsytw&token_type_hint=access_token
응답 예제
HTTP/1.1 200 OK
x-content-type-options: nosniff
x-xss-protection: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
content-type: application/json
date: Mon, 30 Jun 2025 01:18:41 GMT
server: Google Frontend
Transfer-Encoding: chunked
Content-Length: 278
{
"active" : true,
"sub" : "user",
"aud" : [ "laboratory" ],
"nbf" : 1751246321,
"iss" : "http://auth.stable.codes",
"exp" : 1751246621,
"iat" : 1751246321,
"jti" : "6b386a0a-53af-4b46-9f38-567d0ddad906",
"client_id" : "laboratory",
"token_type" : "Bearer"
}
| Path | Type | Description |
|---|---|---|
|
|
토큰 활성화 여부 |
|
|
토큰 주체 (사용자 ID) |
|
|
토큰 대상 (클라이언트 ID 목록) |
|
|
토큰 사용 가능 시작 시간 |
|
|
토큰 발급자 (Issuer) |
|
|
토큰 만료 시간 |
|
|
토큰 발급 시간 |
|
|
토큰 식별자 (JWT ID) |
|
|
클라이언트 ID |
|
|
토큰 타입 (예: Bearer) |
토큰 갱신
Access Token 만료 시 Refresh Token 을 이용한 토큰 갱신
요청 예제
| Name | Description |
|---|---|
|
client_id:client_secret 을 Basic Auth 로 변환 한 값 |
| Parameter | Description |
|---|---|
|
refresh_token 으로 고정 |
|
Refresh Token |
POST /oauth2/token HTTP/1.1
Authorization: Basic bGFib3JhdG9yeTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
Host: auth.stable.codes
Content-Length: 167
grant_type=refresh_token&refresh_token=rhHcP3ihD2U9QYwQvgQ9Huf-ZmVdJkSzyg21Co7FCxpoBCsql29L7e3wDyoK0gAFjWrEzQs8w92WBVO3wUPQCu7letKQgavNJBPfv22Vwq_vBo9muF-yAj4d8qWWz1DY
응답 예제
HTTP/1.1 200 OK
x-content-type-options: nosniff
x-xss-protection: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
content-type: application/json;charset=UTF-8
date: Mon, 30 Jun 2025 01:18:42 GMT
server: Google Frontend
Transfer-Encoding: chunked
Content-Length: 866
{
"access_token" : "eyJraWQiOiIwOTQzZThlYS00NDZhLTQzOTktOWNjNC1lYjBmYTE5NGYyYTIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoibGFib3JhdG9yeSIsIm5iZiI6MTc1MTI0NjMyMiwiaXNzIjoiaHR0cDovL2F1dGguc3RhYmxlLmNvZGVzIiwiZXhwIjoxNzUxMjQ2NjIyLCJpYXQiOjE3NTEyNDYzMjIsImp0aSI6ImM2NjYxODM5LWIxMjctNGE2NC1hN2QzLTdiYjBmMjc0NWMwNCJ9.OwwQtK4u3mfdM-NTFDAyDnIRh0MO4ltuCLQeKN_PCqRLFpQfXMf5Gi3_9VdRbqyUhBNbiYmaYySgjx4GzFkXzobmINCBRFKr4IW1LKsOWF3YHEqKw4yhxMHVVYWAn9rj85AlJ85GNSgHHGFi0Fn_OfqabhYIJmCj3djt3YxkKq1XTvL935iOfspITCH3-aR3PUjNtLMWVu0lbuwoxyT9ykmcKlQ9NujevcJyQDZnmJ_XcnP5D3Gfxe1e-Jjyy1h-dOxxZn-8ZtLwv9L-CN199LWk5EPc_4Gj54CbmkxqbUSbe1sG8P1WEm_OpZenzFqhU2LkmfcN-rBA4uG-qM9dqg",
"refresh_token" : "rhHcP3ihD2U9QYwQvgQ9Huf-ZmVdJkSzyg21Co7FCxpoBCsql29L7e3wDyoK0gAFjWrEzQs8w92WBVO3wUPQCu7letKQgavNJBPfv22Vwq_vBo9muF-yAj4d8qWWz1DY",
"token_type" : "Bearer",
"expires_in" : 299
}
| Path | Type | Description |
|---|---|---|
|
|
Access Token |
|
|
Refresh Token |
|
|
토큰 타입 (예: Bearer) |
|
|
토큰 만료까지 남은 시간 (초) |
토큰 파기
Access Token 혹은 Refresh Token 을 명시적 서버요청으로 파기 할 수 있는 기능
요청 예제
| Name | Description |
|---|---|
|
client_id:client_secret 을 Basic Auth 로 변환 한 값 |
| Parameter | Description |
|---|---|
|
Token 값 |
|
Token 형태 (access_token, refresh_token) |
POST /oauth2/revoke HTTP/1.1
Authorization: Basic bGFib3JhdG9yeTpzZWNyZXQ=
Content-Type: application/x-www-form-urlencoded; charset=ISO-8859-1
Host: auth.stable.codes
Content-Length: 675
token=eyJraWQiOiIwOTQzZThlYS00NDZhLTQzOTktOWNjNC1lYjBmYTE5NGYyYTIiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYXVkIjoibGFib3JhdG9yeSIsIm5iZiI6MTc1MTI0NjMyMSwiaXNzIjoiaHR0cDovL2F1dGguc3RhYmxlLmNvZGVzIiwiZXhwIjoxNzUxMjQ2NjIxLCJpYXQiOjE3NTEyNDYzMjEsImp0aSI6IjZiMzg2YTBhLTUzYWYtNGI0Ni05ZjM4LTU2N2QwZGRhZDkwNiJ9.aJ0udM1-D39vETmaI4DHABUoIxC05i5oTj-1yOE_lkqlZ9ShfubJrn0GiR3XM_ntAx9PtbwNhzN0Nufmf1M0ST7VBIiYQGv2Osjy6QQ1fk-JWKmA8v4rkxQGEZJdrMpjnG4JNxQ0W4dIVJtMAxR6EQT46VSNoqMYnzrKAChhSmHq622VrBltWBi-FrsYbtCPWo60lif5l-8zx2SAenD5k0MHV8iDKL3yhCss5Z9t9ZT0rP-2GIdA5EDFDTMRtUlf_0EQD-39qVHLaN21HtpPCilWjEcsxXWOUH624FinvyrDrbFTu0Z4BVSij-Wh4yDvd12QurjNVxhl44Z0evsytw&token_type_hint=access_token
응답 예제
HTTP/1.1 200 OK
x-content-type-options: nosniff
x-xss-protection: 0
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
x-cloud-trace-context: 8b2effe98c6fd2e115c770d692e359b6
date: Mon, 30 Jun 2025 01:18:42 GMT
content-type: text/html
server: Google Frontend