Spring Security를 이용한 보안 강화를 위해 최소한의 보안 구성(V1)
H2 웹 콘솔의 화면 자체가 내부적으로 태그를 사용하기 때문에 개발환경에서는 H2 웹 콘솔을 정상적으로 사용할 수 있도록 (1)과 같이 추가
.frameOptions().sameOrigin()
호출하면 동일 출처로부터 들어오는 request만 페이지 렌더링 허용
(2) CSRF 공격에 대한 Spring Security 설정 비활성화
(3) CORS 설정 추가 .cors(withDefaults())
일 경우 corsConfigurationSource
라는 이름으로 등록된 Bean을 이용함
- CORS (Cross-Origin Resource Sharing)
애플리케이션 간에 출처(Origin)가 다를 경우 스크립트 기반의 HTTP 통신(XMLHttpRequest, Fetch API)을 통한 리소스 접근이 제한되는데, CORS는 출처가 다른 스크립트 기반 HTTP 통신을 하더라도 선택적으로 리소스에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 정책
CSR 방식에서 주로 사용하는 JSON 포맷으로 Username과 Password를 전송하는 방식을 사용할 것이므로 (4) 폼 로그인 비활성화
HTTP Basic 인증은 request를 전송할 때 마다 Username과Password 정보를 HTTP Header에 실어서 인증하는 방식. 사용하지 않으므로 (5) 비활성화
- 폼 로그인과 HTTP Basic 인증을 비활성화하면 해당 인증과 관련된 Filter가 비활성화됨.
(6)은 JWT를 적용하기 전이므로 우선은 모든 HTTP request 요청에 대해서 접근 허용 설정
(7) PasswordEncoder Bean 객체를 생성
(8) CorsConfigurationSource Bean 생성을 통해 구체적인 CORS 정책 설정
UrlBasedCorsConfigurationSource
클래스의 객체를 생성CorsConfiguration
)을 적용1. MemberDto.Post 클래스에 패스워드 필드 추가
- 회원 등록 시, 회원의 패스워드 정보를 전달 받기 위해
password
필드 추가
- 실제 서비스에서는 회원 가입 시, 사용자가 입력한 패스워드가 맞는지 재확인 하기 위해 패스워드 입력 확인 필드가 추가로 존재하는 경우가 대부분. 입력한 두 패스워드가 일치하는지를 검증하는 로직이 필요함
- 또한 패스워드의 생성규칙에 대한 유효성 검증도 실시한다.
2. Member 엔티티 클래스에 패스워드 필드 추가
- (1) Member 엔티티 클래스에 패스워드 필드 추가 (암호화되어 저장되어서 컬럼 길이 100 지정)
- (2)
@ElementCollection
애너테이션을 이용해 사용자 등록 시, 사용자의 권한 등록 위한 권한 테이블 생성
3. 사용자 등록 시, 패스워드와 사용자 권한 저장
- (1), (2)에서
PasswordEncoder
와CustomAuthorityUtils
클래스를 DI 받도록 필드 추가- (3) 패스워드를 단방향 암호화
- (4) 등록하는 사용자의 권한 정보를 생성한다.
- CustomAuthorityUtils 클래스
JwtAuthenticationFilter
)가 클라이언트의 로그인 인증 정보 수신MemberDetailsService
) 에게 사용자의 UserDetails 조회를 위임MemberDetailsService
)가 사용자의 크리덴셜 DB에서 조회 후 AuthenticationManager에게 사용자의 UserDetails를 전달JwtAuthenticationFilter
구현(2-3번), MemberDetailsService(5번)
구현1. Custom UserDetailsService 구현
데이터베이스에서 사용자의 크리덴셜을 조회한 후, 크리덴셜을 AuthenticationManager에게 전달하는 Custom UserDetailsService를 구현하는 것.
- MemberDetailsService
2. 로그인 인증 정보 역직렬화(Deserialization)를 위한 LoginDTO 클래스 생성
- LoginDTO 클래스는 클라이언트가 전송한 Username/Password를 Security Filter에서 사용할 수 있도록 역직렬화 하기 위한 클래스
3. JWT를 생성하는 JwtTokenizer 구현
- 로그인 인증에 성공한 클라이언트에게 JWT를 생성 및 발급하고 클라이언트의 요청이 들어올때마다 전달된 JWT를 검증하는 역할을 한다.
- (1)
JwtTokenizer
클래스를 Spring Container(ApplicationContext)에 Bean으로 등록하기 위해 @Component 애너테이션 추가- (2),(3),(4) 는 JWT 생성 시 필요한 정보, application.yml 파일에서 로드
- (2) JWT 생성 및 검증 시 사용되는 Secret Key 정보
- (3) Access Token에 대한 만료 시간 정보
- (4) Refresh Token에 대한 만료 시간 정보
- (5)
getTokenExpiration()
메서드는 JWT의 만료 일시를 지정하기 위한 메서드로 JWT 생성 시 사용✔ application.yml
- JWT의 서명에 사용되는 Secret Key 정보는 민감한 정보이므로 시스템 환경 변수의 변수로 등록
- ${JWT_SECRET_KEY} 는 OS의 시스템 환경 변수의 값을 읽어오는 일종의 표현식
⭐ 시스템 환경 변수에 등록한 변수를 사용할 때는 application.yml 파일의 프로퍼티명과 동일한 문자열을 사용하지 않도록 주의
시스템 환경 변수와 application.yml에 정의한 프로퍼티명의 문자열이 동일할 경우 yml 파일에 정의된 프로퍼티를 클래스의 필드에서 참조할 때 (예: ${jwt.key.secret}) 시스템 환경 변수의 값으로 채워지므로 개발자가 의도하지 않은 값으로 채워질 수 있다.
가급적 시스템 환경 변수의 값도 application.yml에서 먼저 로드한 뒤에 application.yml에서 일관성 있게 프로퍼티 값을 읽어오는 것이 좋다.
🔍 맥 사용 시 환경변수 설정
1. 숨김파일 보기 설정 후 user 루트에 .zprofile 파일
2. export JWT_SECRET_KEY="xxxxxxx" 추가
3. 터미널과 인텔리제이 종료 후 다시 실행🔍 프로젝트 별로 env 파일을 만들어 환경 변수 관리하기
1. resource/env.yml 또는 resource/env.properties 파일 만들기 ( JWT키 추가)
2. 사용하고자 하는 클래스에@PropertySource
애너테이션 추가 (main메서드가 존재하는 클래스에 추가)
@PropertySource("classpath:/env.yml")
3. @Value 애너테이션 / Enviroment 객체를 통해 환경변수 가져오기
4. 깃허브에 커밋 시에는.gitignore
에 env 파일을 등록하여 공유하지 않기
4. 로그인 인증 요청을 처리하는 Custom Security Filter 구현
- 클라이언트의 로그인 인증 정보를 직접적으로 수신하여 인증 처리의 엔트리포인트 역할을 하는 필터
- (1)
UsernamePasswordAuthenticationFilter
를 상속한다. 폼 로그인 방식에서 사용하는 디폴트 Security Filter로써, 폼 로그인이 아니더라도 Username/Password 기반의 인증을 처리하기 위해 확장해서 구현할 수도 있다.- (2) AuthenticationManager와 JwtTokenizer를 DI 받는다.
- DI 받은
AuthenticationManager
는 로그인 인증 정보를 전달 받아 UserDetailsService와 인터랙션 한 뒤 인증 여부 판단- DI 받은
JwtTokenizer
는 클라가 인증에 성공할 경우, JWT를 생성 및 발급함- (3)
attmptAuthentication()
은 메서드 내부에서 인증을 시도하는 로직 구현
- (3-1) 클라이언트에서 전송한 Username과 Password를 DTO 클래스로 역직렬화 하기 위해 ObjectMapper 인스턴스 생성
- (3-2)
ServletInputstream
을LoginDto
클래스의 객체로 역질렬화 한다.- (3-3) Username과 Password 정보를 포함한 UsernamePasswordAuthenticationToken을 생성
- (3-4) UsernamePasswordAuthenticationToken을 AuthenticationManager에게 전달하여 인증 처리 위임
- (4)
successfulAuthentication()
메서드는 클라이언트의 인증 정보를 이용해 인증에 성공할 경우 호출됨.
- (4-1)
authResult.getPrincipal()
로 Member 엔티티 클래스의 객체를 얻는다.
AuthenticationManager 내부에서 인증에 성공하면 인증된 Authentication 객체가 생성되면서 principal 필드에 Member 객체가 할당된다.- (4-2) Access Token 생성
- (4-3) Refresh Token 생성
- (5), (6) Access Token과 Refresh Token을 생성하는 구체적인 로직
5. Custom Filter 추가를 위한 SecurityConfiguration 설정 추가
JwtAuthenticationFilter
구현이 되었으면 Spring Security Filter Chain에 추가해서 로그인 인증을 처리하도록 한다.- SecurityConfiguration(V2)
- Spring Security에서는 직접 Custom Configurer를 구성해 Spring Security의 Configration을 커스터마이징 할 수 있다.
- (1)
apply()
메서드에 Custom Configurer를 추가 가능- (2) Custom Configurer인
CustomFilterConfigurer
클래스. 구현한 JwtAuthenticationFilter를 등록하는 역할을 한다.
- (2-1) AbstractHttpConfigurer 를 상속해서 Custom Configurer 구현 가능
AbstractHttpConfigurer
를 상속하는 타입과HttpSecurityBuilder
를 상속하는 타입을 제너릭 타입으로 지정할 수 있다.- (2-2) configure() 메서드를 오버라이드하여 Configuration을 커스터 마이징
- (2-3) AUthenticationManager의 객체를 얻을 수 있다.
getSharedObject()
를 통해서 Spring Security의 설정을 구성하는 SecurityConfigurer간에 공유되는 객체를 얻을 수 있음.- (2-4) JwtAuthenticationFilter를 생성하면서 사용되는 AuthenticationManager와 JwtTokenizer를 DI 해준다.
- (2-5) 메서드를 통해 디폴트 request URL인
/login
을/v11/auth/login
으로 변경- (2-6) 메서드를 통해 JwtAuthenticationFilter를 Spring Security Filter Chain에 추가
AuthenticationSuccessHandler
) 를 지원하며, 인증 실패 시 추가 처리 핸들러(AuthenticationFailureHandler
)를 지원한다.1. AuthenticationSeccessHandler 구현
- Custom으로 정의하려면
AuthenticationSuccessHandler
인터페이스를 구현해야 한다.(1)onAuthenticationSuccess()
추상 메서드를 구현하여 추가 처리
(2) 로그 처리 외에도 Authentication 객체에 사용자 정보를 얻은 후, HttpServletResponse로 출력 스트림을 생성하여 response를 전송할 수 있다.2. AuthenticationFailureHandler 구현
- (1)
AuthenticationFailureHandler
인터페이스 구현onAuthenticationFailure()
추상 메서드 구현하여 추가 처리- (2) sendErrorResponse() 메서드 호출하여 출력 스트림에 Error 정보를 담고 있다.
- (2-1) Error 정보가 담긴 객체를 JSON 문자열로 변환하는데 사용되는 Gson 라이브러리의 인스턴스를 생성
- (2-2) ErrorResponse 객체 생성
HttpStatus.UNAUTHORIZED
상태 코드 전달- (2-3) response의 Content Type이
application/json
이라는 걸 클라이언트에게 알려주도록 HTTP Heeader에 추가- (2-4) response의 status가 401임을 클라이언트에게 알려줄 수 있도록 HTTP Header에 추가
- (2-5) Gson 이용해 ErrorResponse 객체를 JSON 포맷 문자열로 변환 후, 출력 스트림 생성
3. AuthenticationSuccessHandler와 AuthenticationFailureHandler 추가
- SecurityConfiguration(V3)
- (3),(4) 와 같이 JwtAuthenticationFilter에 등록
4. AuthenticationSuccessHandler 호출
- JwtAuthenticationFilter(AuthenticationSuccessHandler 호출 코드 추가)
onAuthenticationSuccess()
메서드 호출- 실패 핸들러는 알아서 호출됨.
1. JWT 검증 필터 구현
JWT를 검증하는 전용 Security Filter를 구현
(1)과 같이
OncePerRequestFilter
를 확장해서 request 당 한번만 실행되는 Security Filter를 구현할 수 있다.(2) JwtTokenizer와 CustomAuthorityUtils를 DI 받는다.
- JwtTokenizer는 JWT를 검증하고 Claims(토큰에 포함된 정보)를 얻는데 사용
- CustomAuthorityUtils는 JWT 검증에 성공하면 Authntication 객체에 채울 사용자의 권한을 생성하는데 사용됨.
(3)
verifyJws()
메서드는 JWT를 검증하는데 사용되는 private 메서드
- (3-1) request의 header에서 JWT를 얻고 있다.
여기서의 JWT는 클라이언트가 response header로 전달 받은 JWT를 request header에 추가해서 서버 측에 전송한 것.Bearer
부분 제거- (3-2) JWT 서명을 검증하기 위한 Secret Key를 얻는다.
- (3-3) JWT에서 Claims를 파싱
즉,verify()
같은 검증 메서드가 따로 존재하는것이 아니라 Claims가 정상적으로 파싱이 되면 서명 검증 역시 성공했다.(4)
setAuthenticationToContext()
메서드는 Authenrication 객체를 SecurityContext에 저장하기 위한 private 메서드
- (4-1) JWT에서 파싱한 Claims에서
username
을 얻는다.- (4-2) JWT의 Claims에서 얻은 권한 정보를 기반으로
List<GrantedAuthority>
를 생성- (4-3)
username
과List<GrantedAuthority>
를 포함한Authentication
객체 생성- (4-4) SecurityContext에
Authentication
객체 저장
- SecurityContext에 Authentication을 저장하게 되면 Spring Security의 세션 정책에 따라서 세션을 생성할 수도있고, 그렇지 않을 수도 있다.
JWT 환경에서는 세션 정책(Session Policy) 설정을 통해 세션 자체를 생성하지 않도록 설정한다.문제없이 JWT 서명 검증에 성공하고, Security Context에 Authentication을 저장한 다음에는 (5) 와 같이 다음 Security Filter를 호출한다.
(6) OncePerRequestFilter의
shouldNotFilter()
를 오버라이드 한 것, 특정 조건에 부합하면(true) 해당 Filter의 동작을 수행하지 않고 다음 Filter로 건너뛰도록 해준다.
- (6-1) Authorization header의 값을 얻은 후
- (6-2) 헤더 값이 null이거나 "Bearer"로 시작하지 않는다면 해당 Filter의 동작을 수행하지 않도록 정의한다.
- JWT가 Authorization header에 포함되지 않았다면 JWT 자격증명이 필요하지 않은 리소스에 대한 요청이라고 판단하고 다음 Filter로 처리를 넘긴다.
2. SecurityConfiguration 설정 업데이트
JwtVerificationFilter를 사용하기 위해서 두가지 설정 추가
- 세션 정책 설정 추가
- JwtVerificationFilter 추가
자격 검증에 성공하면 인증된 Authentication 객체를 SecurityContext에 저장하는데 stateless한 애플리케이션 유지를 위해 세션 유지 시간을 아주 짧게 가져가기 위한 (거의 무상태) 설정을 추가할 필요가 있다.
SecurityConfiguration(V4)
(1) 세션을 생성하지 않도록 설정한다.
(2) JwtVerificationFilter의 인스턴스를 생성하면서 사용되는 객체들을 생성자로 DI 해준다.
(3) JwtVerificationFilter가 JwtAuthenticationFilter가 수행된 바로 다음에 동작하도록 뒤에 추가한다.
MemberController를 통해 접근할 수 있는 리소스에 대한 접근 권한 부여
SecurityConfiguration(V5)
(1) 회원등록 URL 과 HTTP Method(POST) 에 해당된다면 누구나 접근 허용
(2) 회원정보 수정의 경우 (2) 일반사용자 (USER) 권한 사용자만 접근 가능
(3) 모든 회원 정보 목록은 관리자(ADMIN) 권한을 가진 사용자만 접근 가능
(4) 특정 회원 정보 조회는 일반사용자와 관리자 모두 접근 가능
(5) 특정 회원 삭제 요청은 해당 사용자가 탈퇴 처리를 할 수 있어야 하므로 일반사용자만 접근 가능
1. JwtVerificationFilter에 예외 처리 로직 추가
SignatureException
에 대해 처리ExpiredJwtException
에 대한 처리request.setAttribute("exception", Exception객체)
와 같이 HttpServletRequest의 애트리뷰트로 추가된다.AuthenticationException
이 발생한다.2. AuthenticationEntryPoint 구현
Exception 발생으로 인해 SecurityContext에 Authentication이 저장되지 않을 경우 등 AuthenticationException
이 발생할 때 호출되는 핸들러 같은 역할
인증과정에서 AuthenticationException
이 발생할 경우 호출, 처리하고자 하는 로직을 commence()
메서드에 구현하면 된다.
인증 과정에서 AuthenticationException
이 발생하면 ErrorResponse를 생성해서 클라이언트에게 전송
ErrorResponder
ErrorResponse를 출력 스트림으로 생성하는 역할
3. AccessDeniedHandler 구현
handle()
메서드에 구현4. SecurityConfiguration에 AuthenticationEntryPoint 및 AccessDeniedHandler 추가