Restful API 구현을 위한 Spring Security 설정해보기 #2 (UserDetails, UserDetailsService 인터페이스 구현)

안녕하세요. 오늘은 이전 포스팅에서 다뤘던 Java 설정파일에 이어서 인증로직을 구현해보겠습니다.

 

흐름을 파악하시기 위해서 이전 포스팅 내용을 확인 후 진행하시는 편을 추천드립니다.!

https://sas-study.tistory.com/357

 

 


 

이전 포스팅에서는 별다른 인증로직 없이 /permit-all 과 /auth 매핑 주소로 인증을 나눠봤습니다.

단순히 permit-all은 인증 여부와 관련없이 통과되는 url이고, /auth는 인증여부에 따라 성공하거나 실패하는 경우로 나눠봤어요.

 

그런데 /auth는 인증로직이 별도로 구현되지 못했기 때문에 항상 403(Forbidden)을 내려줬을 것입니다.

 

오늘 해볼 작업은 /auth url 에서 200(성공) 코드를 얻어보는 작업을 해보겠습니다.

 

그 중에서 필요한 인터페이스 두가지가 있습니다.

 

- UserDetails

- UserDetailsService

 

이 두 객체는 각각 인증대상객체(흔히, 로그인 객체)인증로직서비스로 보시면 될 것 같습니다.

 


 

먼저 두 인증관련 인터페이스가 어떻게 동작하는지 살펴보겠습니다.

 

UserDetails 는 말그대로 우리가 구현할 User 객체입니다. 이를 UserDetailsService 에서 구현하여 loadUserByUsername 라는 오버라이딩 메소드에서 Request에서 받은 로그인 데이터를 활용하여 로그인 작업을 해주면 되는 것입니다. 이때 loadUserByUsername 메소드는 인증된 결과를 가지고 UserDetails 인터페이스를 구현하는 인증대상객체를 리턴해줍니다.

 

말로 설명하는 것보다 코드를 보는 것이 훨씬 도움이되니 바로 프로젝트 구조부터 시작해서 코드리뷰를 시작하겠습니다.

 


프로젝트 구조

 

- CustemUSerDetailsService 클래스와 SecurityUser 클래스를 생성해줍니다.

- 이 두 클래스는 다들 아시겠지만 각각 UserDetailsService와 UserDetails 인터페이스를 구현하는 구현클래스입니다.

 

SecurityUser.java

public class SecurityUser implements UserDetails {

    //field 영역
    
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return "test";
    }

    @Override
    public String getUsername() {
        return "test";
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

- 간단한 예를 위하여 불필요한 필드는 따로 작성하지 않았습니다. 별도로 실전에서 활용하시고 싶으신 필드를 작성하시고 Username이 프로젝트마다 지칭하는 로그인 아이디로 두시고 패스워드 별도로 두시면 모두 활용 가능합니다.!

- 일단 UserDetails 인터페이스를 구현하면 여러가지 오버라이딩 메소드들이 구현됩니다. 대부분 기본값이 false이거나 null 인데 이를 위와같이 바꿔줍니다. -> 메소드 이름을 읽어보시면 좀더 이해가 되실 겁니다.

- 현재 생성한 SecurityUser 라는 클래스의 인스턴스가 Spring Security 컨텍스트 내에 존재하는 인증정보에 집어넣어 주는 것이 목표입니다.

 

CustomUserDetailsService.java

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String loginId) 
    								throws UsernameNotFoundException {

        System.out.println("인증을 받습니다.");
//로그인 로직 시작
        
        // loginId를 이용하여 DB에서 User 객체를 가져옵니다.
        // User user = mapper.getUser(loginID);
        // User의 정보를 SecurityUser 에 담아줍니다. 이는 생성자를 이용하는 편입니다.!
        
        return new SecurityUser();
    }

}

- 로그인 로직입니다. 

- 실제로는 이 부분에서 Mapper 라던지 Repository라던지 DI가 발생하여 의존성을 주입해주고 디비로 다녀와서 id에 맞는 데이터를 가져와서 Security 객체에 담아주는 작업을 할 것입니다.

- 여기서 UserDetails 인터페이스 구현 클래스의 인스턴스를 리턴해주므로 이제 로그인 로직 작성을 했습니다.

 


 

그런데.. ID 만으로 인증하는 형태로 보여집니다. 근데 이게 어떻게 완벽한 인증로직일까요? 아이디만 일치하고 비밀번호가 일치하지 않을 수가 있잖아요!!??

이것은 저도 처음에 의문을 품었던 질문입니다. 아이디만으로 객체를 가져오고 그것만으로 인증이 끝났다고하면 비밀번호가 필요없게 되버려서 의아했었죠.

 

근데 답은 스프링 시큐리티 내부에 있어 우리 눈에서 보이지 않아 확인하기 어려웠던 것입니다.

 

일단 기준이 되는 데이터는 User 객체에서 받은 데이터입니다. 여기에 아이디와 패스워드가 들어있을 것이고 이는 DB에 저장된 값이므로 회원가입 당시에 입력된 데이터일 것입니다.(별도로 비밀번호를 수정하지 않았다면요)

 

이렇게 DB 정보를 SpringSecurity에서 받아오고 난 후! 사용자 입력데이터와 비교하는 것입니다.

 

loadUserByUsername을 호출하여 인증을 마치고 내부적으로 SecurityUser 클래스와 Password를 passwordEncorder를 이용하여 처리할 수도 있도록 하는 서비스 로직이 존재합니다.

 

 

이에 대해서는 다음 포스팅에서 설명해드리겠습니다.

이번프로젝트를 실행해도 /auth는 인증되지 않습니다. 별도의 인증을 담당하는 Filter가 필요한데 이번장에서 설명하는 것보다 다음장에서 설명드리도록 하겠습니다.

 

위의 질문에 대한 대답도 될 것입니다. 감사합니다.

 

댓글

Designed by JB FACTORY