- 웹 요청이 어떤 프로토콜로 (http or https) 전달되어야 하는지 처리
- 전송 레이어 보안을 위해 SSL 인증서를 생성하고, 이를 Spring Boot 웹 어플리케이션에 적용한다. 이제 웹 어플리케이션은 HTTPS 프로토콜을 통해 서비스 된다.
HTTP와 HTTPS
- HTTP(Hyper Text Transfer Protocol)는 인터넷상에서 데이터를 주고 받기 위한 프로토콜
- 클라이언트와 서버가 주고 받는 데이터는 암호화되어 있지 않음
- 따라서, 악의적인 데이터 감청, 데이터 변조의 가능성이 있음
- HTTPS(HyperT ext Transfer Protocol Secure)는 HTTP 프로토콜의 암호화 버전
- 클라이언트와 서버가 주고 받는 모든 데이터는 암호화되어 있음
- 데이터 암호화를 위해 SSL(Secure Sockets Layer)을 사용
SSL은 Netscape가 개발했으며 SSL 3.0부터 TLS라는 이름으로 변경되었다. 일반적으로 SSL, TLS은 같은 의미를 지닌다. 그러나 SSL이란 용어가 더 많이 사용된다.
- SSL 암호화를 위해 SSL 인증서가 필요함
- 서버는 SSL인증서를 클라이언트에 전달함
- 클라이언트는 서버가 전달한 SSL 인증서를 검증하고, 신뢰할 수 있는 서버인지 확인함
- 신뢰할 수 있는 서버라면 SSL 인증서의 공개키를 이용해 실제 데이터 암호화에 사용될 암호화키를 암호화하여 서버에 전달함
- 실제 데이터 암복호화는 대칭키 방식
- 서버와 클라이언트 사이의 대칭키 공유를 위해 RSA 암호화를 사용함
SSL 인증서 생성
- keytool 도구를 이용해 임의로 SSL 인증서를 생성할 수 있음 (keytool은 Java 설치 경로 bin 디렉토리 아래에 위치함)
- 물론 실제 서비스에는 사용할 수 없으며, 어디까지나 로컬 테스트 용도로만 활용해야 함

- keystore 만들기
- keytool -genkey -alias [keystore 별칭] -keyalg RSA -storetype PKCS12 -keystore [keystore 파일]
iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ keytool -genkey -alias prgrms_keystore -keyalg RSA -storetype PKCS12 -keystore prgrms_keystore.p12 Enter keystore password: Re-enter new password: What is your first and last name? [Unknown]: localhost What is the name of your organizational unit? [Unknown]: Prgrms What is the name of your organization? [Unknown]: Prgrms What is the name of your City or Locality? [Unknown]: Seoul What is the name of your State or Province? [Unknown]: Seoul What is the two-letter country code for this unit? [Unknown]: KR Is CN=localhost, OU=Prgrms, O=Prgrms, L=Seoul, ST=Seoul, C=KR correct? [no]: y iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ ls prgrms_keystore.p12
- keystore 에서 인증서 추출하기
- keytool -export -alias [keystore 별칭] -keystore [keystore 파일] -rfc -file [인증서 파일]
iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ keytool -export -alias prgrms_keystore -keystore prgrms_keystore.p12 -rfc -file prgrms.cer Enter keystore password: Certificate stored in file <prgrms.cer> iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ ls prgrms.cer prgrms_keystore.p12
- trust-store 만들기
- keytool -import -alias [trust keystore 별칭] -file [인증서 파일] -keystore [trust keystore 파일]
iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ keytool -import -alias prgrms_truststore -file prgrms.cer -keystore prgrms_truststore.p12 Enter keystore password: Re-enter new password: Owner: CN=localhost, OU=Prgrms, O=Prgrms, L=Seoul, ST=Seoul, C=KR Issuer: CN=localhost, OU=Prgrms, O=Prgrms, L=Seoul, ST=Seoul, C=KR Serial number: 16cd5188 Valid from: Thu Aug 19 19:37:07 KST 2021 until: Wed Nov 17 19:37:07 KST 2021 Certificate fingerprints: MD5: 26:91:CD:3D:BC:B9:2E:C7:6B:23:2C:B0:3C:DF:E2:BB SHA1: 3E:85:57:2A:7B:51:2B:20:5A:F8:FB:92:41:87:6C:41:A4:1E:01:A5 SHA256: 63:AD:A4:85:49:08:B7:01:75:36:34:A6:02:B6:2A:9B:1F:16:C0:5D:63:CE:F2:66:68:71:65:6E:31:1E:4B:D6 Signature algorithm name: SHA256withRSA Subject Public Key Algorithm: 2048-bit RSA key Version: 3 Extensions: #1: ObjectId: 2.5.29.14 Criticality=false SubjectKeyIdentifier [ KeyIdentifier [ 0000: DB 81 03 CF 01 A9 25 34 70 46 F4 FF EF 8D BA 3D ......%4pF.....= 0010: 24 C7 3B 6C $.;l ] ] Trust this certificate? [no]: y Certificate was added to keystore iyboklee@DESKTOP-6I1BVIA:/mnt/c/Users/iybok/stores$ ls prgrms.cer prgrms_keystore.p12 prgrms_truststore.p12
TrustStore vs KeyStore
- 간단히 말하자면 trustStore는 다른 사람을 식별하는 식별 인증서를 보유하고 keyStore는 본인을 포함하는 식별인증서를 가지고 있는 곳이라고 생각하면 됨
TrustStore와 KeyStore의 차이점은 아래 표 내용에 나와 있습니다.
TrustStore에는 개인 정보와 민감한 정보가 포함되어 있지 않음 | keyStore에는 개인 정보와 민감한 정보가 포함되어 있습니다 |
javax.net.ssl.trustStore는TrustStore를 지정하는 데 사용됩니다. | javax.net.ssl.keyStore는키 저장소를 지정하는 데 사용됩니다. |
클라이언트 측에서 성공적인 연결을 위해서는 TrustStore 설정이 필요함 | SSL에서 서버 측을 설정할 때 keyStore가 필요함 |
TrustStore는 다른 사람의 자격 증명을 저장함 | keyStore는 본인의 자격 증명을 저장함 |
TrustStore는 신뢰하는 외부 시스템의 인증서를 보유함 | KeyStore는 애플리케이션의 인증서를 보유함 |
TrustStore 및 TrustStore 암호는 일반 파일에 저장되며 모두가 볼 수 있음 | 키 저장소 및 키 암호는 해당 그룹의 구성원만 읽을 수 있는 파일에 일반 텍스트로 저장됨 |
SSL 인증서 적용
- prgrms_keystore.p12, prgrms_truststore.p12 2개 파일을 resources 디렉토리로 복사 후 application.xml 파일에 설정 추가
- 포트를 443으로 변경 (HTTPS 기본 포트)
- server.ssl 설정 추가
spring: application: name: spring security 01 autoconfigure: exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration thymeleaf: cache: true security: user: name: user password: user123 roles: USER messages: basename: i18n/messages encoding: UTF-8 cache-duration: PT1H server: port: 443 ssl: enabled: true key-alias: prgrms_keystore key-store: classpath:prgrms_keystore.p12 key-store-password: prgrms123 key-password: prgrms123 trust-store: classpath:prgrms_truststore.p12 trust-store-password: prgrms123
- 웹 어플리케이션을 시작하면 443 (https) 포트를 통해 서비스가 기동하는 것을 로그로 확인할 수 있음

- 웹 브라우저 주소에 https://localhost 를 입력
- 정상적으로 페이지 접근을 확인할 수 있음
- 로그인/로그아웃도 정상적으로 수행 가능
- SSL 인증서가 유효하지 않기 때문에 경고가 뜸 (유효한 인증서라면 경고가 뜨지 않음)
- 브라우저 마다 경고가 다를 수 있음 (크롬의 경우 HTTPS 연결이 사용되지 않았다는 경고 메시지 발생)

Spring Security 설정하기
- HttpSecurity 클래스를 통해 ChannelProcessingFilter 세부 설정을 할 수 있음
@Override protected void configure(HttpSecurity http) throws Exception { http /** * HTTP 요청을 HTTPS 요청으로 리다이렉트 anyRequest에 대해 HTTPS가 필요하다고 말하는것(requiresSecure) requiresInsecure()로 하면 http 요청으로도 들어갈 수 있음 */ .requiresChannel() .anyRequest().requiresSecure() ; }
- ChannelProcessingFilter 설정을 통해 HTTPS 채널을 통해 처리해야 하는 웹 요청을 정의할 수 있음
FilterInvocationSecurityMetadataSource
클래스에 HTTPS 프로토콜로 처리해야 할 URL 정보가 담김- 실제적인 처리를 ChannelDecisionManager 클래스로 위임함
public class ChannelProcessingFilter extends GenericFilterBean { private ChannelDecisionManager channelDecisionManager; private FilterInvocationSecurityMetadataSource securityMetadataSource; // ...생략... @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; FilterInvocation filterInvocation = new FilterInvocation(request, response, chain); Collection<ConfigAttribute> attributes = this.securityMetadataSource.getAttributes(filterInvocation); if (attributes != null) { this.logger.debug(LogMessage.format("Request: %s; ConfigAttributes: %s", filterInvocation, attributes)); /* 아래 부분이 ChannelDecisionManager로 위임하는 부분 */ this.channelDecisionManager.decide(filterInvocation, attributes); if (filterInvocation.getResponse().isCommitted()) { return; } } chain.doFilter(request, response); } // ...생략... }