[Oauth2.0] 스프링부트 OAuth2 jwt kotlin (client credentials)
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
신입 3개월차에 oauth관련해서 서브로 깔짝 개발할 일이 있어서 그때 한 번 잠깐 공부를 했었는데 (결국 이해 못하고 넘어감)
그 때 OAuth! 어렵다! 모르겠다! 는 심리적 벽이 생겨버려서 이번에도 쉽지않았다 (문서가 안읽힘)
타사와 openAPI 를 통해 서로 데이터를 주고받기로함
연동업체에서 인증관련 가이드가 내려옴
------------------------------
API 통신을 위한 인증의 경우 아래 당사 정보보안가이드를 준수해야하므로 ...
API 에 대한 인증 방식은 OneTime Token 을 사용하거나 혹은 그에 준하는 가변값으로 인증이 필요
- OneTime Token 방식인증 API 를 제공하여 1 회성 액세스 토큰 발행
- OneTime Token 에 준하는 가변값의 인증 방식 예제 (사전 협의 필요)
------------------------------
OAuth2 인증 방식 4가지
1) Authorization Code
- ex) 구글, 페이스북 로그인
2) Implicit
3) Resource Oner Password
4) Client Credentials
- 업체끼리 연동
개발환경
spring boot 2.5.6
java 11
kotlin
MySQL
인증 구현은 처음이라 일단 샘플을 만들어본다
accessToken 발급 해보기
build.gradle.kts
dependencies {
//implementation(project(":domain"))
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.0.1.RELEASE")
implementation("org.springframework.boot:spring-boot-autoconfigure")
implementation("org.springframework.boot:spring-boot-starter-web")
...
외에도 뭔가 이것저것 덕지덕지 붙어있는데 생략
@Configuration
class Oauth2Config {
/**
* Oauth2 서버설정
*/
@Configuration
@EnableAuthorizationServer
class OAuth2AuthorizationServerConfiguration : AuthorizationServerConfigurerAdapter() {
override fun configure(clients: ClientDetailsServiceConfigurer) {
/* db사용으로 변경? */
clients
.inMemory()
.withClient("client")
.secret("{noop}secret")
.scopes("default")
.authorizedGrantTypes("client_credentials")
.accessTokenValiditySeconds(86400) // 24 hours
}
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
}
override fun configure(oauthServer: AuthorizationServerSecurityConfigurer) {
oauthServer.allowFormAuthenticationForClients()
}
@Bean
fun accessTokenConverter(): JwtAccessTokenConverter = JwtAccessTokenConverter().apply {
setSigningKey("t0P$3cret")
}
@Bean
fun tokenStore(): TokenStore = JwtTokenStore(accessTokenConverter())
@Bean
@Primary
fun tokenServices(): DefaultTokenServices = DefaultTokenServices().apply {
setTokenStore(tokenStore())
}
}
/**
* 리소스 서버설정
*/
@Configuration
@EnableResourceServer
class OAuth2ResourceServerConfiguration(
private val tokenServices: DefaultTokenServices //default store 랑 그냥 token store 랑 뭐가 다름?
) : ResourceServerConfigurerAdapter() {
override fun configure(resources: ResourceServerSecurityConfigurer) {
resources.tokenServices(tokenServices)
}
// oauth 토큰에 의해 보호되고 있는 리소스 서버에 대한 설정override fun configure(http: HttpSecurity) {}
http.cors().disable()
.csrf().disable() // csrf 보안 토큰 disable
.httpBasic().disable() // rest api만 고려, 기본 설정 해제
.authorizeRequests() // 요청에 대한 사용 권한 체크
.antMatchers("/**").authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint { request: HttpServletRequest?, response: HttpServletResponse, authException: AuthenticationException? -> response.sendError(
HttpServletResponse.SC_UNAUTHORIZED) }
.accessDeniedHandler { request: HttpServletRequest?, response: HttpServletResponse, authException: AccessDeniedException? -> response.sendError(
HttpServletResponse.SC_UNAUTHORIZED) }
}
}
여기까지하고
포스트맨으로 호출 -> 토큰을 발급받아본다
request
url : .../oauth/token
method : POST
params : client_id, client_secret, grant_type
client_id, client_secret = 소스에 기술되어있음
grant_type = client_credentials 고정값
response
토큰이 나왔다
AuthorizationServer에서 클라이언트 설정부분 inmemory 방식으로 되어있던걸 DB방식으로 변경
/**
* Oauth2 서버설정
*/
@Configuration
@EnableAuthorizationServer
class OAuth2AuthorizationServerConfiguration(
private val dataSource: DataSource,
private val clientDetailsService: ClientDetailsService
) : AuthorizationServerConfigurerAdapter() {
// 클라이언트에 대한 설정
override fun configure(clients: ClientDetailsServiceConfigurer) {
/* db사용으로 변경? */
/*clients
.inMemory()
.withClient("client")
.secret("{noop}secret")
.scopes("default")
.authorizedGrantTypes("client_credentials")
.accessTokenValiditySeconds(86400) // 24 hours*/
clients.withClientDetails(clientDetailsService)
}
// 인증, 토큰 엔드포인트, 토큰 서비스 정의
override fun configure(endpoints: AuthorizationServerEndpointsConfigurer) {
endpoints.tokenStore(tokenStore())
.accessTokenConverter(accessTokenConverter())
}
// 토큰 엔드포인트 (/auth/token)에 대한 보안관련 설정
override fun configure(security: AuthorizationServerSecurityConfigurer) {
security.tokenKeyAccess("permitAll()").allowFormAuthenticationForClients()
}
@Bean
fun accessTokenConverter(): JwtAccessTokenConverter = JwtAccessTokenConverter().apply {
setSigningKey("t0P$3cret")
}
@Bean
fun tokenStore(): TokenStore = JwtTokenStore(accessTokenConverter())
@Bean
@Primary
fun jdbcClientDetailsService(dataSource: DataSource?): JdbcClientDetailsService? {
return JdbcClientDetailsService(dataSource)
}
@Bean
@Primary
fun tokenServices(): DefaultTokenServices = DefaultTokenServices().apply {
setTokenStore(tokenStore())
}
@Bean
fun passwordEncoder(): BCryptPasswordEncoder? {
return BCryptPasswordEncoder()
}
}
클라이언트 테이블 생성
CREATE TABLE `oauth_client_details` (
`client_id` varchar(256) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int DEFAULT NULL,
`refresh_token_validity` int DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
샘플 컨트롤러 만들어서 발급받은 토큰으로 포스트맨에서 호출해보기
컨트롤러
@PostMapping("/test")
fun test(@RequestBody test: ForTest): ResponseEntity<ForTest> {
println("testController@@@")
return ResponseEntity.ok(test)
}
Headers
Authorization : Bearer eyJhbGciOiJIUzI1NiIsInR5cC....(/oauth/token 에서 발급받은토큰)
RequestBody
{"id":3334,"name":"secretddd"}Error Response : 토큰 안맞을경우{"error": "invalid_token","error_description": "Cannot convert access token to JSON"}Error Response : Authorization 안보낼경우{"timestamp": "2022-05-31T07:41:26.867+00:00","status": 401,"error": "Unauthorized","message": "No message available","path": "/test"}
이제 액세스토큰을 발행해서 해당토큰으로 리소스 서버 api를 호출하는 것까지는 되었다
더 할일
1. jwt 사인키 방식 변경
2. oneTime AccessToken 으로 변경 (api 호출할때마다 새 토큰 발급받아야 접근가능하도록)
3. 인증서버, 리소스서버 나누기
4. 그 외 더 필요한 인증 및 보안관련 사항있나 확인하기
자바쟁이였는데 갑자기 코틀린으로 하려니까 재봉틀쓰다가 바느질하는기분
작성중...
참고 사이트
https://co-de.tistory.com/36?category=884527
https://brunch.co.kr/@sbcoba/15
https://github.com/AliakseiYaustratsyeu/spring-boot-oauth-client-credentials
https://docs.spring.io/spring-security/reference/5.6.0-RC1/servlet/oauth2/oauth2-client.html
https://velog.io/@skysoo/%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B0%9C%EB%B0%9C%EC%9E%90-%EB%A1%9C%EB%93%9C%EB%A7%B5-%EB%94%B0%EB%9D%BC%EA%B0%80%EA%B8%B0-10.-OAuth2.0
https://autumnly.tistory.com/65?category=813931
https://auth0.com/docs/get-started/authentication-and-authorization-flow/client-credentials-flow
https://github.com/nononsensecode/client-credential-flow-with-signed-jwt
https://blog.naver.com/mds_datasecurity/222182943542
스택오버플로우
- 공유 링크 만들기
- X
- 이메일
- 기타 앱