Search
🔐

Form Login 방식으로 로그인 처리하기

태그
SpringBoot
목차

문제의 시작

오늘도 어김없이 에러를 마주쳐버린 김시은.
역시 인생은 호락호락하지 않아..
vue.js 와 스프링부트를 이용해 로그인 기능을 만들던 도중 이렇게 데이터는 잘 가는데,
여기서 email 과 password 값이 계속 null 로 들어오는 상태! (id 는 email 로 바꾸었습니다.)
얼렁뚱땅 남의 코드 배껴서 해결하기보다는 천천히 공부하고 해결해 보는게 맞다고 판단 하였고, 아래는 그 공부 결과와 해결 방안을 적어보았습니다. 같은 문제로 고민하는 사람들에게 도움이 되었으면 좋겠습니다.

formLogin() 의 동작방식

스프링 시큐리티의 formLogin() 을 사용하면 Content-Type 이 x-www-form-urlencoded 인 방식으로만 데이터를 받을 수 있다.

공식문서가 말하는 FromLoginConfigurer

문서를 보면 FormLoginConfigurer 는 양식 기반 인증을 추가한다고 되어 있습니다. 인증을 추가하며 UsernamePasswordAuthenticationFilter 가 채워진다고 하는데요, 이 필터는 무슨 역할을 할까요?

UsernamePasswordAuthenticationFilter

문서를 보면 이는 인증 양식 제출을 처리한다고 되어 있습니다. 이때 로그인 양식은 이 필터에 username 과 password 두 가지 parameter 를 제공해야 합니다. 기본 매개변수 이름은 static field 인 SPRING_SECURITY_FORM_USERNAME_KEY 에 포함되어 있습니다. 하지만 이는 변경할수도 있습니다. 이 필터는 기본적으로 /login url 에 응답하지만 이 또한 변경할 수 있습니다.

x-www-form-urlencoded 와 Json

스프링 시큐리티의 formLogin() 을 사용하면 Content-Type 이 x-www-form-urlencoded 인 방식으로만 데이터를 받을 수 있다고 했습니다. 이 둘의 차이는 뭘까요?

application/x-www-form-urlencoded

모든 문자들은 서버로 보내기 전 인코딩 되며 인코딩 규칙은 아래와 같습니다.
& 은 입력을 분할합니다.
모든 입력은 튜플인 이름-값으로 이루어집니다.
= 가 포함된 경우 name=value 를 나타냅니다.
white space는 + 를 나타냅니다.
보내고 난 뒤의 페이로드는 아래와 같습니다.
... (Body) x-www-form-urlencoded : "key":value, ...
Plain Text
복사

application/json

@RequestBody 를 사용하여 데이터를 받습니다.
http 의 RequestBody 부분을 그대로 읽어서 반환합니다.
보내고 난 뒤의 페이로드는 아래와 같으며, 제이슨 객체로 받는다는 것을 알 수 있습니다.
... (Body) raw : { "key":value, } ...
Plain Text
복사

왜 Json 을 받을 수 없나요?

유저네임과 패스워드를 받는 메서드를 확인하면 이를 알 수 있습니다.
두 메서드의 파라미터는 request 입니다. 클라이언트가 폼에 양식을 입력하고 request 를 날리면 파라미터로 날아온 request 의 값을 얻기 위해 getParameter() 메서드를 사용합니다. 애초에 형식 자체가 틀렸기 때문에 사용할 수 없습니다.

스프링 시큐리티의 전체적인 동작과정

AuthenticationFilter 는 폼으로 받은 유저 아이디와 비밀번호를 이용해UsernamePasswordAuthenticationToken 을 생성하여 AuthenticationManager 로 전달합니다.
AuthenticationProvider 의 타입 중 하나인 DaoAuthenticationProvider 를 사용하기 위해 providerManager 가 AuthenticationManager 로 설정됩니다.
DaoAuthenticationProvider 가 UserDetailsService 에서 loadUserByUsername 메소드를 Overriding 하여 Username 을 통해 해당되는 유저의 UserDetials 를 생성합니다.
DaoAuthenticationProvider 가 PasswordEncoder 를 이용해서 비밀번호를 유효화하고 다시 이전 단계로 돌아갑니다.
인증이 성공적으로 되었다면 UsernamePasswrodAuthenticationToken 을 반환합니다. 이는 authentication Filter 에 의해 SecurityContextHolder 에 세팅됩니다.

로그인 문제 해결과정

로그인 초안의 문제점과 방식 변경

처음에는 로그인 페이지에서 데이터를 아래와 같이 받아왔습니다.
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() 을 사용하고 데이터를 받아오는 방식을 바꾸는 방향으로 결정했습니다.

시큐리티 커스텀하기

UserDto 만들기

로그인 창에 입력한 이메일과 비밀번호를 받아서 담는 역할을 합니다.

UserDetailsService 커스텀하기

UserDetailsService 안의 loadUserByUsername() 오버라이딩 하여 사용하기 위해 커스텀 합니다.
이는 회원을 조회하기 위해 사용합니다.

AuthenticationProvider Custom 하기

회원을 조회하고 회원이 있다면 패스워드가 맞는지 검증합니다.

PasswordEncoder 클래스 만들어주기

순환 참조를 방지하기 위해 만들어 줍니다.

로그인 성공과 실패시 핸들링하는 클래스 만들기

로그인 성공 핸들러
로그인 실패 핸들러

SecurityConfig 수정하기

알맞게 코드 수정하기

백단에서의 처리는 이제 끝났고! 앞단에서의 처리가 남았습니다. 이제는 JSON 형태가 아닌 multipart/form-data 형태로 받아와야 합니다.

login.vue 수정

enctype 을 넣어주었습니다

pinia action 수정

header 의 Content-Type 을 수정합니다.
vuex 가 아닌 pinia 를 사용중입니다.

Postman 을 통해 확인하기

현재 데이터베이스는 아래와 같습니다.
rawpassword 는 123456789 입니다. 이를 postman 에서 아래와 같이 작성해주고,
콘솔에서 확인해봅니다. 만약 로그인이 제대로 되었다면 successhandler 에서 지정한대로 로그인 성공이라는 문구가 뜨고, 안되면 그 반대가 나올거에요.
드디어 성공 이 문구를 보기위해 오랜시간이 걸렸네요
반대로 요상한 데이터를 넣었을 때도 확인