• 인증 과정
    1. 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilter는 인증된 사용자의 정보가 담기는 인증 객체인 Authentication의 종류 중 하나인 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에게 넘겨 인증을 시도합니다.
    2. 실패하면 SecurityContextHolder를 비웁니다.
    3. 성공하면 SecurityContextHolder에 Authentication를 세팅합니다.

 

 

Authentication 객체의 구성 요소

  • Authentication이란? 현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있습니다.
  • principal:
    • 의미: 현재 인증된 사용자를 식별하는 정보입니다.
    • 일반적인 타입: UserDetails 인스턴스가 일반적입니다. UserDetails는 사용자에 대한 정보를 담고 있는 인터페이스로, 사용자 이름(username), 비밀번호(password), 권한(authorities) 등을 포함합니다.
    • 설명: 로그인 시 제공된 사용자 이름(username) 또는 사용자 객체(user)를 나타냅니다. 인증이 완료된 후에는 UserDetails 인스턴스가 여기에 저장됩니다.
  • credentials:
    • 의미: 인증 과정에서 사용되는 자격 증명입니다. 보통 비밀번호를 의미합니다.
    • 일반적인 타입: 비밀번호 문자열(String)입니다.
    • 설명: 인증 과정에서 사용된 비밀번호가 여기에 저장됩니다. 인증이 완료된 후에는 이 정보는 일반적으로 비워집니다. 이는 보안상의 이유로 비밀번호를 더 이상 저장할 필요가 없기 때문입니다.
  • authorities:
    • 의미: 인증된 사용자에게 부여된 권한을 나타냅니다.
    • 일반적인 타입: GrantedAuthority 인터페이스를 구현한 객체들의 컬렉션입니다. GrantedAuthority는 사용자의 권한을 추상화하는 역할을 합니다.
    • 설명: 사용자가 수행할 수 있는 작업의 범위를 정의합니다. 예를 들어, ROLE_USER, ROLE_ADMIN 등의 권한이 이에 해당합니다. 권한은 인증 후에 설정되며, Authentication 객체가 사용자의 권한을 기반으로 접근 제어를 수행할 수 있게 해줍니다.

 

package com.sparta.myselectshop.security;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.myselectshop.dto.LoginRequestDto;
import com.sparta.myselectshop.entity.UserRoleEnum;
import com.sparta.myselectshop.jwt.JwtUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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;

import java.io.IOException;

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        // 이 필터가 /api/user/login 경로에서만 작동하도록 설정
        setFilterProcessesUrl("/api/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role);
        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, token);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
        response.setStatus(401);
    }

}

 

 

 

 

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

ObjectMapper란?

  • ObjectMapper는 Jackson 라이브러리에서 제공하는 클래스입니다. Jackson은 Java 객체와 JSON 간의 변환을 쉽게 할 수 있도록 도와주는 라이브러리입니다.
  • ObjectMapper는 JSON 데이터를 Java 객체로 역직렬화(deserialize)하거나, Java 객체를 JSON 형식으로 직렬화(serialize)하는 데 사용됩니다.

request.getInputStream()

  • request.getInputStream()은 HttpServletRequest 객체에서 요청 본문을 읽어들이는 데 사용됩니다.
  • 이 메서드는 요청 본문을 바이트 스트림 형태로 반환합니다. 주로 POST, PUT, PATCH와 같은 HTTP 메서드에서 JSON 데이터를 서버로 전송할 때 사용됩니다.
  • 예를 들어, 클라이언트가 로그인 요청을 보낼 때 사용자 이름과 비밀번호를 JSON 형식으로 본문에 담아 보낼 수 있습니다.

new ObjectMapper().readValue()

  • ObjectMapper의 readValue 메서드는 JSON 형식의 데이터를 읽어 Java 객체로 변환하는 역할을 합니다.
  • 이 메서드는 두 가지 인수를 받습니다:
    1. InputStream: JSON 데이터를 담고 있는 입력 스트림 (request.getInputStream()).
    2. Class<T>: JSON 데이터를 변환할 Java 클래스 (LoginRequestDto.class).
 LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

즉, 위 코드는 클라이언트로부터 받은 JSON 데이터를 LoginRequestDto 객체로 변환합니다.

 

 

getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )

UsernamePasswordAuthenticationToken은 스프링 시큐리티 Authentication 객체의 구현체이므로 principal, credentials, Authorities 필드를 갖습니다.

 

UsernamePasswordAuthenticationToken을 생성할 때 세 번째 인자인 authorities를 null로 설정하는 이유 :

  1. 초기 인증 요청: 초기 인증 요청 시점에서는 사용자의 권한이 결정되지 않았기 때문에, authorities는 null로 설정됩니다. 인증 관리자(authentication manager)는 이 객체를 사용하여 인증을 시도하고, 인증이 성공하면 권한을 설정한 후에 Authentication 객체를 반환합니다.
  2. 권한 설정은 인증 후에: 권한(authorization)은 인증(authentication)이 완료된 후, 즉 사용자가 성공적으로 로그인한 후에 설정됩니다. 따라서 초기 UsernamePasswordAuthenticationToken 객체는 권한 정보가 필요 없으며, 인증이 완료된 후에 권한이 설정된 Authentication 객체로 대체됩니다.

 

 

 

'스프링 시큐리티' 카테고리의 다른 글

삭제된 유저 로그인 실패 처리하는 법  (0) 2024.08.28
 

삭제 처리된 유저가 로그인을 못하게 막으려면, 인증 과정에서 유저의 isDeleted 플래그를 확인하여 삭제된 유저인 경우 로그인 요청을 거부하도록 해야 합니다. 이를 구현하기 위해서는 JwtAuthenticationFilter 또는 UserDetailsService에서 추가적인 검증 로직을 넣으면 됩니다.

방법 1: UserDetailsService에서 검증

import com.sparta.delivery.user.User;
import com.sparta.delivery.user.UserRepository;
import com.sparta.delivery.security.UserDetailsImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        if (user.isDeleted()) {
            throw new RuntimeException("This account has been deleted.");
        }

        return new UserDetailsImpl(user);
    }
}

 

 

 

방법 2: JwtAuthenticationFilter에서 검증

import com.sparta.delivery.user.User;
import com.sparta.delivery.user.UserRepository;
import com.sparta.delivery.security.UserDetailsImpl;
import com.sparta.delivery.jwt.JwtUtil;
import com.sparta.delivery.user.dto.LoginRequestDto;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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;

import java.io.IOException;

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;
    private final UserRepository userRepository;

    public JwtAuthenticationFilter(JwtUtil jwtUtil, UserRepository userRepository) {
        this.jwtUtil = jwtUtil;
        this.userRepository = userRepository;
        setFilterProcessesUrl("/api/users/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

            User user = userRepository.findByUsername(requestDto.getUsername())
                    .orElseThrow(() -> new RuntimeException("Invalid username or password"));

            if (user.isDeleted()) {
                throw new RuntimeException("This account has been deleted.");
            }

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }
}

 

 

일반적으로는 UserDetailsService에서 사용자 상태를 검증하는 것이 더 적합하고 권장되는 방식입니다. 이 방식은 로그인 과정에서의 상태 검증을 명확하게 분리하며, 보안 관련 로직을 중앙에서 관리할 수 있어 유지보수와 확장성 측면에서도 유리합니다

+ Recent posts