username과 password로 로그인을 구현해보도록 하겠습니다.
Overview
spring security의 PasswordEncoder를 이용하면 단방향 방식의 암호화를 통해 비밀번호를 안전하게 저장할 수 있습니다. 실제로 회원가입 기능에서 PasswordEncoder를 상속받은 BCryptPasswordEncoder를 이용해서 비밀번호를 저장하는 기능을 구현해봤습니다. 로그인 기능을 구현한다면서 왜 갑자기 회원가입을 언급하고 있는 걸까요?? 이유는 로그인 방법이 두 가지로 나뉘기 때문입니다. 암호화 과정을 두 번 거칠 것인지 아니면 비밀번호 저장 방식을 바꿀 것인지로 말이죠.
저장 방식을 바꿔보자
spring security 공식 문서에서 추천하는 방식은 DelegatingPasswordEncoder로 비밀번호를 저장하는 방식으로 Bcrypt방식으로 암호화된 문자열 앞에 {bcrypt} 이런 문자열을 추가로 붙여줘야합니다.
Example 20에서의 id가 암호화 방식에 해당됩니다. 어쨌든 IllegalArgumentException 에러가 난다면 이러한 Storage format의 문제가 있어서 그런 것이므로 문자열을 수정해서 저장해주세요.
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
Configure 추가
저는 그냥 암호화된 문장을 그대로 저장하는 방식에서 이어서 진행해보겠습니다.
로그인 기능과 관련된 설정을 추가해줍니다. loginPage 지정해주고, login이 성공하면 어디로 보내버릴지(?) 정해줍니다.
Spring Security에서 모든 인증은 AuthenticationManager를 통해 이루어지며 AuthenticationManager를 생성하기 위해서는 AuthenticationManagerBuilder를 사용합니다. UserDetailService를 이용해서 인증에 필요한 정보들을 가져올 겁니다. userService 같은 경우에는 저번에 만들었지만 업데이트가 필요합니다. 바로 loadUserByUsername이라는 게 필요한데 이 친구까지 구현해보도록 하죠.
이 문단은 오로지 제 추측들을 나열한 문단이니 만약 잘못된 정보가 있다면 지적해주시면 감사하겠습니다. 이 새로 추가된 configure는 아마도 사용자 입력을 통해서 넘어온 데이터를 기준으로 인증해주는 파트가 아닌가 생각됩니다. 넘어온 비밀번호 값을 Bcrypt로 암호화하고, 그 값과 db에 저장된 패스워드를 비교해서 인증처리해주는 부분이라고 생각했습니다. 또한 제가 passwordEncoder부분부터 지우고 테스트를 진행하면 위에서 언급한 에러가 발생했습니다. 비밀번호 저장 시에 {bcrypt} 문자열을 붙여서 저장해줬더니 인증이 성공했고요.
아 참고로 사진에는 나와있지 않지만 userService를 생성자 주입해주셔야 합니다. 그렇지 않으면 <Error creating bean with name 'springSecurityFilterChain'>라는 에러가 발생합니다.
Repository 수정
findByUsername을 이용해서 User 테이블에서 username가 동일한 user를 찾아옵니다.
Service 추가
Service에서 loadUserByUsername을 추가했습니다. User table에서 제가 찾고자 하는 username이 없다면 에러 처리를 해주겠죠.
loadUserByUsername은 username(id값 의미)을 기준으로 사용자를 찾습니다. 실제 구현에서는 구현 인스턴스가 구성된 방식에 따라 대/소문자를 구분하거나 대/소문자를 구분하지 못할 수 있습니다. 이 경우 반환되는 UserDetails 개체에 실제로 요청된 사용자 이름과 다른 사용자 이름이 있을 수 있습니다.
User 수정
User가 UserDetails를 상속받도록 수정했습니다. 그래서 사실상 Service에서 (UserDetails)라고 캐스팅하는 부분이 제게는 필요가 없어요. 처음에는 저걸로 알아서 변환이 될지 궁금해서 해봤는데 안되더라구요.. 쭈굴... UserDetails를 상속받고 override 해야 하는 메서드들을 모두 null이나 true값 리턴하도록 했습니다.(지금 당장 제게는 필요하지 않은 부분이라서 그랬습니다.)
비밀번호 체크(아직 파악 못함)
여러분께 프론트 페이지가 이미 있다면 여기까지만 구현하셔도 로그인 기능이 제대로 동작합니다. 저는 이게 의문이었어요. findByUsername으로 입력한 username의 값을 가져왔겠죠. 그런데 그래서 비밀번호가 맞는지는 대체 어디서 확인하는 걸까요??
*// Create an encoder with strength 16*
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
공식 문서에서 알려주는 비밀번호가 맞는지 확인하는 구문입니다. 그런데 저는 이걸 사용한 기억이 없어요. 제가 공식 api문서를 파고파고 들어간 내용을 토대로 설명을 드려볼게요. 영어 오역으로 인해 잘못된 정보를 드릴지도 모르니 아예 문서를 캡처해왔습니다.
DAO를 통해서 user 정보를 받아오는 인터페이스입니다. 여기서 언급하는 DaoAuthenticationProvider 문서로 들어가 봅시다.
맨 위에 있는 Check 메서드를 이용하면 비밀번호 체크가 가능해 보입니다.
실제로 spring security github에서 해당 메서드를 찾아본 모습입니다. password matche를 하는 모습을 보면 이 메서드를 호출하면 비밀번호 비교가 가능하겠네요. 그런데 일단 dedeprecation이라고 경고하는 메시지가 붙어있고 실제로 쟤가 호출되는 것을 찾을 수가 없는데요..
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService)
.passwordEncoder(new BCryptPasswordEncoder());
}
또한 이 부분을 아예 없애버려도 로그인 인증이 됩니다. 대체.. 무엇에 의하여 정상적으로 비밀번호 체크가 되는지 제 눈으로 직접 확인해볼 수 없었습니다. 일단 스프링 비밀번호 체크 관련해서 HomoEfficio님께서 관련 내용을 정리해주셨습니다. 그러나 그것도 살짝 부족한 감이 있어서 추가 검색을 했습니다. 'spring security authentication architecture'라는 키워드를 통해서 찾아봤습니다.
springbootdev.com/2017/08/23/spring-security-authentication-architecture/
(여기 문단 글 정렬 왜이러죠..ㅜㅜ)
이 두 글을 통해서 제가 원하던 내용을 찾을 수 있었습니다. 다 비슷한 내용을 말하고는 있죠. UsernamePasswordAuthenticationToken을 AuthenticationManagager에게 넘깁니다. AuthenticationProvider에게 넘기면서 password 비교를 합니다. 여기서는 아까 제가 본 DaoAuthenticationProvider가 포함됩니다. spring security authentication 구조 자체가 저러하다면 이해할 수 있죠. 너무 알 수 없이 자동으로 이뤄지니까 당황했었는데 조금은 알 것 같아서 후련합니다. 위 두 포스팅은 킵해두고 계속 읽어야겠습니다.
참고
'web > spring&spring boot' 카테고리의 다른 글
[spring/spring boot] 스프링 부트 회원가입 후 자동로그인 기능 구현하기 (0) | 2021.05.17 |
---|---|
[spring boot] 로그아웃 기능 구현하기 (0) | 2021.05.15 |
[spring boot] 회원가입 기능 구현하기(2) - SecurityConfig 작성 (0) | 2021.04.23 |
[spring boot] spring security를 이용한 회원가입 기능 구현 (0) | 2021.04.22 |
[Gradle] compile 대신 implementation을 쓰는 이유 (0) | 2021.04.15 |