Spring Boot - Admin!
Boot Admin
github: https://github.com/codecentric/spring-boot-admin ref doc: https://codecentric.github.io/spring-boot-admin/2.6.6/
spring actuator 를 사용한 오픈소스 모니터링 프로젝트
모니터링에 대해 별도의 운영환경이 없다면 사용해볼만한 프로젝트임.
관제 서버로 사용할 Boot Admin Server
디펜던시
관제할 클라이언트로 사용할 Boot Admin Client
디펜던시가 따로 있다.
implementation 'de.codecentric:spring-boot-admin-starter-server:2.6.6'
implementation 'de.codecentric:spring-boot-admin-starter-client:2.6.6'
Admin Server
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'de.codecentric:spring-boot-admin-starter-server:2.6.6'
Admin Server
를 운영하기 위해 위 3개 dependency
는 필수
security
의 경우 id/pw
를 통해 자동으로 보안처리 할 수 있음으로 사용 권장한다.
우선 security 에 고정된 문자열로 계정을 사용한다.
server.port=8080
spring.security.user.name=admin
spring.security.user.password=admin
security
에 UI
를 위한 리소스 파일은 권한없이 접근하도록 지정하고
csrf
, cors
등의 대한 처리를 진행
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final AdminServerProperties adminServer;
private final SecurityProperties security;
public SecurityConfig(AdminServerProperties adminServer, SecurityProperties security) {
this.adminServer = adminServer;
this.security = security;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(this.adminServer.getContextPath() + "/applications");
http
.httpBasic().and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()).ignoringRequestMatchers(
new AntPathRequestMatcher(this.adminServer.getContextPath() + "/instances", HttpMethod.POST.toString()),
new AntPathRequestMatcher(this.adminServer.getContextPath() + "/instances/*", HttpMethod.DELETE.toString()),
new AntPathRequestMatcher(this.adminServer.getContextPath() + "/actuator/**")).and()
.cors().configurationSource(corsConfigurationSource()).and()
.authorizeRequests()
.antMatchers(this.adminServer.getContextPath() + "/favicon.ico").permitAll()
.antMatchers(this.adminServer.getContextPath() + "/assets/**").permitAll()
.antMatchers(this.adminServer.getContextPath() + "/login").permitAll()
.anyRequest().authenticated().and()
.formLogin().loginPage(this.adminServer.getContextPath() + "/login").successHandler(successHandler).and()
.logout().logoutUrl(this.adminServer.getContextPath() + "/logout").and()
.rememberMe().key(UUID.randomUUID().toString()).tokenValiditySeconds(1209600);
//.httpBasic(Customizer.withDefaults())
}
// Required to provide UserDetailsService for "remember functionality"
// 사전에 설정한 security user, pw 사용
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser(security.getUser().getName())
.password("{noop}" + security.getUser().getPassword()).roles("USER");
}
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
this.adminServer.getContextPath()
의 default
값은 공백이지만 proxy
환경이나 별도의 Boot Admin
을 위한 context path
를 지정하고 싶을때 spring.boot.admin.context-path
를 사용하여 변경 가능하다.
Boot Admin
의 경우 각종 메트릭 정보를 Boot Client
의 actuator
로부터 가져오는데
Boot Client
에서도 아무나 actuator 정보를 가져가지 못하도록 Security 를 설정해야 한다.
필자의 경우 Boot Client
애서 Filter
를 통해 헤더에 x-api-key
가 들어있을 경우 매트릭 정보를 얻기 위한 접근으로 인식하도록 할 예정이기에 HttpHeadersProvider
Bean
을 등록해 Boot Admin
의 모든 요청에 헤더를 심어서 요청하도록 할 예정이다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${spring.security.user.password}")
private String xApiKey;
@Bean
public HttpHeadersProvider customHttpHeadersProvider() {
return instance -> {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("x-api-key", xApiKey);
return httpHeaders;
};
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedHeaders("*")
.allowedMethods("*")
.maxAge(-1) // add maxAge
.allowCredentials(false);
}
}
Admin Client
Boot Admin
에서 지정한 security 계정, actuator
에 대한 설정, log 를 볼수 있도록 logfile
에 대해 설정한다.
# --------------- profile
server.port=8082
management.endpoint.health.enabled=true
management.endpoints.web.base-path=/actuator
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
spring.profiles.active=dev
spring.application.name=store
application.api.key=1q2w3e4r
# --------------- Spring Boot Admin
spring.boot.admin.client.url=http://localhost:8080
spring.boot.admin.client.username=admin
spring.boot.admin.client.password=admin
logging.file.name=${HOME}/demo/logs/${spring.application.name}/application.log
위에서 말했던 대로 actuator
의 모든 endpoint
를 security
로 보호되며
정의한 RequestFilter
에 의해 x-api-key
헤더가 있을경우 Boot Admin
의 요청으로 인식하도록 설정한다.
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${application.api.key}")
private String apiKey;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors().configurationSource(corsConfigurationSource()).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.anyRequest().authenticated().and()
.addFilterAfter(new RequestFilter(apiKey), BasicAuthenticationFilter.class);
}
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList("*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(false);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
public class RequestFilter extends OncePerRequestFilter {
private static final String API_KEY = "api-key";
private final String apiKey;
public RequestFilter(String apiKey) {
this.apiKey = apiKey;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
// x-api-key 를 가지고 있을 경우 system 호출
final String apiKey = request.getHeader(API_KEY);
if (apiKey != null && apiKey.equals(this.apiKey)) {
Collection<GrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_SYSTEM"));
Authentication authentication = new UsernamePasswordAuthenticationToken("admin", "admin", authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
return;
} else {
String msg = "api key does not exist";
request.setAttribute("exception", msg);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
}
Spring Boot Amdin + Proxy
Nginx
나 Aws 환경에서 Boot Admin
프로젝트를 사용할 경우 Proxy
환경에서 운영하게 될 수 있는데
Boot Admin
의 경우 관련 UI 관련 리소스 파일을 가져오거나 로그인 후 redirect
요청등을 수행했을 때 되지 않는 경우가 대부분이다.
로그인 핸들러의 successHandler.setDefaultTargetUrl
함수의 redirect url
을 주의하여 삽입하고 각종 UI 리소스 파일의 경로, proxy 조건에 따라 context path
를 사용할지 안할지를 잘 지정해서 Boot Admin
을 구축해야한다.
spring.boot.admin.ui.public-url
UI를 위한 js
, css
등의 리소스 파일을 가져올때 spring.boot.admin.ui.public-url
속성을 설정하지 않을 경우
spring boot admin
이 동작하고있는 서버의 url
을 가져오기 때문에 proxy
서버에서 사용되는 url
과 다를 수 있다.
# default "/"
spring.boot.admin.context-path=/admin
spring.boot.admin.ui.public-url=https://custom.domain.com/admin
데모 코드
https://github.com/Kouzie/spring-boot-demo/tree/main/admin-demo