티스토리 뷰

반응형

우선 제목은 거창하지만 사실 내용은 별거 없다... 

 

옛날에 프로젝트를 진행할 때 Spring Security를 사용해 로그인 시스템을 구현한적이 있는데 최근 인프런 강의에서 Spring Security를 사용해 로그인 시스템을 구현하는 것을 공부하게 되었다.

 

물론 사람마다 코드를 짜는 방식이 다르겠지만 내가 프로젝트에서 사용한 방법과 인프런 강의에서 소개된 방식의 차이를 알아보고자 이번 포스팅을 작성하게 되었다.

첫 번째 방법


첫 번째 방법은 아래 링크에서 소개했던 방식으로 자세한 내용을 확인하려면 아래 링크를 확인하면 된다.

https://ch4njun.tistory.com/228

 

[Spring Boot] Spring Security

Vue.js와 Spring Boot를 사용 해 진행중인 나의 프로젝트에서 로그인 시스템을 만들려던 중 Spring Boot에서 인증/인가를 구현하기 위한 프레임워크인 Spring Security에 대해서 알게되었다. 하지만 단순히

ch4njun.tistory.com

 

로그인 Request가 넘어올 경우 진행되는 프로세스를 먼저 살펴보도록 한다.

 

1. LoginFilter

LgoinFilter를 생성할 때 defaultUrl로 /api/*/auth/login을 설정했기 때문에 로그인 Request인 /api/v1/auth/login은 해당 필터에 인터셉트 된다.

2. LoginFilter의 attemptAuthentication()

필터에 인터셉트된 로그인 Request는 LoginFilter 클래스의 attemptAuthentication 메서드로 넘어온다. 로그인 정보를 Dto에 저장한 후 super.getAuthenticationManager().authentication() 을 호출해 Authentication Provider를 호출한다.

 

여기서 파라미터로 넘기는 PreLoginAuthorizationToken 객체는 Authentication Object로 UsernamePasswordAuthenticationToken 클래스를 상속받은 클래스이다. (이 내용은 두 번째 구현방식에서도 나오니 참고하면 좋을듯 하다.)

3. LoginAuthenticationProvider의 authenticate()

실제 인증을 거치는 곳으로 인자로받은 Token(Authentication Object)에 저장된 userId, password를 받아 DB의 정보와 비교한다.

 

이후 올바른 값이라면 인증된 Token(Authentication Object)을 반환하고 올바르지 않다면 InvalidUserException을 뿌린다. 이때 반환되는 Authentication Object 또한 UsernamePasswordAuthenticationToken 클래스를 상속받은 클래스이다.

 

조금 주의깊게 봐야할 것은 이 반환되는 Authentication Object에 어떤 정보를 저장하는지이다. 

위 정적 팩토리 메서드를 살펴보면 Principal에 userId가 아니라 UserContext 객체를 저장하는 것을 확인할 수 있다. 사실 중요한 것은 인증 완료 후 반환되는 Authentication Object에는 인증된 사용자의 정보가 저장되어야 한다는 것이다.

(ID, Password, Role 이정도만 저장되면 되는듯...)

4. LoginFilter의 successfulAuthentication()

인증에 성공했을 경우 successfulAuthentication() 메서드가 호출된다. 반대로 인증에 실패한 경우는 unsuccessfulAuthnetication() 메서드가 호출된다. 이번 포스팅에서는 인증에 성공한 경우만 살펴보도록 하자.

 

인증에 성공한 경우 JWT Token을 발행하고(발행할 때 Authentication Object에 저장되어 있던 사용자 정보를 활용) 응답값에 포함시켜 돌려준다고 생각하면 된다.

 

여기서 드는 의문은 "어? 보통 요청이 오면 Controller에 URL과 매칭되는 메서드가 호출되는게 아닌가..? 여기서 끝이라고?" 이다. 근데 여기서 끝이 맞다. 냐하면 chain.doFilter(req, res) 를 통해 다음 Filter로 해당 Request를 전달하지 않았기 때문이다.

 

따라서 Controller에 /api/v1/auth/login 이라는 URL을 만들 필요도 없다. 이렇게 처리하는 것이 나는 매우 간결해 보인다. 하지만 이 방법이 흔히 사용되는 방법인진 모르겠다. (아시는 분은 댓글로 부탁드립니다)

 

위 코드는 첫 번째 방법을 사용하기위해 필요한 Configuration(SecurityConfig)이다. WebSecurity에 Filter와 Authentication Provider를 추가해주는 과정이다.

두 번째 방법


이어서 인프런 강의에서 공부했던 로그인 인증 시스템의 프로세스에 대해서 소개하겠습니다.

1. AuthenticationFilter

이 AuthenticationFilter는 첫 번째 방법에서 LoginFilter와 동일한 역할을 수행한다. 하지만 여기서는 defaultUrl을 지정해주지 않았다. 그런데도 어떻게 로그인 Request를 식별해고 Filter에서 인터셉트하는지 궁금하다.

 

그래서 조금 뜯어보니 여기서 구현한 AuthenticationFilter는 첫 번째 방법에서의 AbstractAuthenticationProcessingFilter 와는 다르게 UsernamePasswordAuthenticationFilter 클래스를 상속받은 클래스였다. 그리고 이 UsernamePasswordAuthenticationFilter 클래스가 AbstractAuthenticationProcessingFilter 클래스를 상속받았다. 즉, 한 뎁스가 더 깊다는 말이다.

 

그리고 UsernamePasswordAuthenticationFilter에는 아래와 같은 PathMatcher를 사용한다.

그렇기 때문에 [POST] /login 으로 오는 로그인 Request를 해당 Filter에서 인터셉트할 수 있는 것이다.

2. AuthenticationFilter의 attemptAuthentication()

이후에 로그인 Requests는 마찬가지로 Filter의 attemptAuthentication 메서드로 들어온다. 하지만 여기엔 한가지 차이점이 있는데 첫 번째 방법에서는 UsernamePasswordAuthenticationToken을 상속받는 두 가지 Token을 만들어서 사용했다. 하지만 여기선 그냥 사용한다.

 

그러면 왜 첫 번째 방법에선 굳이 나눠 사용한 걸까?

 

아래와 같이 UsernamePasswordAuthenticationToken 클래스에는 두 가지 생성자가 있다.

두 생성자는 꽤 큰 차이를 가지고 있는데 생성자의 인자가 2개인 것을 사용해 객체를 생성하면 인증전 객체를 의미하고 인자가 3개인 것을 사용해 객체를 생성하면 인증후 객체를 의미하게 된다.

 

그냥 알아서 잘 사용하면되지 뭐가문제야? 라고 생각할 순 있지만 이를 조금 더 명확하게 사용하기 위해서 첫 번째 방법에서는 UsernamePasswordAuthenticationToken을 상속받는 두 가지 Token 클래스를 만들어 사용한 것이다.

3. UserService의 loadUserByUsername()

두 번째 방법은 여기서 인증과정을 거친다. 엥? 인증과정은 Authentication Provider에서 처리하는 거라매!!! 라고 생각할 수 있다. 아직 정확한 내용은 모르겠지만 해당 Service가 Authentication Provider의 역할을 수행하는 것 같다.

(이 부분은 추가적으로 알아본 후 정리하도록 할 예정이다. 혹시 아시는분은 댓글로 부탁드립니다.)

 

사실 이렇게 UserService에서 처리하기 위해서는 UserService가 UserDetailsService 클래스를 상속받아야 한다. 그리고 UserDetailsService 클래스에 있는 loadUserByUsername() 메서드를 오버라이딩하는 것이다.

 

확실히 UserContext 클래스 만들며 처리하는 것보다 간결하고 Authentication Provider를 직접 만들필요도 없어서 많이 간결하긴 한 것 같다.

 

이렇게 UserDetailsService 클래스를 상속받은 UserService를 Configuration(SecurityConfig)을 통해 등록해줘야 한다.

이렇게 할 경우 해당 userService를 Authentication Provider처럼 동작시키고 심지어 패스워드 인코딩까지 자동으로 해준다.. 좀 더 자세히 뜯어볼 필요성은 있는 것 같다.

 

아무튼 loadUserbyUsername() 메서드에서 DB와 비교하여 해당 Username을 가지는 사용자가 있는지 확인한다. 어.... 근데 패스워드를 확인하는 내용은 없다. 근데 아무튼 이부분도 해준다.... 진짜 어지럽다 ㅠㅠ

 

자동화 해주는게 좋고 반가워야되는데 왜이렇게 어지럽지ㅋㅋㅋ

4. AuthenticationFilter의 successfulAuthentication()

인증이 성공하면 첫 번쨰 방법과 마찬가지로 successfulAuthentication() 메서드로 돌아온다.

여기서도 첫 번째 방법과 마찬가지로 chain.doFilter(req, res) 는 존재하지 않는다. 따라서 이에 매칭되는 URL을 Controller에 만들 필요도 없다.

참고로 chain.doFilter(req, res)를 임의로 추가하자 해당 URL에 매칭되는 것(Servlet)이 업기 때문에 NOT FOUND 에러를 반환한다.

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함