1. 스프링부트 h2console 관련 설명

스프링부트 1.3.0 부터 h2console 기능을 제공한다.

개발자 개인개발환경(로컬local)에서 개발할 때 h2database를 Database로 사용하면 개발이 매우 용이해진다. 그리고 h2database에서는 데이터베이스에 에 접근할 수 있는 클라이언트를 제공하는데 그게 바로 h2console 이다.

이전버전에서 h2console을 사용하기 위해서는 다음과 같은 형태로 ServletRegistrationBean 으로 h2에서 제공하는 WebServlet을 서블릿빈으로 등록한다.

@Bean
public ServletRegistrationBean h2servletRegistration() {
    ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet());
    registration.addUrlMappings("/h2console/*");
    return registration;
}

스프링부트 1.3.0 부터는 @WebServlet, @WebFilter @WebListener 애노테이션을 선언한 클래스들을@ServletComponentScan으로 탐색하는 기능도 제공한다.

어쨌든~~ h2console은 기본적으로 애플리케이션의 데이터에 접근하는 부분이기 때문에 보안과 관련되어 있다. 그래서 스프링부트 1.3.0에 추가된 H2ConsoleAutoConfiguration 에서 기본보안에 대한 설정을 하는 부분도 볼 수 있다.

H2ConsoleSecurityConfiguration.java
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass(WebServlet.class)
@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true", matchIfMissing = false)
@EnableConfigurationProperties(H2ConsoleProperties.class)
@AutoConfigureAfter(SecurityAutoConfiguration.class)
public class H2ConsoleAutoConfiguration {
 
@Autowired
private H2ConsoleProperties properties;
 
@Bean
public ServletRegistrationBean h2Console() {
String path = this.properties.getPath();
String urlMapping = (path.endsWith("/") ? path + "*" : path + "/*");
return new ServletRegistrationBean(new WebServlet(), urlMapping);
}
 
@Configuration
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnProperty(prefix = "security.basic", name = "enabled", matchIfMissing = true)
static class H2ConsoleSecurityConfiguration {
 
@Bean
public WebSecurityConfigurerAdapter h2ConsoleSecurityConfigurer() {
return new H2ConsoleSecurityConfigurer();
}
 
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
private static class H2ConsoleSecurityConfigurer
extends WebSecurityConfigurerAdapter {
 
@Autowired
private H2ConsoleProperties console;
 
@Autowired
private SecurityProperties security;
 
@Override
public void configure(HttpSecurity http) throws Exception {
String path = this.console.getPath();
String antPattern = (path.endsWith("/") ? path + "**" : path + "/**");
HttpSecurity h2Console = http.antMatcher(antPattern);
h2Console.csrf().disable();
h2Console.httpBasic();
h2Console.headers().frameOptions().sameOrigin();
String[] roles = this.security.getUser().getRole().toArray(new String[0]);
SecurityAuthorizeMode mode = this.security.getBasic().getAuthorizeMode();
if (mode == null || mode == SecurityAuthorizeMode.ROLE) {
http.authorizeRequests().anyRequest().hasAnyRole(roles);
}
else if (mode == SecurityAuthorizeMode.AUTHENTICATED) {
http.authorizeRequests().anyRequest().authenticated();
}
}
 
}
 
}
 
}

2. h2console을 사용하기 위한 설정

h2console을 사용하는 방법은 간단하다. application.yml(혹은 application.properties) 파일에spring.h2.console.* 속성을 정의하면 된다. 여기서 정의하는 속성은

H2ConsoleProperties.java
@ConfigurationProperties(prefix = "spring.h2.console")
public class H2ConsoleProperties {
 
/**
 * Path at which the console will be available.
 */
@NotNull
@Pattern(regexp = "/[^?#]*", message = "Path must start with /")
private String path = "/h2-console";
 
/**
 * Enable the console.
 */
private boolean enabled = false;
 
public String getPath() {
return this.path;
}
 
public void setPath(String path) {
this.path = path;
}
 
public boolean getEnabled() {
return this.enabled;
}
 
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
 
}

spring.h2.console.* 가 제공하는 속성은 크게 두가지다.

# H2 Web Console (H2ConsoleProperties)
spring.h2.console.enabled=false # Enable the console.
spring.h2.console.path=/h2-console # Path at which the console will be available.

위의 속성에서 볼 수 있듯이 h2console은 기본은 비활성화 되어 있다.

spring.h2.console.enabled=true
spring.h2.console.path=/h2console

로 설정하면 h2console 사용을 위한 기본준비는 끝난다.

스프링부트 애플리케이션을 실행시키면 * h2console 로그인창이 뜨고 ** http://localhost:8080/h2console

h2console-loging

h2console

  • h2console 창이 뜨면 정상적으로 접근이 된 것이다.

h2console-success

h2console-success

3. h2console 로그인을 통과했는데 흰화면만 나온다면?

그런데 스프링시큐리티를 사용하고 있다면 흰화면만 나오고 화면이 뜨지 않을 수도 있다. 브라우저의 개발도구를 열어 콘솔창을 보면 다음과 같은 메시지를 볼 수 있다.

h2console-white

h2console-whitescreen

Load denied by X-Frame-Options: http://localhost:9090/h2console/header.jsp?jsessionid=62e96686014c88a9e644647c7a4bf069 does not permit framing. <알 수 없음>
Load denied by X-Frame-Options: http://localhost:9090/h2console/query.jsp?jsessionid=62e96686014c88a9e644647c7a4bf069 does not permit framing. <알 수 없음>
Load denied by X-Frame-Options: http://localhost:9090/h2console/help.jsp?jsessionid=62e96686014c88a9e644647c7a4bf069 does not permit framing. <알 수 없음>
Load denied by X-Frame-Options: http://localhost:9090/h2console/tables.do?jsessionid=62e96686014c88a9e644647c7a4bf069 does not permit framing. <알 수 없음>

스프링시큐리티에서 headers 에 있는 X-Frame-Options 옵션을 거부했기 때문에 발생한다.

그래서 스프링시큐리티 설정부분에서 아래와 같이 설정해버렸다.

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                //중략 
                .anyRequest().authenticated()
            .and()
                .headers()
                    .addHeaderWriter(new StaticHeadersWriter("X-Content-Security-Policy","script-src 'self'"))
                    .frameOptions().disable();
    }

개발하는 애플리케이션의 보안 정책에 따라서 상세하게 설정하자.

위와 같은 형태로 X-Frame-Options 옵션을 비활성화하면 h2console 화면에 접근이 가능하다.


1. 발생증상

IO Exception: "java.io.IOException: Block not found in id [1, -13, 23, -57, 1] [1.4.188/50]"; "lob: null table: -3 id: 5868" [90031-188] 90031/90031 (Help)
org.h2.jdbc.JdbcSQLException: IO Exception: "java.io.IOException: Block not found in id [1, -13, 23, -57, 1] [1.4.188/50]"; "lob: null table: -3 id: 5868" [90031-188]
    at org.h2.message.DbException.getJdbcSQLException(DbException.java:345)
    at org.h2.message.DbException.get(DbException.java:168)
    at org.h2.message.DbException.convertIOException(DbException.java:330)
    at org.h2.value.ValueLobDb.getString(ValueLobDb.java:306)
    at org.h2.jdbc.JdbcResultSet.getString(JdbcResultSet.java:285)
    at org.h2.server.web.WebApp.escapeData(WebApp.java:1773)
    at org.h2.server.web.WebApp.getResultSet(WebApp.java:1686)
    at org.h2.server.web.WebApp.getResult(WebApp.java:1405)
    at org.h2.server.web.WebApp.query(WebApp.java:1061)
    at org.h2.server.web.WebApp.query(WebApp.java:1037)
    at org.h2.server.web.WebApp.process(WebApp.java:223)
    at org.h2.server.web.WebApp.processRequest(WebApp.java:170)
    at org.h2.server.web.WebServlet.doGet(WebServlet.java:125)
    at org.h2.server.web.WebServlet.doPost(WebServlet.java:162)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:707)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:790)
    at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:808)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1669)
    at org.eclipse.jetty.websocket.server.WebSocketUpgradeFilter.doFilter(WebSocketUpgradeFilter.java:224)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.boot.actuate.autoconfigure.EndpointWebMvcAutoConfiguration$ApplicationContextHeaderFilter.doFilterInternal(EndpointWebMvcAutoConfiguration.java:295)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:330)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:118)
    at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:84)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:113)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:103)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:113)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:154)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:45)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.www.BasicAuthenticationFilter.doFilter(BasicAuthenticationFilter.java:150)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:199)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:57)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:87)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:50)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:160)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:77)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.boot.actuate.trace.WebRequestTraceFilter.doFilterInternal(WebRequestTraceFilter.java:102)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:85)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.springframework.boot.actuate.autoconfigure.MetricsFilter.doFilterInternal(MetricsFilter.java:69)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1652)
    at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:585)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:143)
    at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:577)
    at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:223)
    at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1127)
    at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:515)
    at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:185)
    at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1061)
    at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141)
    at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:97)
    at org.eclipse.jetty.server.Server.handle(Server.java:499)
    at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:310)
    at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:257)
    at org.eclipse.jetty.io.AbstractConnection$2.run(AbstractConnection.java:540)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:635)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$3.run(QueuedThreadPool.java:555)
    at java.lang.Thread.run(Thread.java:745)
Caused by: java.io.IOException: Block not found in id [1, -13, 23, -57, 1] [1.4.188/50]
    at org.h2.mvstore.StreamStore$Stream.read(StreamStore.java:466)
    at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:283)
    at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:325)
    at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:177)
    at java.io.InputStreamReader.read(InputStreamReader.java:184)
    at java.io.BufferedReader.fill(BufferedReader.java:154)
    at java.io.BufferedReader.read1(BufferedReader.java:205)
    at java.io.BufferedReader.read(BufferedReader.java:279)
    at org.h2.util.IOUtils.copyAndCloseInput(IOUtils.java:200)
    at org.h2.util.IOUtils.readStringAndClose(IOUtils.java:307)
    at org.h2.value.ValueLobDb.getString(ValueLobDb.java:296)
    ... 79 more
Caused by: java.lang.IllegalStateException: Block 199 not found [1.4.188/50]
    at org.h2.mvstore.DataUtils.newIllegalStateException(DataUtils.java:773)
    at org.h2.mvstore.StreamStore.getBlock(StreamStore.java:388)
    at org.h2.mvstore.StreamStore$Stream.nextBuffer(StreamStore.java:503)
    at org.h2.mvstore.StreamStore$Stream.read(StreamStore.java:460)
    ... 89 more

2. 설정

  • 현재 H2Database 옵션은 다음과 같다.

jdbc:h2:file:~/.h2database/db;CACHE_SIZE=10240;DB_CLOSE_DELAY=-1;AUTO_SERVER=TRUE;DB_CLOSE_ON_EXIT=FALSE;MVCC=TRUE;LOCK_TIMEOUT=15000;

인터넷을 찾아보면 MVCC=TRUE 옵션을 사용하지 말라는 이야기가 있지만, 현재 사용하고 있는 Qaurtz Scheduler DBStore 옵션에서는 MVCC=TRUE 옵션을 권장하고 있기에 문제를 찾기 위한 방법을 모색했다.

3. 해결방법

h2database의 lob으로 설정한 컬럼을 null로 저장되지 않도록 처리했다.

이후 추이를 지켜보도록 한다.


SpringBoot으로 구현한 애플리케이션의 DB를 H2Database(http://h2database.com)를 사용한다.

물론 다른 것을 사용할 수도 있다. 개발하고 있는 제품의 특성에 맞춰 h2Database를 선택했을 뿐.

h2database에 접속할 수 있는 웹콘솔로 h2console이 제공되는데, 이 녀석을 애플리케이션에서
함께 실행할 수 있는 방법을 찾고 있었다.

그러다가 찾은 정보가!

spring boot default H2 jdbc connection (and H2 console)

h2database 에 포함된 org.h2.server.web.WebServlet을 등록하면 콘솔을 빈으로 등록하고 웹상으로 접근이 가능한 것이다!

@Bean
public ServletRegistrationBean h2servletRegistration() {
    ServletRegistrationBean registration = new ServletRegistrationBean(new WebServlet());
    registration.addUrlMappings("/h2console/*");
    return registration;
}

위처럼 빈Bean 선언을 하고 실행을 하면! localhost:{port}/h2console으로 접근하면!



JDBC URL은 database 설정에서 url을 복사해서 넣으면 된다. 테스트하려면 jdbc:h2:mem:test

의 모습을 볼 수 있다.

애플리케이션에서는 스프링시큐리티를 이용하여 접근제어를 하고 있다.

로그인을 하면 다음과 같은 백지화면이 나타난다. 그 이유를 찾아보려고 개발자도구를 열어보니



와 같은 메시지가 뜨는 것을 확인한다. 재빨리 검색을 들어가서!

Content-Security-Policy Spring Security - stackoverflow

스프링시큐리티 설정코드 부분에

@Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      // ...
      .headers()
        .addHeaderWriter(new StaticHeadersWriter("X-Content-Security-Policy","script-src 'self'"))
      // ...
  }

다음과 같이 "X-Content-Security-Policy" 선언을 해줘야 한다. 애플리케이션을 재시작하면!



참 쉽죠잉??


현재 개발중인 프로젝트는 SpringBoot를 사용하여 Jetty를 내장형 웹컨테이너로 사용하고 데이터베이스는 H2Database를 사용하고 있다.
H2Database를 파일DB형식으로 사용하도록 정의했다.

spring:
  datasource:
    initialize: false
    driverClassName: org.h2.Driver
    url: jdbc:h2:file:./h2database
    username: sa
    password:

웹애플리케이션을 실행하면 기본설정에 의해서 DB가 잠기게 된다.
그런데 입력된 데이터를 보겠다고 h2console을 실행하면, 웹애플리케이션에 의해 잠긴 DB는 완전히 잠기면서 더이상이 데이터입력이 되지 않는다.
이게 이상하다고 해서…
H2Database를 Server 모드로 실행하도록 했다.

spring:
  datasource:
    initialize: false
    driverClassName: org.h2.Driver
    url: jdbc:h2:file:./h2database;AUTO_SERVER=TRUE
    username: sa
    password:

와 같이 변경했다.
굳이 h2console을 실행하지 않고 이클립스에서 찾는 방법도 확인했다. 외부에서 연결할 떄는 외부연결에 의해서 파일이 잠기지 않도록

url: jdbc:h2:file:./h2database;FILE_LOCK=NO

과 같이 설정하기 바란다.

참고문헌


+ Recent posts