Home [Spring Security JWT] DB기반 로그인 검증 로직 추가
Post
Cancel

[Spring Security JWT] DB기반 로그인 검증 로직 추가

이 게시글은 유튜브 개발자 유미님의 무료강의 스프링 시큐리티 JWT 8 : DB기반 로그인 검증 로직 을 수강하며 공부한 내용을 담고있습니다.
이 게시글은 공부한 내용을 제가 보기 편하게 정리한 내용이므로 정확한 정보는 위 영상을 시청해주세요.

이 게시글은 기본적인 Spring 기본 실습을 해봤다는 가정하에 기초적인 설명은 생략되어 있습니다.
또한 실습을 따라가며 기본적인 JWT 사용방법을 학습하기에 상세한 원리와 설명은 없는 경우가 있습니다.

개요

Login Filter 에서 작성했던 AuthenticationManager 에 대해 자세히 설명하지 않고 넘어갔다.
여기서 자세히는 아니고 간단히 짚고 넘어간다.
Authentication Manager 는 사용자의 username 과 password 를 검증하지 않는다.
Why???
이름을 보면 알 수 있듯 Manager 이기 때문이다.
식당 매니저, 옷가게 매니저 등 매니저는 보통 직접 손님을 응대하지 않는것처럼 말이다.
그럼 무엇을 하는가?
사용자가 건네준 정보를 적절한 로그인 인증 객체에게 넘겨주는 역할을 한다.
OAuth 인지, JWT 인지, username/password 인지 마다 다른 객체에게 인증을 수행하도록 요구를 한다.
그 객체들을 Authentication Provider(s) 라고 한다(여러개니까 s 가 붙음). 여기서는 username/password 방식을 사용하기 때문에 DAO Authentication Provider 에게 인증을 요구한다.

인증을 하려면 어떻게 해야할까?
사용자가 입력한 정보와 DB 에 저장된 정보를 비교한다.
Provider 는 사용자가 입력한 정보를 가지고 있지만 DB 에 저장된 정보가 없기 때문에 UserDetailsService 에게 요구한다.
UserDetailsService 는 DB 에서 사용자 정보를 조회해서 Provider 에게 넘겨주는데, 그냥 값을 대충 넘겨주는게 아니라 UserDetail 이라는 DTO 형식으로 줘야한다.

값을 넘겨받은 Provider 는 존재하는 사용자인지 검증을 한다.

findByUsername

UserRepository 에 username 으로 회원을 조회하는 메서드를 추가하자.
UserDetailsService 에서 필요하다.

1
2
3
4
5
public interface UserRepository extends JpaRepository<UserEntity, Integer> {
    Boolean existsByUsername(String username);
    UserEntity findByUsername(String username); // 추가
    // select * from userRepository where username = 매개변수;
}

UserDetailsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class CustomUserDetailsService implements UserDetailsService {
    UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 회원 조회
        UserEntity userData = userRepository.findByUsername(username);
        if(userData != null) {
            // 조회한 userData 를 UserDetails 라는 DTO 에 담아서 반환한다.
            // 그럼 Provider 가 검증을 한다.
            return new CustomUserDetails(userData);
        }
        // 조회된 user 가 없다.
        return null;
    }
}

UserDetailsService 를 만들긴할건데, 아얘 통째로 만드는게 아니라 UserDetailsService 를 상속받아서 만들면 된다.
상속을 받으면 loadUserByUsername 를 Override 하고 내부에서 회원정보를 반환하면 끝난다.
반환할 때는 UserDetails 라는 DTO 에 담아서 반환하면 되는데, 이것도 마찬가지로 UserDetails 만드는게 아니라 상속받아 만들면 된다.

UserDetails

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class CustomUserDetails implements UserDetails {
    private final UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity) {
        this.userEntity = userEntity;
    }

    // Role 값을 반환
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();

        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return userEntity.getRole();
            }
        });

        return collection;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getUsername();
    }

    // 계정 만료 여부
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정 잠김 여부
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 비밀번호 만료 여부
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정 활성화 여부?
    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserDetails 를 implement 한 후 일단 전부 Override 하면 된다.
멤버 변수로 UserEntity 추가 후 생성자 주입을 작성한다.

getAuthorities() 메서드는 role 값을 반환한다.
Collection 으로 반환해야 하는거 보면 뭔가 있는것 같은데, 일단 우리는 복잡하지 않으므로 간단명료하게 role 값을 collection 에 add 하고 그 collection 을 return 하면 된다.

getPassword, getUsername 은 userEntity 에 있는 값을 그대로 반환하도록 작성한다.

그 아래에 계정만료, 잠김, 활성화 여부 등은 DB 에 추가하지 않았었다. 그래서 여기서는 그냥 무조건 만료되지 않았고 잠기지 않았고, 활성화 되어있다는 의미로 true 를 리턴하도록 한다.

Login Filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

    // 생략

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // 인증 성공 메서드
        // 5-1. authenticationManager.authenticate(authToken) 이 성공하면 이 메서드가 실행됨
        System.out.println("로그인 성공");
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // 인증 실패 메서드
        // 5-2. authenticationManager.authenticate(authToken) 이 실패하면 이 메서드가 실행됨
        System.out.println("로그인 실패");
    }
}

이제 Login Filter 로 돌아가서 successful/unsuccessfulAuthentication 내부에 System.out.println() 을 작성하고 로그인 테스트를 한다.

Test

앞서 JoinService 에서 회원가입한 username, password 로 테스트 하면 잘 될 것이고, 없는 username, password 로 하면 오류메시지와 로그인 실패가 뜰 것이다.
없는 회원으로 로그인하면 오류가 발생한다.
CustomUserDetails 의 UserEntity 가 null 이므로 getRole, getUsername 등에서 오류가 발생하는 것으로 추정된다.



이전 포스팅 : 로그인 필터
다음 포스팅 : JWT 발급 메서드 추가

This post is licensed under CC BY 4.0 by the author.

[Spring Security JWT] 로그인 필터

[Spring Security JWT] JWT 발급 메서드 추가