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 디펜던시가 따로 있다.

1
2
implementation 'de.codecentric:spring-boot-admin-starter-server:2.6.6'
implementation 'de.codecentric:spring-boot-admin-starter-client:2.6.6'

Admin Server

1
2
3
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 를 통해 자동으로 보안처리 할 수 있음으로 사용 권장한다.

참고: https://www.baeldung.com/spring-boot-admin

우선 security 에 고정된 문자열로 계정을 사용한다.

1
2
3
server.port=8080
spring.security.user.name=admin
spring.security.user.password=admin

securityUI 를 위한 리소스 파일은 권한없이 접근하도록 지정하고
csrf, cors 등의 대한 처리를 진행

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@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 Clientactuator 로부터 가져오는데
Boot Client 에서도 아무나 actuator 정보를 가져가지 못하도록 Security 를 설정해야 한다.

필자의 경우 Boot Client 애서 Filter 를 통해 헤더에 x-api-key 가 들어있을 경우 매트릭 정보를 얻기 위한 접근으로 인식하도록 할 예정이기에 HttpHeadersProvider Bean 을 등록해 Boot Admin 의 모든 요청에 헤더를 심어서 요청하도록 할 예정이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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 에 대해 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# --------------- 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 의 모든 endpointsecurity 로 보호되며
정의한 RequestFilter 에 의해 x-api-key 헤더가 있을경우 Boot Admin 의 요청으로 인식하도록 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@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 과 다를 수 있다.

1
2
3
# 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