요즘 프로젝트를 하고 있는데, 역시나 자바 시스템이 있다. Spring Boot3 을 사용하고 있고 자바 17을 쓰는등 나름대로 괜찮은 환경에서 개발이 이루어지고 있다. 그런데, 이것을 서버에서 배포를 하고 Spring Boot 를 실행해야 하는데, 어떻게 하나 봤더니 초보자 수준도 못 벗어나는 설정을 하고 있으니… 안타까운 마음에 어떻게 하는 것이 좋은 것인지 한번 적어봤다.
Spring boot, jar 실행 파일
Spring Boot3 를 컴파일 하면 jar 파일 나온다. 그리고 별다른 서버 없이도 바로 실행하고 접속이 가능해 진다. 한가지 재미있는 사실은 많은 사람들이 Spring boot3 에 실행 파일 jar 이 내장된 WAS 서버가 구동되면서 실행된다는 걸 모른다는 거다. 심지여 그것이 Tomcat 이라는 것도. 물론 어떤 프로그래밍 모델인지에 따라서 Tomcat 이 되기도 하고 Netty 되기도 하지만, 과연 Reactive Programming Model 로 짜는 사람이 몇이나 있나 싶다. 대부분 Spring MVC 이고 Servlet 이면 기본적으로 Tomcat 이 구동된다. 내장형 Tomcat
이 글은 Spring5 에 Spring-Security 에 대한 기초 템플릿이다. Session 유지를 위해서 Redis 를 필요로 한다. 또, ElasticSearch 의 RestHighLevelClient 로 ElasticSearch를 연결 한다. 데이터베이스는 JNDI 설정으로 연결 된다.
다음과 같은 내용을 담았다.
JDK 11
Tomcat 서버 9 를 이용.
spring-session.xml 을 작성해 Redis 를 세션으로 사용하도록 했다.
JNDI 를 이용해 MySQL 에 연결된다.
Redis 설정한 이유는 Spring Security 에 인증을 InMemory 를 요구 한다. 이는 다음과 같은 설정 때문이다.
spring-security.xml 에 bcrypt 패스워드 설정
XHTML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<authentication-manager>
<authentication-provider>
<user-service>
<!-- This is required a InMemory DB. So I setup Redis and config to a spring-session.xml for connecting the redis
if you set spring-session.xml, you should config also web.xml for springSessionRepositoryFilter
이 문서는 Spring5 Security 기초 템플릿에 대한 것이다. Spring5 에 Spring-Security 를 이용해 기초적인 로그인을 구현 했다. Session 은 InMemory 가 아닌 WAS 서버에 세션을 저장한다. 로그인 정보는 spring-security.xml 에 정의된 것을 그대로 사용했다. 그야말로 기초적인 내용만 담았다.
이 템플릿이 동작하기 위한 조건은 다음과 같다.
Java 1.8 기반.
데이터베이스가 연결되어 있어야 한다. 데이터베이스는 MySQL 이다.
WAS 서버에 JNDI 를 연결된다.
users 테이블이 필요한데, user.sql 파일에 내용이 있다.
spring security 의 Role 기반 권한 접근 제어.
context root 는 malluser 이다.
작동방식
권한을 체크하기 때문에 로그인 화면이 먼저 나온다. 권한 체크는 다음과 같이 Controller 에서 이루어진다.
SpringFramework 을 이용하다보면 데이터베이스 액세스를 위해서 MyBatis 를 사용하곤 한다. SQL 매퍼라고 불리기도 하는 것인데, 이를 이용하면 손쉽게 자바 코드와 SQL 문을 분리해줄 수 있을뿐만 아니라 MyBatis 에서 제공하는 여러가지 추가적인 기능을 이용해 데이터베이스를 좀 더 유연하게 사용할 수 있다.
DAO
보통 MyBatis 를 이용할대는 DAO 를 구조를 사용하곤 했다. Data Access Object 라고 불리는 것으로 말 그대로 데이터 접근을 위한 객체로서 sqlSession 객체를 이용해 데이터베이스 조회만 전담하는 객체다.
이 구조는 인터페이스와 그것을 구현한 구현체 클래스가 있어야 한다. 이를 위해서 Spring의 컨텍스트 설정을 해줘야 하는데 대략 다음과 같다.
이렇게 함으로써 Mapper 를 이용하면서도 DAO 구조를 그대로 유지할 수 있다. 문제는 DAO 에서 Mapper 인터페이스 객체를 매번 생성해 줘야 한다는 것이다. sqlSession.getMapper 를 이용해서 매번 가지고 오게 되는데, 이렇게 하는 것보다는 Spring Context 에서 빈으로 등록해 와이어로 가지고 오게 되면 DAO 가 아닌 Service 계층에서 호출이 가능하다.
UserService 에 Mybatis 의 Mapper 인터페이스를 Auto wiring 예제
Java
1
2
3
4
5
@Service
publicclassUserServiceImplimplementsUserService{
@Autowired
publicUserTableMapper userTableMapper;
하지만 이렇게 할 경우에 Mapper 인터페이스 객체 하나하나 전부 Spring Context 에서 빈으로 만들어 줘야 한다. 그래서 이렇게 하지말고 Mapper 인터페이스를 스캔하도록 할 수 있는데 이를 위해서 mybatis-spring 라이브러리 패키지와 xsd 설정을 해줘야 한다. xsd 과 설정은 Spring Context 파일에 다음과 같이 해준다.
의외로 웹 프로그래밍을 하는 사람들 조차도 HTTP 응답 헤더에 대해서 생각을 하지 않는다. HTTP 응답 헤더에 대한 생각을 하지 않는다는 것은 웹 프로그래밍을 깊게 공부한 사람이 아니라고 말할 수도 있다.
Cache Control
대부분의 사람들은 이 부분에 대해서는 잘 안다. Cache Control 헤더는 Client 에 컨텐츠를 어떻게 캐시할 것인지에 대한 제어를 할 수 있다.
하지만 고려해야할 사항이 존재한다. 만약 AWS Cloud 를 사용한다면 더 더욱 고려해야할 사항이 존재한다. AWS 의 CloudFront 는 CDN 서비스로서 컨텐츠 캐시를 기반으로 한다.
만일 특정 컨텐츠에 대해서 캐시를 하고 싶지 않다면 어떻게 해야할까? AWS 에서는 다음과 문서에 기술해 놨다.
사용자 지정 오리진 웹 서버 애플리케이션에서, Cache-Control no-cache, no-store 또는 private 명령을 CloudFront에서 캐시하지 않으려는 객체에 추가합니다. 또는 Expires 명령을 CloudFront에서 캐시하지 않으려는 객체에 추가합니다.
CloudFront에서 특정 파일 캐시를 방지하려면 어떻게 해야 합니까?
오리진(origin) 의 웹 서버나 애플리케이션에서 Cache-Control 헤더를 조작하도록 권고 하고 있다.
위는 응답 캐싱 정책을 정의하는 사용하는 헤더 지시문이다. Cache-Control 은 HTTP/1.1 사양의 일부로 정의 되었다. Expire 는 Cache-Control 이전에 사용했던 지시문이다. 현대의 대부분 브라우저는 HTTP/1.1 를 지원하지만 구형을 쓰는 곳이 있을 수 있음으로 Expire 지시문을 함께 사용한다.
no-cache 는 말그대로 캐시를 하지 말라는 것을 지시한다. 이 지시자를 쓸 경우에 매번 요청할 때마다 문서의 유효성을 검사한다. 그래서 컨텐츠가 변경된 경우에 최신 버전을 가지고 오게 된다. 컨텐츠의 변화를 감지하는 매커니즘을 가진다는 것이 매우 중요하다. 또 모든 것을 캐시를 하지 않는다. 예를들어, 비공계 개인 데이터와같은 것은 이 지시자로 캐시를 제어를 할 수 없다.
no-store 는 비공개 개인 데이터를 포함해 모든 캐시를 하지 않다록 해준다.
public, private 이라는 것도 있다. 이는 중간 캐시를 이용할 경우에 유용하다. 중간 캐시는 CDN 을 의미한다. 만일 CDN 과 같은 중간 캐시 서버가 캐시 데이터를 가지고 있기를 원하지 않는다면 private 를 써야 한다.
그리고 위 코드에 보이는과 같이 모든 것을 다 포함하도록 지시문을 사용해서는 안된다. Pragma, Expire 는 HTTP/1.0 스펙에서 유효하다. 거기다 Cache-Control 지시문에서 no-cache, no-store, max-age 를 한꺼번에 쓰는건 잘못된 것이다.
index.html 파일 안에 구조가 저렇게 되어 있다고 가정하면, 대략 다음과 같은 형태의 캐시 전략을 쓸 수 있다.
Contents
Cache-Control
index.html
Cache-Control: no-cache
common.css
Cache-Control: max-age=86400
a.js
Cache-Control: private, max-age=86400
CSS 파일은 최대 1일까지만 캐시하도록 지시했고, Javascript 파일은 CDN 에서 캐시를 하지 못하도록 하고 브라우져에서 최대 1일까지만 캐시하도록 했다. index.html 은 no-cache 를 주어서 캐시를 하지 못하게 했지만 매번 요청할 때마다 변경이 되었는지 체크하고 변경되었다면 최신판을 새로 받도록 했다.
must-revalidate 가 있는데, 강제 사항을 적용할 때 사용 한다. 이는 다음과 같이 사용한다.
cache control 헤더
1
Cache-Control:max-age=86400,must-revalidate
이 경우에 1일동안 캐시를 하고 그 이후에는 서버에 반드시 유효성검사를 하도록 강제한다. 만일 네트워크 상황으로 유효성 검사를 못할 경우에는 절대로 캐시 데이터를 사용하지 못하도록 한다.
이론을 기반으로 현실에서 사용 전략을 다음과 같이 세울 수 있다.
경우의 수
Cache-Control
금융
Cache-Control: no-store
장바구니, 결재 페이지
Cache-Control: no-store
개인정보 수정 페이지
Cache-Control: no-store
동적 웹 페이지
Cache-Control: no-cache
정적 파일
Cache-Control: max-age=86400
금융 관련된 웹 애플리케이션을 사용할 때에는 캐시를 되도록이면 하게 하면 안된다.
Nginx 와 같은 경우에 다음과 같이 HTTP Header 에 Cache-Control 을 붙일 수 있다.
Nginx 서버에 Cache Control 헤더 추가 예제
Apache
1
2
3
4
location/static{
expires1M;
add_headerCache-Control"public, must-revalidate";
}
Nginx 에서 max-age 설정은 expires 해도 된다. 이렇게 하면 구형의 Expire 지시자도 함께 지정된다.
브라우저는 content sniffing 같은 기술을 사용해 다운받을려는 컨텐츠 타입을 추정하고 특정하게 된다. 하지만 이러한 것은 XSS 와 같은 공격을 제공하는 빌미가 된다. 그래서 이것을 방지하기 위해서 다음과 같이 헤더응답을 넣는다.
X-Content-Type-Options 헤더
1
X-Content-Type-Options:nosniff
Spring Security 에서는 간단하게 설정할 수 있다.
spring security 를 이용한 X-Content-Type-Options 설정 헤더
XHTML
1
2
3
<headers>
<content-type-options />
</headers>
HTTP Strict Transport Security(HSTS)
보통 사이트에 접속하는데에 프로토콜을(HTTPS, HTTP) 붙이지 않는다. 이럴 경우 브라우저는 HTTP 로 먼저 접속해보고 서버의 설정에 따라서 HTTPS 로 리다이렉트를 하게 된다.
만일 중간에 해커가 Proxy Server 를 두게되면 클라이언트는 HTTP 로만, Proxy Server 가 이를 받아서 HTTPS 로 뒷단 서버에 연결을 하게된다. 이렇게 되면 모든 정보를 볼 수 있게 된다. 이러한 해킹 기법을 “SSL Stripping” 공격 혹은 SSL/TLS Hijacking 이라고 부른다.
HSTS 는 애초에 접속을 HTTPS 로만 하라고 강제한다. 이는 브라우저와 서버 모두 HSTS 를 모두 지원해야 한다는 제한이 있지만 2010에 HSTS 에 대한 논의로 인해서 현재에는 모든 서버와 브라우저가 지원하고 있다.
HSTS 를 지원하는 브라우저는 도메인 목록을 유지한다. HTTPS 로만 접속 해야만 하는 도메인을 가지고 있게된다. 만일 HSTS 목록에 있는 도메인 주소를 억지로 HTTP 로 접속하게 되면 서버가 리다이렉트를 하는게 아니라 HTTPS 로 다시 접속하라고 브라우저에 요청하게 된다. HSTS 는 목록에 도메인을 유지하는 시간을 지정할 수 있다.
무조건 HTTPS 로만 접속하도록 강제하기 때문에 SSL 도메인 인증서가 있는 사이트에서만 사용 가능하다.
HTML 내에 또 다른 frame 을 넣는 기술은 자주 사용 되었었다. 최근에는 사용이 줄었지만 그래도 여전히 사용되어지는 기술이다. 하지만 이러한 frame 을 이용할 경우에 그것이 변조에 취약하다는 문제가 있다.
그래서 이 frame 을 실행하지 못하도록 브라우저에게 요청할 수 있는데, 이것이 X-Frame-Options 다. 기본 값은 DENY 이지만 같은 도메인의 frame 일 경우에는 허용할 수도 있다.
Spring Security 에서는 다음과 같이 설정할 수 있다.
spring security 를 이용한 frame-options 헤더 설정
XHTML
1
2
3
4
5
<headers>
<frame-options />
혹은
<frame-options policy="SAMEORIGIN" />
</headers>
SAMEORIGIN 일 경우에는 같은 도메인의 frame 일 경우에는 허용하게 한다.
X-XSS-Protection
XSS 를 브라우저에서 실행하도록 한다. 주의해야할 것은 서버의 웹 애플리케이션에서 하는게 아니라 브라우저의 XSS 필터에서 하는 것이다. 그러니까 서버에 요청을 넣기전에 브라우저가 사전에 XSS를 한번 차단하게 하는 것이다.
Spring Security 에서는 다음과 같이 설정할 수 있다.
spring security 를 이용한 xss-protection 헤더 설정
XHTML
1
2
3
4
5
<headers>
<xss-protection />
혹은
<xss-protection block="false" />
</headers>
CONTENT-SECURITY-POLICY
웹에 자원은 동일한 도메인에서만 작동되도록 설계되었다. 하지만 자신만의 스크립트를 삽입하는 XSS 공격을 고안해 냈다. 이를 방어하기 위해서 고안된 것이 CSP(Content Security Policy) 이다. 이것을 HTTP 헤더에 정의하면 브라우저에는 이곳에서 받은 리소스만 실행허거나 렌더링하도록 강제 한다.
Spring Seuciryt 에서는 다음과 같이 설정할 수 있다.
spring security 를 이용한 content-security-policy 헤더 설정
많은 웹 애플리케이션이 Spring 을 이용해서 만들어진다. 그리고 인증을 위해서 Spring Security 를 사용한다. 하지만 대부분의 소스를 보면 위에서 언급한 기본적인 보안조차 되어 있지 안되어 있는 곳이 대부분이다. 대기업의 프로젝트에서조차도 이런것을 고려하지도 않는다.
위에서 언급한 모든 내용은 다음과 같이 Spring Security 에서 간단하게 설정할 수 있다.
spring security 를 이용한 기본 헤더 설정
XHTML
1
2
3
4
5
6
7
<headers>
<cache-control />
<content-type-options />
<hsts />
<frame-options />
<xss-protection />
</headers>
아주 간단하다. 그냥 가져다 붙여넣기만 해도 된다. 하지만 이것조차도 사용하지 않는 대기업 프로젝트가 부지기수다. 뭐가 기술력이 좋다는 건지… IT 선진국이라는 것이 좋은 인프라에 작동하기만 하면 되는 앱만으로 얻을 수 있는 거였다니..
이클립스(Eclipse)에서 STS 를 설치하면 아주 간단하게 Spring MVC 샘플 프로젝트를 생성할 수 있다. 이 샘플 프로젝트의 디렉토리는 다음과 같다.
엄밀히 말하면 사실 이 디렉토리 구조는 Maven 이 관리 한다. Maven 에서는 기본적으로 Java 소스 디렉토리, Resource 디렉토리, webapp 디렉토리를 위와 같이 정의해 놓고 있다. STS 의 Spring MVC 샘플 프로젝트도 Maven을 기반으로 만들어 졌기 때문에 위와 같은 디렉토리 구조를 가진다.
하지만 한국에서는 다음과 같은 디렉토리 구조를 주로 사용한다.
webapp 의 루트(root) 디렉토리를 WebContent 로 바꾸고 스프링의 설정 파일인 root-context.xml, servlet-context.xml 를 Resource 디렉토리의 config 디렉토리 아래로 옮겨준다.
이러한 구조로 바꾸기 위해서는 Maven 설정을 만줘져야 하는데, 이클립스를 이용하면 손쉽게 위와같은 구조로 변경할 수 있다.
WebContent 디렉토리 생성
webapp 디렉토리를 WebContent 로 바꾸기 위해서는 먼저 디렉토리를 만들어야 한다. 이클립스에서 프로젝트 아이콘에서 오른쪽 버튼을 클릭해서 WebContent 를 만들어 준다.
생성이 되었다면 이제 기존의 webapp 디렉토리 아래에 모든 디렉토리와 파일을 WebContent 디렉토리로 옮겨준다. 그리고 webapp 디렉토리를 삭제해 준다.
이렇게 옮겨 놓고 보면 pom.xml 파일이 오류가 난다. 오류의 내용은 다음과 같다.
web.xml 오류.
1
2
Description Resource Path Location Type
web.xml ismissing and<failOnMissingWebXml>isset totruepom.xml/Spring5 line8Maven Java EE Configuration Problem
Maven 에서 정해진 위치에 있어야 할 web.xml 이 없기 때문에 나오는 오류다. 따라서 Maven 에서 web.xml 디렉토리 위치를 바꿔 주면 되는데 먼저 pom.xml 에 maven-war-plugin 를 추가해 준다.
그리고 이클립스에 설정으 바꿔 줍니다. 프로젝트이름에서 마오스 오른쪽 클릭 후에 ‘Properties -> Deployment Assembly’ 를 보면 /src/main/webapp 이 보인다. 이것을 삭제하고 WebContent 를 추가해준다.
이렇게 한 후에 pom.xml 파일의 오류가 사라지는것을 볼 수 있다. 만일 그렇지 않다면 Maven Update 를 한번 해준다.
이제 Maven 을 이용해서 프로젝트를 빌드 해본다. 성공적으로 빌드가 됐다면 다음과 같이 나온다.
Spring 설정을 WebContent 와 분리
WebContent 이름에서 보이듯이 이 디렉토리는 Web 관련 파일들만 모아놓기위한 것이다. Web 관련 파일이라면 CSS, JS, Images, JSP 로 대표되는 Web Frontend 파일을 말한다. 하지만 자세히 보면 Spring 에 관련된 파일 들도 있어서 이것을 Resource 디렉토리로 옮겨 놓는게 적절해 보인다.
먼저 src/main/resources 디렉토리에 ‘spring’ 이름의 디렉토리를 생성해준다. 디렉토리를 생성했는데 패키지 아이콘이 나온다면 ‘Build Path’ 에서 Exclude 해주면 된다. 그리고 spring-root.xml, spring-context.xml 두개의 파일을 모두 resources/spring 으로 옮겨주고 기존의 WEB-INF/spring 디렉토리는 삭제해준다.