[Spring] RequestCacheAwareFilter, ChannelProcessingFilter 사용하기
RequestCacheAwareFilter
FilterChainProxy를 구성하는 주요 Security Filter 중 하나로 로그인 성공 이후 가로채어진 사용자의 원래 요청으로 이동하기 위해 사용된다.
익명의 사용자(로그인 안한 사용자)가 보호받는 리소스 페이지에 접근하는 경우
- 접근 권한이 없기 때문에 AccessDecisionManager에서 접근 거부 예외가 발생함
- ExceptionTranslationFilter 접근 거부 예외를 처리함
- 현재 사용자가 익명 사용자라면, 보호받는 리소스로의 접근을 캐시 처리하고, 로그인 페이지로 이동시킴
- 로그인이 완료되면 원래 접근하려 했던 캐싱 페이지로 접근할 수 있게 함
private void handleAccessDeniedException(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, AccessDeniedException exception) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = this.authenticationTrustResolver.isAnonymous(authentication);
if (isAnonymous || this.authenticationTrustResolver.isRememberMe(authentication)) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Sending %s to authentication entry point since access is denied",
authentication), exception);
}
sendStartAuthentication(request, response, chain,
new InsufficientAuthenticationException(
this.messages.getMessage("ExceptionTranslationFilter.insufficientAuthentication",
"Full authentication is required to access this resource")));
}
else {
if (logger.isTraceEnabled()) {
logger.trace(
LogMessage.format("Sending %s to access denied handler since access is denied", authentication),
exception);
}
this.accessDeniedHandler.handle(request, response, exception);
}
}
protected void sendStartAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
AuthenticationException reason) throws ServletException, IOException {
// SEC-112: Clear the SecurityContextHolder's Authentication, as the
// existing Authentication is no longer considered valid
SecurityContextHolder.getContext().setAuthentication(null);
this.requestCache.saveRequest(request, response);
this.authenticationEntryPoint.commence(request, response, reason);
}
- RequestCacheAwareFilter를 통해 위에서 살펴본 캐시된 요청을 처리할 수 있음
- 캐시된 요청이 있다면 캐시된 요청을 처리하고, 캐시된 요청이 없다면 현재 요청을 처리함
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest wrappedSavedRequest = this.requestCache.getMatchingRequest((HttpServletRequest) request,
(HttpServletResponse) response);
chain.doFilter((wrappedSavedRequest != null) ? wrappedSavedRequest : request, response);
}
ChannelProcessingFilter
전송 레이어 보안을 위해 SSL 인증서를 생성하고, 이를 Spring Boot 웹 어플리케이션에 적용한다. 이를 통해 웹 어플리케이션은 HTTPS 프로토콜을 통해 서비스 된다.
HTTP와 HTTPS
HTTP(Hyper Text Transfer Protocol)
- 인터넷 상에서 데이터를 주고 받기 위한 프로토콜
- 클라이언트와 서버가 주고 받는 데이터는 암호화되어 있지 않음
- 따라서 악의적인 데이터 감청, 데이터 변조의 가능성이 있음
HTTPS(Hyper Text 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 파일]
$ keytool -genkey -alias prgrms_keystore -keyalg RSA -storetype PKCS12 -keystore prgrms_keystore.p12
키 저장소 비밀번호 입력:
새 비밀번호 다시 입력:
이름과 성을 입력하십시오.
[Unknown]: localhost
조직 단위 이름을 입력하십시오.
[Unknown]: prgrms
조직 이름을 입력하십시오.
[Unknown]: prgrms
구/군/시 이름을 입력하십시오?
[Unknown]: Seoul
시/도 이름을 입력하십시오.
[Unknown]: Seoul
이 조직의 두 자리 국가 코드를 입력하십시오.
[Unknown]: KR
CN=localhost, OU=prgrms, O=prgrms, L=Seoul, ST=Seoul, C=KR이(가) 맞습니까?
[아니오]: y
keystore에서 인증서 추출하기
keytool -export -alias [keystore 별칭] -keystore [keystore 파일] -rfc -file [인증서 파일]
$ keytool -export -alias prgrms_keystore -keystore prgrms_keystore.p12 -rfc -file prgrms.cer
키 저장소 비밀번호 입력:
인증서가 <prgrms.cer> 파일에 저장되었습니다.
trust-store 만들기
keytool -import -alias [trust keystore 별칭] -file [인증서 파일] -keystore [trust keystore 파일]
$ keytool -import -alias prgrms_truststore -file prgrms.cer -keystore prgrms_truststore.p12
키 저장소 비밀번호 입력:
새 비밀번호 다시 입력:
소유자: CN=localhost, OU=prgrms, O=prgrms, L=Seoul, ST=Seoul, C=KR
발행자: CN=localhost, OU=prgrms, O=prgrms, L=Seoul, ST=Seoul, C=KR
일련 번호: 37c3cb60
적합한 시작 날짜: Sat Sep 03 13:23:01 KST 2022 종료 날짜: Fri Dec 02 13:23:01 KST 2022
인증서 지문:
SHA1: 72:85:A2:F5:C6:A3:F7:96:E0:2B:69:C6:44:7A:0F:6B:3A:0B:19:6E
SHA256: A1:47:86:97:DE:53:93:80:53:99:9C:01:D4:4B:3D:A1:8C:A8:00:17:02:84:4A:80:66:40:1D:41:82:F9:75:30
서명 알고리즘 이름: SHA256withRSA
주체 공용 키 알고리즘: 2048비트 RSA 키
버전: 3
확장:
#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 29 E6 C4 87 D9 A6 B5 CA 59 89 A3 DC 91 34 9E 0D ).......Y....4..
0010: 0F 33 2E 5B .3.[
]
]
이 인증서를 신뢰합니까? [아니오]: y
인증서가 키 저장소에 추가되었습니다.
SSL 인증서 적용
prgrms_keystore.p12, prgrms_truststore.p12 2개 파일을 resources 디렉토리로 복사 후 application.xml 파일에 설정 추가
- port를 443으로 변경 (https 프로토콜의 디폴트 포트가 443)
- server.ssl 설정 추가
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 설정하기
ChannelProcessingFilter 설정을 통해 HTTPS 채널을 통해 처리해야 하는 웹 요청을 정의할 수 있음
- FilterInvocationSecurityMetadataSource 클래스에 HTTPS 프로토콜로 처리해야 URL 정보가 담김
- 실제적인 처리를 ChannelDecisionManager 클래스로 위임함
HttpSecurity 클래스를 통해 ChannelProcessingFilter 세부 설정을 할 수 있음
@Override
protected void configure(HttpSecurity http) throws Exception {
http
/**
* HTTP 요청을 HTTPS 요청으로 리다이렉트
*/
.requiresChannel()
.anyRequest().requiresSecure()
;
}