빙응의 공부 블로그

[Spring]Spring Security + JWT(1) 본문

Spring/개인공부_실습

[Spring]Spring Security + JWT(1)

빙응이 2024. 4. 7. 17:14

📝JWT를 구현해보자! 

JWT를 간단하게 구현해보자.

나는 2가지 토큰을 이용한 방식을 사용하지 않고
1가지 토큰만 사용하는 방식을 사용하겠다!

개발자 유미 - YouTube

 

개발자 유미

백엔드 개발자 유미 - 실습 위주 진행 (개념적인 부분은 공식 Docs 참조 및 개인 학습 바랍니다!) - 간혹 댓글 알림이 안오는 경우가 있습니다.

www.youtube.com

이분꺼 보고 학습했으니 구독박자 

📝1. 회원가입 구현

회원가입은 통상의 Spring Security와 다른 점이 없다.

간단하게 중복 검사를 통한 회원 가입을 진행한다.

 🚩JWT에 초점을 맞춰 우리는 비밀번호 암호화를 제외한 Validation은 진행하지 않는다!

  @PostMapping("/join")
  public String joinProcess(JoinDTO joinDTO) {
    joinService.joinProcess(joinDTO);

    return "ok";
  }
@Service
@RequiredArgsConstructor
public class JoinService {

  private final UserRepository userRepository;
  private final BCryptPasswordEncoder bCryptPasswordEncoder;


  public void joinProcess(JoinDTO joinDTO) {

    String username = joinDTO.getUsername();
    String password = joinDTO.getPassword();

    Boolean isExist = userRepository.existsByUsername(username);

    if (isExist) {

      return;
    }

    UserEntity data = new UserEntity();

    data.setUsername(username);
    data.setPassword(bCryptPasswordEncoder.encode(password));
    data.setRole("ROLE_ADMIN");

    userRepository.save(data);
  }
}

📝2. 로그인 구현

🚩기존 Spring Security 방식은 DispatcherServlet(Controller) 전에 기본으로 내장된 로그인 필터를 이용해서 로그인 로직을 구현했습니다. 

  • 그러나 우리는 Spring Security의 formlogin을 사용하지 않기 때문에 직접 로그인 필터를 구현해주어야 한다.!

/**
 * 우리는 JWT로 인해 formLogin 방식을 사용하지 않기 때문에 UsernamePassweordAuthenticationFilter를 직접 커스텀으로 만들어 인증을 진행한다.
 */
@RequiredArgsConstructor
@Slf4j
public class LoginFilter extends UsernamePasswordAuthenticationFilter {

  private final AuthenticationManager authenticationManager;
  private final JWTUtil jwtUtil;


  @Override
  public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {

    String username = obtainUsername(request);
    String password = obtainPassword(request);
	// 사용자가 제출한 인증 정보로 Authentication 객체 생성
    UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
	// AuthenticationManager에게 인증을 위임
    return authenticationManager.authenticate(authToken);
  }

  @Override
  protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
	//나중에 로그인 성공 시 토큰을 만들어줄 메소드
  }

  @Override
  protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {

  }
}

 

🚩 attemptAuthentication 메소드

  • UsernamePasswordAuthenticationFilter에서 제공하는 로그인 시 자동으로 호출되는 메소드이다
  • 사용자의 username, password 입력을 받아 인증 처리를 위한 위임을 한다.

🚩 successfulAuthentication 메소드

  • attemptAuthentication에서 인증을 위임한 AuthenticationManager가 인증 성공 시 실행하는 메소드이다.
  • 인증 성공 후 토큰을 생성하는 메소드를 적을 장소이다.

🚩 unsuccessfulAuthentication 메소드

  • 로그인 인증에 실패 시 실행하는 메소드이다.
  • 보통 사용자 오류 처리가 이루어진다. 

📝3. Security Config 구현하기

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    //AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
    private final AuthenticationConfiguration authenticationConfiguration;


		//AuthenticationManager Bean 등록
		@Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {

        return configuration.getAuthenticationManager();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {

        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {


        http
                .csrf((auth) -> auth.disable());

        http
                .formLogin((auth) -> auth.disable());

        http
                .httpBasic((auth) -> auth.disable());

        http
                .authorizeHttpRequests((auth) -> auth
                        .requestMatchers("/login", "/", "/join").permitAll()
                        .anyRequest().authenticated());

	//필터 추가 LoginFilter()는 인자를 받음 (AuthenticationManager() 메소드에 authenticationConfiguration 객체를 넣어야 함) 따라서 등록 필요
        http
                .addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);

        http
                .sessionManagement((session) -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS));

        return http.build();
    }
}

 

🚩 우리가 만든 커스텀필터를 Security Config에 설정을 해주어야 한다. 

  • 또한 JWT는 무상태성이기 때문에 세션을 무상태성으로 설정해야한다.

 

📝4. 인증 객체 구현하기 

🚩우리는 인증을 하기 위한 필터를 구현하였지만 AuthenticationManager가 인증을 위해 사용할 객체는 구현하지 않았다.

/**
 * 스프링 시큐리티에서 사용자의 인증 정보를 로드하는데 사용
 */
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

  private final UserRepository userRepository;

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

    //DB에서 조회
    UserEntity userData = userRepository.findByUsername(username);

    if (userData != null) {

      //UserDetails에 담아서 return하면 AutneticationManager가 검증 함
      return new CustomUserDetails(userData);
    }

    return null;
  }
}

 

🚩 해당 메소드는 UserDetailsService를 상속받는 것으로 AutneticationManager가 검증을 위해 사용할 객체를 반환한다.

  • loadUserByUsername 메소드
    • 입력으로 들어온 username을 통해 비교할 객체를 가져온다. 
    • 이를 통해 검증을 시작하는 메소드이다. 
/**
 * 스프링 시큐리티에서 사용자의 정보를 표현하는데 사용
 */
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {

  private final UserEntity userEntity;


  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    // 사용자가 가진 권한을 담을 리스트
    List<GrantedAuthority> authorities = new ArrayList<>();

    // 사용자 엔티티로부터 권한 정보를 가져와서 권한 객체로 변환하여 리스트에 추가
    String role = userEntity.getRole();
    authorities.add(new SimpleGrantedAuthority(role));

    return authorities;
  }

  @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;
  }
}

🚩UserDatails는 Spring Security가 사용자 정보를 표현하기 위한 객체이다. 

  • 해당 인터페이스는 다음과 같은 역할을 가진다.
    • 사용자 인증 정보 제공
    • 사용자 정보 제공 : 로그인한 사용자의 정보를 제공하는 역할

[Spring]Spring Security + JWT(2) (tistory.com)

 

[Spring]Spring Security + JWT(2)

🚩 전 포스팅가기 [Spring]Spring Security + JWT(1) (tistory.com) [Spring]Spring Security + JWT(1) 📝JWT를 구현해보자! JWT를 간단하게 구현해보자. 나는 2가지 토큰을 이용한 방식을 사용하지 않고 1가지 토큰만 사

quddnd.tistory.com