diff --git a/auth-service/pom.xml b/auth-service/pom.xml index 78c63e6..addd95c 100644 --- a/auth-service/pom.xml +++ b/auth-service/pom.xml @@ -47,15 +47,15 @@ org.springframework.cloud spring-cloud-starter-netflix-eureka-client + org.springframework.cloud spring-cloud-security - - + org.springframework.boot diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/AuthServiceApplication.java b/auth-service/src/main/java/com/assu/cloud/authservice/AuthServiceApplication.java index 1a165a5..d857384 100644 --- a/auth-service/src/main/java/com/assu/cloud/authservice/AuthServiceApplication.java +++ b/auth-service/src/main/java/com/assu/cloud/authservice/AuthServiceApplication.java @@ -17,22 +17,19 @@ import java.util.Map; @EnableResourceServer @EnableAuthorizationServer // 이 서비스가 OAuth2 인증 서버가 될 것이라고 스프링 클라우드에 알림 public class AuthServiceApplication { - /** * 사용자 정보 조회 시 사용 * OAuth2 로 보호되는 서비스에 접근하려고 할 때 사용 * 보호 서비스로 호출되어 OAuth2 액세스 토큰의 유효성을 검증하고 보호 서비스에 접근하는 사용자 역할 조회 */ - @RequestMapping(value = { "/user" }, produces = "application/json") + @RequestMapping(value = { "/user" }, produces = "application/json") // /auth/user 로 매핑 public Map user(OAuth2Authentication user) { Map userInfo = new HashMap<>(); userInfo.put("user", user.getUserAuthentication().getPrincipal()); userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities())); return userInfo; } - public static void main(String[] args) { SpringApplication.run(AuthServiceApplication.class, args); } - } diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/security/OAuth2Config.java b/auth-service/src/main/java/com/assu/cloud/authservice/security/OAuth2Config.java new file mode 100644 index 0000000..893630b --- /dev/null +++ b/auth-service/src/main/java/com/assu/cloud/authservice/security/OAuth2Config.java @@ -0,0 +1,48 @@ +package com.assu.cloud.authservice.security; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; +import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; + +/** + * OAuth2 인증 서버에 등록될 애플리케이션 정의 + * AuthorizationServerConfigurerAdapter: 스프링 시큐리티 핵심부, 핵심 인증 및 인가 기능 수행하는 기본 메커니즘 제공 + */ +@Configuration +public class OAuth2Config extends AuthorizationServerConfigurerAdapter { + + private final AuthenticationManager authenticationManager; + private final UserDetailsService userDetailsService; + + public OAuth2Config(AuthenticationManager authenticationManager, UserDetailsService userDetailsService) { + this.authenticationManager = authenticationManager; + this.userDetailsService = userDetailsService; + } + + /** + * 인증 서버에 등록될 클라이언트 정의 + * 즉, OAuth2 서비스로 보호되는 서비스에 접근할 수 있는 클라이언트 애플리케이션 등록 + */ + @Override + public void configure(ClientDetailsServiceConfigurer clients) throws Exception { + clients.inMemory() // 애플리케이션 정보를 위한 저장소 (인메모리 / JDBC) + .withClient("assuapp") // assuapp 애플리케이션이 토큰을 받기 위해 인증 서버 호출 시 제시할 시크릿과 애플리케이션명 + .secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("12345")) + .authorizedGrantTypes("refresh_token", "password", "client_credentials") // OAuth2 에서 지원하는 인가 그랜트 타입, 여기선 패스워드/클라이언트 자격증명 그랜트타입 + .scopes("webclient", "mobileclient"); // 토큰 요청 시 애플리케이션의 수행 경계 정의 + } + + /** + * AuthorizationServerConfigurerAdapter 안에서 사용될 여러 컴포넌트 정의 + * 여기선 스프링에 기본 인증 관리자와 사용자 상세 서비스를 이용한다고 선언 + */ + @Override + public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { + endpoints.authenticationManager(authenticationManager) + .userDetailsService(userDetailsService); + } +} diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/security/WebSecurityConfigurer.java b/auth-service/src/main/java/com/assu/cloud/authservice/security/WebSecurityConfigurer.java new file mode 100644 index 0000000..afb1861 --- /dev/null +++ b/auth-service/src/main/java/com/assu/cloud/authservice/security/WebSecurityConfigurer.java @@ -0,0 +1,15 @@ +package com.assu.cloud.authservice.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } +} diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContext.java b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContext.java new file mode 100644 index 0000000..668db7e --- /dev/null +++ b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContext.java @@ -0,0 +1,24 @@ +package com.assu.cloud.authservice.utils; + +import org.springframework.stereotype.Component; + +/** + * 서비스가 쉽게 액세스할 수 있는 HTTP 헤더를 만들어 저장하는 클래스 + * HTTP 요청에서 추출한 값을 보관하는 POJO + */ +@Component +public class CustomContext { + public static final String CORRELATION_ID = "assu-correlation-id"; + + private String correlationId = new String(); + + // 그 외 필요한 항목 넣을 수 있음 (인증 토큰 등...) + + public String getCorrelationId() { + return correlationId; + } + + public void setCorrelationId(String correlationId) { + this.correlationId = correlationId; + } +} diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextFilter.java b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextFilter.java new file mode 100644 index 0000000..c25bbd7 --- /dev/null +++ b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextFilter.java @@ -0,0 +1,40 @@ +package com.assu.cloud.authservice.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +/** + * 유입되는 HTTP 요청을 가로채서 필요한 헤더값을 CustomContext 에 매핑 + * + * REST 서비스에 대한 모든 HTTP 요청을 가로채서 컨텍스트 정보(상관관계 ID 등)를 추출해 CustomContext 클래스에 매핑하는 HTTP 서블릿 필터 + * REST 서비스 호출 시 코드에서 CustomContext 액세스가 필요할 때마다 ThreadLocal 변수에서 검색해 읽어올 수 있음 + */ +@Component +public class CustomContextFilter implements Filter { + + private static final Logger logger = LoggerFactory.getLogger(CustomContextFilter.class); + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + + // HTTP 호출 헤더에서 상관관계 ID 를 검색하여 CustomContextHolder 의 CustomContext 클래스에 설정 + CustomContextHolder.getContext().setCorrelationId(httpServletRequest.getHeader(CustomContext.CORRELATION_ID)); + // 그 외 필요한 항목 넣을 수 있음 (인증 토큰 등...) + + logger.debug("상관관계 ID {} 로 실행된 동적 라우팅", CustomContextHolder.getContext().getCorrelationId()); + + filterChain.doFilter(httpServletRequest, servletResponse); + } + + @Override + public void init(FilterConfig filterConfig) {} + + @Override + public void destroy() {} +} diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextHolder.java b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextHolder.java new file mode 100644 index 0000000..f67e7ad --- /dev/null +++ b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextHolder.java @@ -0,0 +1,37 @@ +package com.assu.cloud.authservice.utils; + +import org.springframework.util.Assert; + +/** + * ThreadLocal 저장소에 CustomContext 를 저장하는 클래스 + * * ThreadLocal 변수: 사용자 요청을 처리하는 해당 스레드에서 호출되는 모든 메서드에서 액세스 가능한 변수 + * + * CustomContext 가 ThreadLocal 저장소에 저장되면 요청으로 실행된 모든 코드에서 CustomContextHolder 의 CustomContext 객체 사용 가능 + */ +public class CustomContextHolder { + + /** 정적 ThreadLocal 변수에 저장되는 CustomContext */ + private static final ThreadLocal customContext = new ThreadLocal<>(); + + /** + * CustomContext 객체를 사용하기 위해 조회해오는 메서드 + */ + public static final CustomContext getContext() { + CustomContext ctx = customContext.get(); + + if (ctx == null) { + ctx = createEmptyContext(); + customContext.set(ctx); + } + return customContext.get(); + } + + public static final void setContext(CustomContext ctx) { + Assert.notNull(ctx, "customcontxt is null."); + customContext.set(ctx); + } + + public static final CustomContext createEmptyContext() { + return new CustomContext(); + } +} diff --git a/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextInterceptor.java b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextInterceptor.java new file mode 100644 index 0000000..7ee8a12 --- /dev/null +++ b/auth-service/src/main/java/com/assu/cloud/authservice/utils/CustomContextInterceptor.java @@ -0,0 +1,27 @@ +package com.assu.cloud.authservice.utils; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +import java.io.IOException; + +/** + * RestTemplate 인스턴스에서 실행되는 모든 HTTP 기반 서비스 발신 요청에 상관관계 ID 삽입 + */ +public class CustomContextInterceptor implements ClientHttpRequestInterceptor { + /** + * RestTemplate 로 실제 HTTP 서비스 호출 전 intercept 메서드 호출 + */ + @Override + public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { + HttpHeaders headers = httpRequest.getHeaders(); + + headers.add(CustomContext.CORRELATION_ID, CustomContextHolder.getContext().getCorrelationId()); + // 그 외 필요한 항목 넣을 수 있음 (인증 토큰 등...) + + return clientHttpRequestExecution.execute(httpRequest, bytes); + } +}