지난 포스팅에서 Jwt를 이용하여 인증서버를 구성하는 방법을 설명하였습니다. 이 시스템에서는 서비스영역과 인증영역이 병합되어 있어 추후 시스템의 확장 또는 제공 서비스에 따라 시스템을 분리하기가 어렵습니다. 본 포스팅에서는Authorization Server와 Resource Server를 분리하는 방법에 대해서 설명합니다.
Resource Server 분리하기
Jwt 방식을 사용하는 경우
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.util.FileCopyUtils;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(ResourceServerConfig.class);
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.requestMatchers().antMatchers("/api/**");
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource("kr.ejsoft.oauth2.publickey.txt");
String publickey = null;
try {
publickey = asString(resource);
log.info("Jwt Verifier Key : {} ", publickey);
} catch(final IOException e) {
throw new RuntimeException(e);
}
converter.setVerifierKey(publickey);
return converter;
}
public static String asString(Resource resource) throws IOException {
Reader reader = new InputStreamReader(resource.getInputStream(), "UTF-8");
return FileCopyUtils.copyToString(reader);
}
}
Jwt 방식을 사용하지 않는 경우
Jwt 방식을 사용하지 않는 경우에는 Resource 서버에서 token 검증 요청을 Authorization 서버로 보내게 됩니다. 토큰의 검증을 위해 Authorization Server의 /oauth/check_token 경로를 호출하며 호출전에 미리 접근권한이 설정이 되어 있어야 합니다.
/oauth/check_token 활성화
AuthorizationServerConfig에서 아래와 같이 .checkTokenAccess("isAuthenticated()")을 추가하여 접근권한을 부여합니다. 기본적으로 접근을 할 수 없습니다.
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()") //allow check token
.allowFormAuthenticationForClients();
}
ResourceServerConfig 수정
설정시에는 Authorization Server의 경로와 Client Id, Client Secret를 전달해주어야 합니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(ResourceServerConfig.class);
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().anyRequest().authenticated()
.and()
.requestMatchers().antMatchers("/api/**");
}
@Primary
@Bean
public RemoteTokenServices tokenService() {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
tokenService.setClientId("foo");
tokenService.setClientSecret("bar");
return tokenService;
}
}
여기서 의문이 생겼습니다. "여러 종류의 Client가 동일 서비스를 사용하지 않게 한 이유는 뭘까? 왜 Client Id/Secret를 지정해야 하지?" Stackoverflow에서 찾은 결론은 "ResourceServer에서는 사용자 위주로 권한을 부여하기 때문에 Client 식별값이 없으면 토큰에 대한 명확한 사용자를 식별할 수 없다" 입니다. 아래의 주소를 찾고하십시오.
Why Resource Server has to know client_id in Spring OAuth2?
서비스 제공 예시
Service Controller 추가
서비스를 제공하기 위한 임의의 서비스 컨트롤러 객체를 생성합니다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class RestResourceSample {
private static final Logger log = LoggerFactory.getLogger(RestResourceSample.class);
@RequestMapping(value="/api/users", method=RequestMethod.GET)
@ResponseBody
public String profile() {
// Build some dummy data to return for testing
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
log.debug("Principal : {}", principal);
String username = "";
String email = "";
if(principal instanceof User) {
User user = (User) principal;
username = user.getUsername();
email = user.getUsername() + "@email.com";
} else {
username = (String) principal;
email = username + "@email.com";
}
UserProfile profile = new UserProfile();
profile.setName(username);
profile.setEmail(email);
log.debug("/api/users : {}", profile);
return profile.toJson();
}
}
class UserProfile {
private String name;
private String email;
// Setters and getters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "UserProfile [name=" + name + ", email=" + email + "]";
}
public String toJson() {
return String.format("{\"name\" : \"%s\", \"email\" : \"%s\", }", name, email);
}
}
서비스 호출
GET방식으로 서비스를 요청합니다.
요청주소 : http://localhost:9091/api/user
요청헤더 :
Content-Type=application/x-www-form-urlencoded
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzQ4NDA5NTUsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJ1c2VyIl0sImp0aSI6ImQ2NTU1MzAzLTllMzAtNDBkMi1hYWU4LTIzZjNjNDIwNGEwNSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWRfcHJvZmlsZSJdfQ.rxXCbRXQ0vOkKkaMNh4bBq2bHNtewtF4VqX9PCEaBIIjLdZHpxQ_vNl7yLsfJqi5fuGnpibekkXpeKtbiq817NYWnYvoXq_WRpFrJIcHritqWCWf_8yzqP7ElLsFui7Fm3nI8Bbd9K1Pkyeo08LktQe1lBuNifm3ZLR_TOUKu_pGPHXXum0-LRA_teJ0dKzhdWlwT7OLxrElR0pi7VUaHzoYFWrI7psXFHAERXpoA0YfuJGAXLYrj5nbgFgnmWZ3UnkEy9oE43d0_Cyi5QAmKgjp5pYVXOjRppC-h4F-3ZxamVFwCPGWcJOXbg9htVqU3N6n8fippiGDdb_m-ZXOIA
헤더의 Authorization 값은 발급된 Access Token을 사용합니다.
요청본문
GET /api/users HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzQ4NDA5NTUsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJ1c2VyIl0sImp0aSI6ImQ2NTU1MzAzLTllMzAtNDBkMi1hYWU4LTIzZjNjNDIwNGEwNSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWRfcHJvZmlsZSJdfQ.rxXCbRXQ0vOkKkaMNh4bBq2bHNtewtF4VqX9PCEaBIIjLdZHpxQ_vNl7yLsfJqi5fuGnpibekkXpeKtbiq817NYWnYvoXq_WRpFrJIcHritqWCWf_8yzqP7ElLsFui7Fm3nI8Bbd9K1Pkyeo08LktQe1lBuNifm3ZLR_TOUKu_pGPHXXum0-LRA_teJ0dKzhdWlwT7OLxrElR0pi7VUaHzoYFWrI7psXFHAERXpoA0YfuJGAXLYrj5nbgFgnmWZ3UnkEy9oE43d0_Cyi5QAmKgjp5pYVXOjRppC-h4F-3ZxamVFwCPGWcJOXbg9htVqU3N6n8fippiGDdb_m-ZXOIA
Content-Type: application/x-www-form-urlencoded
User-Agent: http4e/5.0.12
Host: localhost:9091
응답본문
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 47
Date: Wed, 27 Nov 2019 07:20:01 GMT
{"name" : "user", "email" : "user@email.com", }
참고사이트
- Spring Boot Oauth2 – ResourceServer
https://daddyprogrammer.org/post/1754/spring-boot-oauth2-resourceserver/
소스코드
소스코드는 여기에서 다운로드 가능합니다.
'Development > Spring Framework, Security, OAuth2' 카테고리의 다른 글
Spring Boot with OAuth2 - 7. JWT Refresh Token 관리 (0) | 2019.12.02 |
---|---|
Spring Boot with OAuth2 - 6. OAuth2 Server 접근/권한 제어 (0) | 2019.11.27 |
Spring Boot with OAuth2 - 4. OAuth2 Server 확장 3 - JwtTokenStore (0) | 2019.11.26 |
Spring Boot with OAuth2 - 3. OAuth2 Server 확장 2 - MyBatis + MariaDB 연결 (0) | 2019.11.21 |
Spring Boot with OAuth2 - 2. OAuth2 Server 확장 1 - Hibernate + MariaDB 연결 (0) | 2019.11.20 |