-

[스프링부트 및 JPA] - UserDetailService만들기 본문

백엔드 기술 정리/스프링 부트

[스프링부트 및 JPA] - UserDetailService만들기

흣차 2022. 2. 11. 16:50
728x90
반응형

이제는 로그인을 구현해보도록 하겠습니다.

마찬가지로 기본 프론트 양식은 갖추어져 있기 때문에 다듬는 과정으로 진행할 것입니다.

signin.jsp의 action과 method를 다음과 같이 바꿉니다.

이렇게 로직을 짜면  로그인할 때 제출 방식이 "POST"가 됩니다.

아마 이런 질문을 하실 수도 있을 것 같습니다.

"데이터를 입력하는데 도대체 왜 post방식을 써야 하나요?" 라고 말이죠.

지금 로그인은 데이터를 Insert하는 것이 아니라 데이터를 Select하는 것이 아닌가? 도 생각할 수 있습니다.

맞습니다.

원래는 아시다시피 Select할 때에는 Get방식을 채용해야 합니다.

그런데도 method = "POST"라고 입력한 이유는 username과 pw는 귀중한 정보이기 때문에 GET방식을 쓰게되면 주소창에 쉽게 노출이 되기 때문에 그렇습니다.

예날에 개발된 웹서비스들은 아직도 주소창에 아이디가 노출되는 경우가 종종 있습니다만 계속해서 없어지는 추세입니다.

그래서! Data를 Body에 담고 실어야 하기 때문에 Post방식을 써야 하는 것입니다.

예외적으로 로그인만 Post방식을 쓴다는 점을 꼭 기억해야겠습니다.

 

그리고 또 한가지

스프링부트에서는 로그인 컨트롤러에 대해서 따로 제작하지 않아도 됩니다.

회원가입할 때에는 

우리가 일일히 컨트롤러를 만들었잖아요???

하지만 스프링부트는 로그인 컨트롤러를 따로 만들지 않아도 자동으로 데이터만 Input해도 로그인을 지원해줍니다.

굉장히 편리하죠???

그래서 config패키지의 SecurityConfig클래스를 다음과 같이 바꿉니다.

여기서 주목할 것은 .loginPage는 GET방식으로 동작하고

.loginProcessingUrl은 POST방식으로 동작한다는 것입니다.

로그인 요청이 필요한, 인증이 필요한 요청을 하면 GET방식을 쓸 것이고 누군가가 로그인을 하려고 POST방식을 요청하면  .loginProcessingUrl이 실행될 것입니다.

이걸 보고 어떤 생각이 드시나요???

우리가 스프링에게 그냥 던져주기만 해도 알아서 로그인을 착착 진행한다는 점이 신기하지 않나요?

그리고 loginProcessingUrl이 낚아챈 데이터들은 넘어가서 로그인을 진행시켜줍니다.

 

계속 해보겠습니다.

config내에 auth라는 패키지를 만들고 PrincipalDetailsService라는 클래스를 생성합니다.

그리고 UserDetailsService를 implements하고 add unimplemented method를 눌러서 다음과 같이 작성합니다.

이 클래스도 Service의 범주에 속하기 때문에 위에 @Service어노테이션도 붙혀야 합니다.

여기서 주목해야할 것이 있습니다.

 

로그인 프로세스를 진행하는 과정에서 우리는 .loginProcessingUrl("/auth/signin")을 실행하여 스프링부트에게 로그인 과정을 위임시켰습니다.

스프링부트가 로그인하는 과정을 자세히 들여다보면 이 서비스는 스프링의 UserDetailsService가 처리합니다.

그런데 우리가 만든 것은 PrincipalDetailsService가 UserDetailsService를 implements하고 있었잖아요?

그럼 스프링은 어떤 생각을 하냐면 "어 이 로그인 과정은 UserDetailsService가 처리하는게 맞는데 PrincipalDetailsSerice도 같은 부모를 가지고 있으니 얘로 덮어씌워야 겠구나" 라고 생각을 합니다.

 

자 그렇다면 PrincipalDetailsService로 도대체 뭘 할건데? 

무엇을 만드는 것이 좋을까요?

스프링부트에서 지원하는 메서드 기본 양식을 보면 username만 파라미터로 넣어도 loadUserByUsername을 실행하는 것을 보면 username만 있어도 되는 것 같습니다.

즉, Password는 따로 구현하지 않아도 된다는 뜻입니다.

나머지는 스프링부트가 알아서 비교를 해주기 때문입니다!

그러므로 우리가 구현해야할 것은 username이 있는지 없는지만 체크하면 되겠죠?

그럼 이 username은 어디서 찾아야 하죠?

UserRepository에서 찾아야겠죠?

그런데 서비스에서 UserRepository를 찾으려면? 바로 의존성 주입(DI)가 필요하겠죠?

이전에 UserRepository를 만들어 두기만 하고 구현을 안했었는데 

이렇게 입력합니다.

JpaRepository 안을 들여다보면 PagingAndSortingRepository가 있고 그 안을 또 보면

얘는 CrudRepository를 상속합니다.

그리고 그 내부를 보면 save할 수도 있고 delete할 수도 있고 findById도 있는데,

여러가지를 제공해주지만 ID만 가지고 구현하기는 사실상 무리입니다.

그러므로 우리는 Jpa Query Creation을 사용할 것입니다. (JPA query method)

실제 스프링 공식 문서를 보면 findBy는 고정 Keyword이고 그 뒤의 문자들이 쿼리를 자동으로 만들어준다는 의미입니다.

저희는 비교적 간단한 것을 만들 것이기 때문에 (복잡한 것은 native query로 짜야합니다.) 

위와 같이 구현만 하면 끝입니다.

그 다음에 다시 PrincipalDetailsService로 돌아가서 찾아보겠습니다.

userEntity에 userRepository에서 찾은 username을 담고 if,else문으로 분기별 탐색을 진행합니다.

만약 userEntity가 null이면 return null일 것이고 else이면 userEntity를 보내고 싶은데 오류가 뜨죠?

UserDetails타입과 다르기 때문입니다.

여기서도 2가지 특징이 두드러지는데, 첫 번째로 패스워드는 자동으로 체킹되기 때문에 아이디만 찾아도 된다는 점. 

그리고 두 번째로 리턴이 잘되면 자동으로 세션을 만들어주기 때문에 결국 우리가 해야할 것은 return타입만 잘 생성해서 만들면 끝나는 것입니다.

return타입을 맞추어 봅시다.

UserDetails가 어떤 타입인지 보기 위해 클래스 내부로 들어가보았습니다.

음 이런것만 가지고 있네요.

인터페이스다 보니까 무엇을 return하는지 감이 안잡힙니다.

그럴 땐 우리가 직접 클래스를 생성해서 return해주어야 합니다.

이렇게 UserDetails를 implements하고 노란 줄 쳐져 있는 것의 add unimplemented method를 클릭합니다.

그럼 UserDetails가 가지고 있는 return타입을 모두 가져옵니다.

VersionUID도 하나 같이 생성하고

이렇게 자동 오버라이딩을 진행합니다.

이후 PrincipalDetailsService로 돌아와서 return new PrincipalDetails로 고쳐줍니다.

이러면 PrinicpalDetails만 손보면 거의 끝나네요!

이 과정을 통해 자동으로 UserDetails타입을 세션으로 만들 수 있습니다.

다시 PrincipalDetails로 돌아와서 다음과 같이 입력합니다.

이렇게 하면 

얘가 오류가 나는데, 여기에 userEntity를 넣어봅시다.

그럼 PrincipalDetails가 세션에 저장이 될 때 userEntity를 들고 있으니까 세션에 저장된 User 오브젝트를 이제부터 계속 활용할 수 있게 됩니다.

또한 PrincipalDetails 클래스로 돌아가보면 오류가 또 날텐데 그건 Getter,Setter가 없어서 그렇습니다.

마찬가지로 @Data를 붙혀주겠습니다.

그리고 다음과 같이 바꿔주는데, getPassword()와 getUsername()은 return으로 저렇게 작성하면 됩니다.

그 이하의 내용은 isAccountNonExpired() -> 계정이 만료된거 아니니? -> true

isAccountNonLocked() -> 계정이 잠긴거 아니니? -> true

isCredentialsNonExpired() -> 결제가 만료된거 아니니? -> true

isEnabled() -> 이용할 수 있니? -> true로 바꾸어줍니다.

이 메서드들은 나중에 회사갔을 때 서비스할 때 필요한 부분입니다.

개인정보 법에 의거해 1년이 지난 정보는 파괴해버린다던지 하는 약관이 있는데, 여기에서 구현을 할 수 있습니다.

우리가 만드는 서비스는 이런게 필요없기 때문에 모두 스킵하기 위해 true로 바꾸겠습니다.

만약 false가 되어있으면 로그인이 진행 안됩니다.

 

마지막으로 권한을 조금 손 보겠습니다.

return user.getRole()하고 싶은데 오류가 납니다.

이유는 Collection타입으로 return해야하기 때문에 그렇습니다.

우리의 권한이 한 개가 아닌 복수 개 일수도 있잖아요?

어떤 사람은 3개의 권한이 있을 수 있고 누구는 1개만 있을 수 있습니다.

그러므로 return할 때 저렇게 하지말고 

이렇게 입력할 수도 있습니다.

Collection의 타입은 GrantedAuthority이고 ceollector에 ArrayList타입으로 선언합니다.

ArrayList타입의 부모는 Collection타입이기 때문에 

참고하시면 되겠습니다.

어쨌든 이렇게 권한을 return하는 것을 해보았는데 굉장히 거추장스럽습니다.

그래서 등장한 것이 람다식이지요.

다음과 같이 바꾸어주어도 굉장히 깔끔하게 동작합니다.

람다식의 목적은 add안에 함수를 넣고 싶은 것이 목적입니다.

그러나 자바에서는 매게변수에 함수를 못넣습니다.

따라서 인터페이스를 넣어야 하는데 파라미터로 전달하고 싶으면 자바에서는 무조건 인터페이스를 넘기게 되어있습니다.

혹은 오브젝트를 넘겨도 되구요.

따라서 우리는 람다식으로 함수를 넘겨준 것입니다.

끝났습니다. 어려운건 거의 다했네요.

 

다음 포스팅에는 로그인된 세션을 어떻게 찾아 쓸 수 있는지 해보겠습니다.

 

728x90
반응형
Comments