Search

DaoAuthenticationProvider 와 JSON 형식의 데이터를 이용한 로그인 기능 구현 일대기😎

formLogin() 의 동작방식

스프링 시큐리티의 formLogin() 을 사용하면 Content-Type 이 x-www-form-urlencoded 인 방식으로만 데이터를 받을 수 있다.
도대체 어떻게 동작하기에,,, 이러는 건가요,,, 이제부터 하나하나 알아보겠습니다.
공식문서가 말하는 FromLoginConfigurer
문서를 보면 FormLoginConfigurer 는 양식 기반 인증을 추가한다고 되어 있습니다. 인증을 추가하며 UsernamePasswordAuthenticationFilter 가 채워진다고 하는데요, 이 필터는 무슨 역할을 할까요?
UsernamePasswordAuthenticationFilter
문서를 보면 이는 인증 양식 제출을 처리한다고 되어 있습니다. 이때 로그인 양식은 이 필터에 username 과 password 두 가지 parameter 를 제공해야 합니다. 기본 매개변수 이름은 static field 인 SPRING_SECURITY_FORM_USERNAME_KEY 에 포함되어 있습니다. 하지만 이는 변경할수도 있습니당. 이 필터는 기본적으로 /login url 에 응답하지만 이 또한 변경할 수 있습니다.
로그인 요청에서 기본 username 과 password 변경
void setUsernameParameter(String usernameParameter)
void setPasswordParameter(String passwordParameter)
이 필터에서 username 과 password 를 얻는 메소드를 보겠습니다.
obtainUsername
추가 값 및 구분 기호를 포함하는 등 하위 클래스가 사용자 이름을 재정의할 수 있습니다.
이는 Authentication 요청 토큰에 표시될 사용자 이름 AuthenticationManager 입니다.
obtainPassword
추가 값 및 구분 기호를 포함하는 등 하위 클래스가 사용자 암호 구성을 재정의할 수 있습니다.
이는 Authentication 요청 토큰에 표시될 사용자 이름 AuthenticationManager 입니다.
두 메소드는 request 에서 getParameter 로 데이터를 받아오기 때문에 parameter 형식이 아닌 JSON 데이터는 가져올 수 없습니다.
그렇다면 어떻게 해야할까요?
우선, 스프링 시큐리티의 전체적인 동작과정을 다시한번 살펴보도록 하겠습니다!
일단 AuthenticaitonFilter 를 거쳐야 하므로, JSON 형태로 받은 데이터에서 username 과 passsword 를 알아내야합니다.
이렇게 알아낸 username 과 password 를 이용해 UsernamePasswordAuthenticaionToken 을 생성합니다.
그 후 UserDetailsService 를 상속받아 loadUserByUsername 메소드를 Overriding 하여 Username 을 통해 해당되는 유저의 UserDetials 를 생성합니다.
여기까지 하면 DaoAuthenticationProvider 를 이용하여 비밀번호 체크 과정을 거치고, 로그인처리를 할 수 있습니다.

DaoAuthenticationProvider 의 동작과정

각 번호에 대한 설명
username 과 password 를 인증하는 필터는 UsernamePasswordAuthenticationToken 을 생성하여 AuthenticationManager 로 전달합니다.
시큐리티 아키텍쳐 다시보기
AuthenticationProvider 의 타입 중 하나인 DaoAuthenticationProvider 를 사용하기 위해 providerManager 가 AuthenticationManager 로 설정됩니다.
DaoAuthenticationProvider 가 UserDetailsService 에서 UserDetials 를 찾습니다.
DaoAuthenticationProvider 가 PasswordEncoder 를 이용해서 비밀번호를 유효화하고 다시 이전 단계로 돌아갑니다.
인증이 성공적으로 되었다면 UsernamePasswrodAuthenticationToken 을 반환합니다. 이는 authentication Filter 에 의해 SecurityContextHolder 에 세팅됩니다.
SecurityContextHolder 구조 다시보기

로그인 문제 해결과정

vue.js 와 스프링부트를 이용판 프로젝트를 진행하던 중, 로그인 관련 문제가 발생했다. 관련 오류와 내용은 시큐리티 로그인처리 를 통해 볼 수 있습니다.
처음에는 로그인 페이지에서 데이터를 아래와 같이 받아왔습니다.
export const loginStore = defineStore("login", { id: "login", state: () => ({ email : "", password: "" }) actions: { // 로그인 async login() { try{ await axios.post(`api/user/login`, JSON.stringfy(this.$state)) console.log(this.$state); } catch (e) { console.log('에러났어요'); } } } })
Java
복사
Content-Type 또한 main.js 에서 아래와 같이 지정해주었기 때문에 데이터는 기본적으로 JSON 형태로 보내졌습니다.
하지만 formLogin() 을 사용할 경우 JSON 형식은 사용할수 없기 때문에 필터를 이용하여 이를 적절하게 바꾸어 주어야 한다고 했습니다. 토큰도 사용해야하고,,, 위에 적어놓은 방향으로 가기위해 여러곳을 참고하여 시도해봤지만 잘 안되서 그냥 formLogin() 을 사용하고 데이터를 받아오는 방식을 바꾸는 방향으로 결정했습니다.
Rest Api 를 이용하였기 때문에 시큐리티에서 기본적으로 제공되어지는 것들을 커스터마이징 해야했습니다. 순서대로 정리하면 아래와 같습니다.
로그인 창에 입력한 이메일과 비밀번호를 받아서 담아놓을 UserDto 만들기
회원을 조회하기 위해 UserDetailsService 안의 loadUserByUsername() 사용하기 위해 UserDetailsService 커스텀하기
회원을 조회하고 회원이 있다면 패스워드가 맞는지 검증하는 AuthenticationProvider 를 Custom 하기
혹시나 순환참조 에러가 발생할 수 있으니 PasswordEncoder 클래스 만들어주기
로그인 성공과 실패시 핸들링하는 클래스 만들기
SecurityConfig 수정하기
백단에서의 처리는 이제 끝났고! 앞단에서의 처리가 남았습니다. 이제는 JSON 형태가 아닌 multipart/form-data 형태로 받아와야 합니다
login.vue 수정
pinia action 수정 → header 의 Content-Type 바꾸기
앞단에서의 처리도 다 끝났으니 실제로 postman 을 통해 확인해보도록 하겠습니다
현재 데이터베이스는 아래와 같습니다.
rawpassword 는 123456789 입니다. 이를 postman 에서 아래와 같이 작성해주고,
콘솔에서 확인해봅니다. 만약 로그인이 제대로 되었다면 successhandler 에서 지정한대로 로그인 성공이라는 문구가 뜨고, 안되면 그 반대가 나올거에요
드디어 성공 이 문구를 보기위해 오랜시간이 걸렸네요
반대로 요상한 데이터를 넣었을 때도 확인
굿굿 아주 좋아요

회고

아아,, 저는 정말로 JSON 을 받아와서 처리하고 싶었단 말이에요.. 조금더 공부해서 JWT 인증도 구현해봐야겠습니다. 누구에게는 정말 쉬운 일이지만 초심자에겐 정말 어려운,, 시큐리티 아키텍쳐를 다 이해하지는 못했지만 대략 어떻게 돌아가는지 공부하는것도 꽤나 오랜 시간이 걸렸습니다.
그래도 괜찮아요. 이렇게 다들 성장하는거니까요
매일매일 꾸준히 열심히해서 멋진 직장도 가고, 주니어 개발자, 시니어 개발자까지 쭉쭉 가보겠습니당 가보자구 진짜루,,