문제의 시작
오늘도 어김없이 에러를 마주쳐버린 김시은.
역시 인생은 호락호락하지 않아..
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 에서 지정한대로 로그인 성공이라는 문구가 뜨고, 안되면 그 반대가 나올거에요.
드디어 성공 이 문구를 보기위해 오랜시간이 걸렸네요
반대로 요상한 데이터를 넣었을 때도 확인