[스프링 시큐리티 JWT] 로그인을 해보자 + JWT 로그인 과정

코린이서현이·2024년 6월 18일
0
post-thumbnail

들어가면서

jwt 로그인

💻 공식 문서

JWT를 사용하는 프로젝트의 로그인은 뭐가 다른데?

회원가입로직은 저번 글에서 이미 다룸

jwt 로그인 과정


1. 사용자의 요청이 필터를 타고 ID와 비밀번호를 가지고 들어오면 UserPasswordAuthenticationFilterusernamepassword를 꺼내서 로그인을 진행하기 위해서 AuthenticationManager에게 넘겨준다.

  1. AuthenticationManager은 DB로부터 회원정보를 가져와서 검증을 진행한다.

  2. 검증이 완료되면 sucessfullAutn이 동작하는 데, JWT를 만들어서 사용자에게 응답을 준다.

jwt 로그인의 특징

  1. UserPasswordAuthenticationFilter을 직접 만들어야한다.
    jwt방식을 사용하면 Form로그인방식을 사용하지 않기 때문에 직접 구현해야한다.

시큐리티 필터 동작 과정

스프링 시큐리티는 클라이언트의 요청이 여러개의 필터를 거쳐 
최종적으로는 DispatcherServlet(Controller)으로 향한다.
향하는 중 중간 필터에서 요청을 가로챈 후 검증(인증/인가)을 진행한다.

스프링 부트는 Tomcat위에서 동작한다.

스프링 부트 어플리케이션는 TomCat이라는 서블릿 컨테이너 위에서 동작한다.
요청이 왔을 경우 서블릿 필터를 다 통과한 이유 스프링 부트에 전달된다.

필터 등록

필터가 요청을 가로채서 회원정보를 검증하게 된다.

Delegating Filter Proxy(서블릿필터)

모든 요청을 시큐리티 필터로 전달한다.(가로채서 보낸다.)
가로챈 요청은 SecurityFilterChain으로 전달해 적절하게 거부,리디렉션, 서블릿을 요청 전달을 진행한다.

SecurityFilterChain의 필터 목록과 순서

폼 로그인에서의 UsernamePasswordAuthenticationFilter

  • Form 로그인 방식에서는 클라이언트단이 username과 password를 전송한 뒤 Security 필터를 통과하는데 UsernamePasswordAuthentication 필터에서 회원 검증을 시작한다.

따라서 강제로 우리가 커스텀해서 진행해야한다.

실제 코드짜기

아이디, 비밀번호 검증을 위한 커스텀 필터 작성, UserPasswordAuthenticationFilter

LoginFilter

  • LoginFilterUserPasswordAuthenticationFilter을 상속받아 구현한 jwt필터이다.
  • usernamepassword를 꺼내서 로그인을 진행하기 위해서 AuthenticationManager에게 넘겨준다.
package com.jwt.jwtstudy_youtube.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;

    public LoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    @Override //필수 -> ⭐ 로그인 전달 메서드
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        //클라이언트 요청에서 username, password 추출
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);

		//인증진행 -> AuthenticationManager에게 DTO라는 바구니에 담아서 유효성 검사 맡기기
        return authenticationManager.authenticate(authToken);
    }


    //로그인 성공시 실행 메소드
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
        //(여기서 JWT를 발급하면 됨)
        System.out.print("성공==============================");
    }

    //로그인 실패시 실행 메소드
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        System.out.println("실패==============================");
    }
}

DB에 저장되어 있는 회원 정보를 기반으로 검증할 로직 작성

사용자 유효성 검사하는 CustomUserDetailsService 구현

package com.jwt.jwtstudy_youtube.service;


import com.jwt.jwtstudy_youtube.dto.CustomUserDetails;
import com.jwt.jwtstudy_youtube.entity.UserEntity;
import com.jwt.jwtstudy_youtube.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;


@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

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

    
    @Override //필수
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //구현해야할 동작
        // 1. 사용자 유효한지 검사
        // -> 유효한 경우 UserDetails 반환

        UserEntity userData = userRepository.findByUsername(username);
        if (userData != null) {
            return new CustomUserDetails(userData);
        }
        return null;
    }
}

loadUserByUsername 메서드의 역할

username을 기반으로 사용자 정보를 로드하고 이를 UserDetail로 반환해 리턴한다.

만약 jwt가 아니라 세션을 사용하는 프로젝트라면?!

  • UserDetailsServiceloadUserByUsername의 반환값을 세션에 저장할 것이다. (로그인 완료)

그러나 이 프로젝트는 jwt를 사용하는 프로젝트 🪄

  • 세션을 서버에서 유지하지 않는다. (클라이언트에서 상태를 유지))
  • 유효한 사용자라고 판단한 경우 클라이언트에게 JWT를 반환한다.
  • 이후 클라이언트가 재 요청을 할때, 다시 토큰을 전달받아 사용자를 인증한다.
    (이후 클라이언트는 서버에 요청을 보낼 때, JWT를 포함해서 보낸다.
    서버는 JWT를 검증하고, 토큰이 유효한 경우 토큰에 포함된 사용자 정보를 기반으로 UserDetails 객체를 생성한다.)

결국 우리 프로젝트에서는, 유효성 검사 역할을 수행한다.

CustomUserDetails 구현

package com.jwt.jwtstudy_youtube.dto;

import com.jwt.jwtstudy_youtube.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.ArrayList;
import java.util.Collection;

public class CustomUserDetails implements UserDetails {

    private final UserEntity userEntity;

    public CustomUserDetails(UserEntity userEntity) {
        this.userEntity = userEntity;
    }
    
    @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;
    }
}

getAuthorities() 메서드

    : 인증된 사용자가 가진 권한을 반환하는 메서드다.
    보통 어떤 URL에 대한 접근을 허용 여부 결정에 쓰인다.
    따라서 Spring Security는 이 메서드를 사용하여 
    사용자가 특정 리소스, 기능에 접근 권한이 있는지 확인할 수 있다.

    반환값 : Collection<? extends GrantedAuthority>
        - 사용자가 가진 모든 권한 ("ROLE_USER", "ROLE_ADMIN"...) 모두를 반환한다.

커스텀 필터 LoginFilter 등록

package com.jwt.jwtstudy_youtube.config;

import com.jwt.jwtstudy_youtube.jwt.LoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;

@Configuration
@EnableWebSecurity //security를 위한 것이라고 알려주는 기능
public class SecurityConfig {

    private final AuthenticationConfiguration authenticationConfiguration;

    public SecurityConfig(AuthenticationConfiguration authenticationConfiguration) {
        this.authenticationConfiguration = authenticationConfiguration;
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		~~~ 이 부분은 다른 글 참조 ~~~
        //jwt로그인을 위한 필터등록
        http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);
		
        return http.build();
    }
}

새로 알게 된 점!!

LoginFilter가 상속받은 UsernamePasswordAuthenticationFilter

잘 보면 이 글에서 따로 컨트롤러를 두지 않은걸 볼 수 있다.
또한 어떤 코드에서도 "/login" 경로를 지정하지 않은 걸 확인할 수 있다.

그러면 지정 부분은 어디있을까..?!!?

바로 UsernamePasswordAuthenticationFilter에 있다 ^__^

private static final AntPathRequestMatcher
 DEFAULT_ANT_PATH_REQUEST_MATCHER = 
					new AntPathRequestMatcher("/login","POST");

따라서 JWT 로그인시 "/login" 주소로 POST 방식 요청하지 않으면 원하는 대로 동작하지 않으니까 주의하자! 🩷👍👍

마무리하면서

새로 알게 되는 부분이 있으면 뿌듯하고 실력이 향상된다고...(아닌가요?ㅜ) 생각돼서
공부할 재미가 난다 🫶🫶 
profile
24년도까지 프로젝트 두개를 마치고 25년에는 개발 팀장을 할 수 있는 실력이 되자!

0개의 댓글