전체 소스코드
https://github.com/GHGHGHKO/Springboot/tree/main/pepega_chapter_8
이전 포스팅에서는
Spring에서 메시지를 처리하는 방법에 대해서 알아봤고
MessageSource를 이용하여 Exception Message를 고도화하였다.
swagger에서 response body의 내용을 한글, 영어로 바꾸며 도출하는 내용을 포스팅 했었다.
이번 포스팅에서는
SpringSecurity를 이용하여 api 서버의 사용 권한을 제한하는 방법에 대해 포스팅 할 것이다.
지금까지 개발한 api는 권한 부여 기능이 없어서
누구나 회원 정보를 조회하고, 생성 및 수정, 삭제를 할 수 있었다.
이 상태로 api를 서비스에 보낸다면 많은 문제가 생길 것으로 예상된다.
이러한 문제를 해결하기 위해 인증된 회원만 api를 사용할 수 있도록 개선할 것이다.
SpringSecurity
Spring에서는 인증 및 권한 부여를 통해 resource의 사용을 쉽게 컨트롤 할 수 있는
SpringSecurity framework를 제공한다.
Springboot 기반의 프로젝트에 SpringSecurity를 적용하면
보안 관련 처리를 자체적으로 구현할 필요 없이 필요한 기능을 구현할 수 있다.
간략하게 아래의 그림처럼 SpringSecurity는
Spring의 DispatcherServlet 앞단에 Filter를 등록시켜 요청을 가로챈다.
클라이언트에게 리소스 접근 권한이 없을 경우엔 인증(ex. 로그인) 화면으로 자동으로 리다이렉트 한다.
SpringSecurity Filter
SpringSecurity는 기능별 필터의 집합으로 되어있고
필터의 처리 순서는 아래와 같다.
종류가 매우 많지만 여기서 중요한 것은 필터의 처리순서다.
클라이언트가 리소스를 요청할 때 접근 권한이 없는 경우
기본적으로 로그인 폼으로 보내게 되는데
그 역할을 하는 필터는
UsernamePasswordAuthenticationFilter 이다.
Rest Api에서는 로그인 폼이 따로 없기 때문에
인증 권한이 없다는 오류 JSON을 내려줘야 하므로
UsernamePasswordAuthenticationFilter 전에 관련 처리를 넣어야 함을 알 수 있다.
1. ChannelProcessingFilter
2. SecurityContextPersistenceFilter
3. COncurrentSessionFilter
4. HeaderWriterFilter
5. CsrfFilter
6. LogoutFilter
7. X509AuthenticationFilter
8. AbstractPreAuthenticatedProcessingFilter
9. CasAuthenticationFilter
10. UsernamePasswordAuthenticationFilter
11. basicAuthenticationFilter
12. SecurityContextHolderAwareRequestFilter
13. JaasApiIntegrationFilter
14. RememberMeAuthenticationFilter
15. AnonymousAuthenticationFilter
16. SessionManagementFilter
17. ExceptionTranslationFilter
18. FilterSecurityInterceptor
19. SwitchUserFilter
API 인증 및 권한 부여, 제한된 리소스의 요청
인증을 위해 가입(Signup) 및 로그인(Signin) api를 구현한다.
가입 시 제한된 리소스에 접근할 수 있는 ROLE_USER 권한을 회원에게 부여한다.
SpringSecurity 설정에는 접근 제한이 필요한 리소스에 대해서 ROLE_USER 권한을 가져야 접근이 가능하도록 세팅한다.
권한을 가진 회원이 로그인 성공 시엔 리소스에 접근할 수 있는 Jwt 보안 토큰을 발급한다.
Jwt 보안 토큰으로 회원은 권한이 필요한 api 리소스를 요청하여 사용한다.
JWT란?
JSON Web Token (JWT)은 JSON 객체로서 당사자간에 안전하게 정보를 전송할 수 있는
작고 독립적인 방법을 정의하는 공개 표준 (RFC 7519)이다.
자세한 내용은 https://jwt.io/introduction/
Jwt는 JSON 객체를 암호화하여 만든 String 값이다.
기본적으로 암호화되어있어 변조하기 어려운 정보다.
또한, 다른 토큰과 달리 토큰 자체에 데이터를 가지고 있다.
api 서버에서는 로그인이 완료된 클라이언트에게 회원을 구분할 수 있는 값을 넣은 Jwt 토큰을 생성하여 발급한다.
클라이언트는 이 Jwt 토큰을 이용하여 권한이 필요한 resource를 서버에 요청하는데 사용할 수 있다.
api서버는 클라이언트에게서 전달받은 Jwt 토큰이 유효한지 확인하고
담겨있는 회원 정보를 확인하여 제한된 resource를 제공하는데 이용할 수 있다.
build.gradle에 library 추가
SpringSecurity 및 Jwt 관련 라이브러리를 build.gradle에 추가한다.
JwtTokenProvider 생성
Jwt 토큰 생성 및 유효성 검증을 하는 컴포넌트다.
Jwt는 여러가지 암호화 알고리즘을 제공하며 알고리즘(SignatureAlgorithm.XXXXX)과
비밀키(secretKey)로 토큰을 생성한다.
이 때 claim 정보에는 토큰에 부가적으로 실어 보낼 정보를 세팅할 수 있다.
claim 정보에 회원을 구분할 수 있는 값을 세팅하였다가
토큰이 들어오면 해당 값으로 회원을 구분하여 resource를 제공하면 된다.
그리고 Jwt 토큰에는 expire 시간을 세팅할 수 있다.
토큰 발급 후 일정 시간 이후에는 토큰을 만료시키는 데 사용할 수 있다.
resolveToken 메서드는 Http request header에 세팅된 토큰 값을 가져와 유효성을 체크한다
유저가 제한된 resource를 요청할 때 Http header에 토큰을 세팅하여 호출하면
유효성을 검증하여 사용자 인증을 할 수 있다.
com.example.pepega.config 안에 security package를 만든다.
security package 안에 JwtTokenProvider 클래스를 생성한다.
application.yml 에서
jwt secret 키 값을 임의로 넣어준다.
JwtAuthenticationFilter 생성
Jwt가 유효한 토큰인지 인증하기 위한 Filter이다.
com.example.pepega.config.security 하위에
JwtAuthenticationFilter 클래스를 생성한다.
이 Filter는 Security 설정 시 UsernamePasswordAuthenticationFilter 앞에 세팅할 것이다.
SpringSecurity Configuration
서버에 보안 설정을 적용할 것이다.
com.example.pepega.config.security 하위에
WebSecurityConfig 클래스를 삭제하고
SecurityConfiguration 클래스를 작성한다.
아무나 접근 가능한 resource는 permitAll()로 세팅한다.
나머지 resource는 다음과 같이 'ROLE_USER' 권한이 필요함으로 명시한다.
anyRequest().hasRole("USER") 또는 anyRequest().authenticated()는 동일한 동작을 한다.
위에서 설명했듯이 해당 filter는 UsernamePasswordAuthenticationFilter 앞에 설정해야 한다.
SpringSecurity 적용 후에는 모든 리소스에 대한 접근이 제한된다.
Swagger 페이지에 대해서 예외를 적용해야 페이지에 접근할 수 있다.
resource 접근 제한 표현식은 여러가지가 있으며 다음과 같다.
hasIpAddress(ip) - 접근자의 IP 주소가 매칭 하는지 확인한다.
hasRole(role) - 역할이 부여된 권한(Granted Authority)과 일치하는지 확인한다.
hasAnyRole(role) - 부여된 역할 중 일치하는 항목이 있는지 확인한다.
ex) access = "hasAnyRole('ROLE_USER', 'ROLE_ADMIN')"
permitAll - 모든 접근자를 항상 승인한다.
denyAll - 모든 사용자의 접근을 거부한다.
anonymous - 사용자가 익명 사용자인지 확인한다.
authenticated - 인증된 사용자인지 확인한다.
rememberMe - 사용자가 remember me를 사용해 인증했는지 확인한다.
fullyAuthenticated - 사용자가 모든 크리덴셜을 갖춘 상태에서 인증했는지 확인한다.
Custom UserDetailsService 정의
토큰에 세팅된 유저 정보로 회원정보를 조회하는 UserDetailsService를 재정의한다.
com.example.pepega.service pacakge 안에
security package를 생성한다. 그 안에
CustomUserDetailService 클래스를 생성한다.
findById 부분에서 오류가 날 수 있다.
하단을 진행하면 된다.
User Entity 수정
SpringSecurity의 보안을 적용하기 위해 User entity에 UserDetails 클래스를 상속받아 추가 정보를 재정의한다.
roles는 회원이 가지고 있는 권한 정보이고, 가입했을 때는 기본 "ROLE_USER"가 세팅된다.
귄한은 회원당 여러 개가 세팅될 수 있으므로 Collection으로 선언한다.
getUsername은 security에서 사용하는 회원 상태 값이다.
여기선 모두 사용 안 하므로 true로 설정한다.
JSON 결과로 출력하지 않을 데이터는
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
annotation을 선언한다.
isAccountNonExpired - 계정이 만료되지 않았는지
isAccountNonLocked - 계정이 잠기지 않았는지
isCredentialsNonExpired - 계정 패스워드가 만료되지 않았는지
isEnabled - 계정이 사용 가능한 상태인지
UserJpaRepo에 findByUid 추가
회원 가입 시 가입한 이메일 조회를 위해 다음 메서드를 선언한다.
로그인 예외 추가
com.example.pepega.advice.exception package 하단에
CEmailSigninFailedException 클래스를 생성한다.
resources.i18n 하위의 yml 파일들을 수정해준다.
com.example.pepega.advice package 안에
ExceptionAdvice 내용을 추가해준다. (이메일 예외처리)
가입 / 로그인 Controller 추가
로그인 성공 시에는 결과로 Jwt 토큰을 발급한다.
가입 시에는 패스워드 인코딩을 위해 passwordEncoder 설정을 한다.
기본 설정은 bcrypt encoding이 사용된다.
com.example.pepega.controller.v1 package 안에
SignController 클래스를 생성한다.
SpringRestApiApplication에 passwordEncoder bean 추가
UserController 수정
유효한 Jwt 토큰을 설정해야만 User 리소스를 사용할 수 있도록
Header에 X-AUTH-TOKEN을 인자로 받도록 선언한다.
application.yml 수정
create로 바꾼 후 서버 시작
테이블이 생성되면
로 교체한다.
Test Swagger
회원가입 -> 로그인 -> 토큰을 이용한 회원정보 조회순으로 Test를 진행한다.
Vitas가 회원가입 되었다.
vitas가 로그인에 성공했다.
로그인 후 도출된 X-AUTH-TOKEN으로 회원을 조회해보자.
아주 잘된다.
인증 토큰을 발급받아 제한된 리소스에 접근하는 것에 대한 테스트가 완료됐다.
예외 처리 보완
다음과 같은 상황을 예측해 볼 수 있다.
1. Jwt 토큰 없이 api를 호출하였을 경우
2. 형식에 맞지 않거나 만료된 Jwt 토큰으로 api를 호출한 경우
3. Jwt 토큰으로 api를 호출하였으나 해당 리소스에 대한 권한이 없는 경우
커스텀 예외 처리가 적용이 안되는 이유
위의 상황에서 Custom으로 적용한 예외 처리가 적용이 안 되는 이유는 필터링의 순서 때문이다.
지금까지 적용한 Custom 예외 처리는 ControllerAdvice 즉
Spring이 처리 가능한 영역까지 리퀘스트가 도달해야 처리할 수 있다.
그러나 SpringSecurity는 Spring 앞단에서 필터링을 하기 때문에
해당 상황의 exception이 Spring의 DispatcherServlet까지 도달하지 않게 되는 것이다.
1, 2번에 대한 해결책
온전한 Jwt가 전달이 안될 경우는 토큰 인증 처리 자체가 불가능하기 때문에
토큰 검증 단에서 프로세스가 끝나버리게 된다.
해당 예외를 잡아내려면 SpringSecurity에서 제공하는 AuthenticationEntryPoint를 상속받아 재정의 해야한다.
예외가 발생한 경우 아래에서는 /exception/entrypoint로 포워딩되도록 처리하였다.
CustomAuthenticationEntryPoint 작성
config.security 안에 CustomAuthenticationEntryPoint 클래스를 생성한다.
ExceptionController 작성
controller package 하위에 exception package를 생성한다.
exception package 안에 ExceptionController를 작성한다.
내용은 /exception/entrypoint 로 주소가 들어오면
CAuthenticationEntryPointException을 발생시키라는 것이다.
CAuthenticationEntryPointException 생성
Message 내용 추가
ExceptionAdvice 내용 추가
SpringSecurityConfiguration에 CustomAuthenticationEntryPoint 설정 추가
.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint())를 추가하고
/exception 주소를 PermitAll()에 추가한다.
Swagger Test
3번에 대한 해결책
3번은 Jwt 토큰이 정상이라는 가정 하에 Jwt 토큰이 가지지 못한 권한의 리소스를 접근할 때 발생하는 오류다.
이 경우에는 SpringSecurity에서 제공하는 AccessDeniedHandler를 상속받아 커스터마이징 해야한다.
예외가 발생할 경우 handler에서는 /exception/accessdenied로 포워딩되도록 할 것이다.
CustomAccessDeniedHandler 생성
ExceptionController 수정
위에서 생성한 ExceptionController에 다음 내용을 추가한다.
이미 존재하는 Exception이므로 커스텀 Exception은 따로 만들지 않고
기존 AccessDeniedException을 발생시킨다.
Message 내용 추가
ExceptionAdvice 내용 추가
SpringSecurityConfiguration에 CustomAccessDeniedHandler 설정 추가
테스트를 위해 /users api는 ROLE_ADMIN 권한만 접근 가능하게 처리한다.
.antMatchers("/*/users").hasRole("ADMIN")
예외 처리 고도화까지 적용하였다.
이제 서버는 어느정도 보안성을 갖추게 되었다.
SpringSecurity를 적용하지 않고 동일한 프로세스에서 처음부터 만들려고 했으면
많은 고난과 예외 상황에 대한 대처를 위해 많은 시간과 노력이 피룡했을 것이다.
그렇지만 SpringSecurity를 적용하는데도 상당한 지식과 노력이 필요하다.
본문에 나온 내용은 Security의 일부 내용일 뿐이다. 그만큼 SpringSecurity 내용은 방대하다.
이 포스팅을 통해 서버를 보다 더 견고하게 만드는 첫 걸음이 되면 좋겠다.
추가) annotation으로 리소스 접근 권한 걸정
실습에서는 리소스에 대한 접근권한 설정 시 아래와 같이
SecurityConfiguration.java의 configure(HttpSecurity http) 메서드 내부에서
authorizeRequests()를 통해 세팅하였다.
이렇게 하면 리소스의 권한을 중앙관리한다는 점에서 이점이 있다.
추가로 Spring에서는 annotation으로도 권한 설정이 가능하도록 지원하고 있다.
각각의 방식은 장단점이 있으므로 상황에 따라 적합한 방법을 채택하면 될 것 같다.
@PreAuthorize, @Secured
권한 설정이 필요한 리소스(실습에서는 Controller의 각 메서드에 해당)에
@PreAuthorize, @Secured로 권한을 세팅할 수 있다.
둘 다 같은 역할을 하지만 아래와 같은 차이가 있다.
@PreAuthorize
표현식 사용 가능
ex) @PreAuthorize("hasRole('ROLE_USER') and hasRole('ROLE_ADMIN')")
@Secured
표현식 사용 불가능
ex) @Secured({"ROLE_USER", "ROLE_ADMIN"})
annotation으로 권한 설정을 하려면 GlobalMethodSecurity를 활성화해야 한다.
아래와 같이 SecurityConfiguration 상단에
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
를 추가한다.
그리고 configure 메서드 안에서
authorizeRequest() 설정은 주석 처리하거나 삭제한다.
Controller에 권한을 설정한다.
만약 Controller 내부의 모든 리소스에 대하여 일괄로 동일한 권한을 설정할 것이면
Controller 상단에 annotation을 세팅한다.
만약 각각 리소스마다 권한을 설정해야 하면
해당 메서드 위에 annotation을 세팅하면 된다.
위에서 annotation으로 권한을 설정한 리소스 외 나머지 리소스들은
누구나 접근 가능한 리소스로 설정된다.
출처
https://daddyprogrammer.org/post/636/springboot2-springsecurity-authentication-authorization/
https://github.com/codej99/SpringRestApi
'SpringBoot' 카테고리의 다른 글
DB 통신 속도 증가 Cacheable CacheEvict redis 활용 (0) | 2021.12.21 |
---|---|
Junit에서 assertThat(.isEqualTo) 활용하기 (0) | 2021.10.22 |
springboot로 Rest api 만들기(7) MessageSource를 이용한 Exception 처리 (0) | 2021.10.22 |
springboot로 Rest api 만들기(6) ControllerAdvice를 이용한 Exception 처리 (0) | 2021.10.22 |
springboot로 Rest api 만들기(5) API 인터페이스 및 결과 데이터 구조 설계 (0) | 2021.10.22 |