스프링 시큐리티를 쓰는 이유
예를 들어보자)
모든 API 요청에 토큰을 보네는 것으로 사용자를 확인한다. 그러면 각 API는 맨 처음 토큰을 확인함으로써 접근을 허용 또는 거부하는 코드를 실행할 것이다. 문제는 모든 API가 이 작업을 해야한다는 것이다. 50개가 넘는 API가 있으면 요청을 인증하는 코드를 50번 반복해야 한다는 것이다. 하지만 스프링 시큐리트를 이용해 코드를 한 번만 짜고, 이 코드가 모든 API를 수행하기 바로 전에 실행되도록 설정하고 구현 하면 된다.
build.gradle에 jjwt 라이브러리 추가
TokenProvider 클래스
계정 생성 후 token필드 반환 확인
성공!
스프링 시큐리티는 아주 간단히 말하면 서블릿 필터의 집합이다. 그렇다면 서블릿 필터는 무엇인가?
서블릿 필터는 서블릿 실행 전에 실행되는 클래스들이다.
- 구현된 로직에 따라 원하지 않는 HTTP 요청을 걸러 낼 수 있다.
- 걸러낸 HTTP는 거절되는 것이고, 서블릿 필터에서 전부 살아남은 HTTP 요청은 마침내 디스패쳐 서블릿으로 넘어와 우리 컨트롤러에서 실행된다.
서블릿 필터가 꼭 한 개일 필요는 없다. 기능에 따라 다른 서블릿 필터를 작성할 수 있고, 이 필터들을 FilterChain을 이용해 연쇄적으로 순서대로 실행 할 수 있다.
그렇다면 이 서블릿 필터에서 스프링 시큐리티의 위치와 우리가 구현할 필터의 위치는 어디일까?
바로 JwtAuthenticationFilter부분이다!!!!
스프링 시큐리티 gradle에 추가
JwtAuthenticationFilter
코드 설명
서블릿 필터를 구현하는 일을 하였으므로 서블릿 필터를 사용하라고 알려주는, 설정 작업을 해준다.
WebSecurityConfig
HttpSecurity는 시큐리티 설정을 위한 오브젝트이다. 이 오브젝트는 빌더를 제공하는데, 빌데럴 이용해 cors, csrf, httpbasic, session, authorizeRequest 등 다양한 설정을 할 수 있다.즉 web.xml대식 HttpSecurity를 이용해 시큐리티 관련 설정을 하는 것이다.
addFilterAfter() : JwtAuthenticationFilter를 CorsFilter 이후에 실행하라고 설정하는 것이다.
테스팅
로그인 후 GET요청에 Bearer토큰 함께 보내기
정상적인 인증 성공!
토큰이 다르면 403 Forbidden!
그 전에는 String temporaryUserId = "temporary-user"; 로 임시로 지정해 놓았던 유저 아이디를 사용하고 있었다. 이제 인증된 유저 아이디를 사용할 수 있도록 각 메서드에 userId 매개변수를 추가한다.
TodoController
userId는 도대체 누가 어떻게 String인 것을 알고 넘겨주는 것일까?
@AuthenticationPrincipal이 userId를 찾아낸다. 그렇다면 @AuthenticationPrincipal은 무엇인가? 답을 찾기 위해 JwtAuthenticationFilter 클래스를 봐보자.
JwtAuthenticationFilter에서 UsernamePasswordAuthenticationToken을 생성했다. 이때 생성자의 첫 매개변수로 넣은 것이 AuthenticationPrincipal이다. 이 AuthenticationPrincipal에 String형의 userId를 넣었다.
따라서
- 스프링은 컨트롤러 메서드를 부를 떄 @AuthenticationPrincipal 어노테이션이 있다는 것을 안다.
- SecurityContextHolder에서 SecurityContext::Authentication, 즉 UsernamePasswordAuthenticationToken을 가져와 컨트롤러 메서드에 넘겨준다.
테스팅
두 계정을 생성하고 각각 Todo를 추가해보았다 첫번째 사용자가 추가한 Todo는 보이지 않고 오직 두 번째 사용자가 추가한 Todo만 반환된다.
UserService.class
보통 암호화된 패스워드를 비교해야 하는 경우, 사용자에게 받은 패스워드를 같은 방법으로 암호화한후, 그 결과를 데이터베이스의 값과 비교하는 것이 자연스러운 흐름이다.
우리는 matches() 메서드를 사용했는데 BCryptPasswordEncoder는 같은 값을 인코딩하더라도 값이 다르기 때문이다. (패스워드에 랜덤하게 의미 없는 값(salt)을 붙여 결과를 생성한다.)
따라서 사용자에게 받은 패스워드를 인코딩해도 데이터베이스에 저장된 패스워드와는 다를 확률이 높다. 대신 mathches() 메서드가 Salt를 고려해 두 값을 비교해준다.
UserController.class
회원가입 시 패스워드를 인코딩해 저장하는 부분과 로그인 시 수정한 getByCredentials에 BCryptPasswordEncorder를 넘겨주는 것이다.