코딩과 결혼합니다

231013 - [Game_Crew] 로그인 구현 본문

코딩과 매일매일♥/Game_Crew

231013 - [Game_Crew] 로그인 구현

코딩러버 2023. 10. 13. 17:16
728x90

사용자의 세부정보를 제공하는 클래스 

public record UserDetailsImpl(User user) implements UserDetails {

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.emptyList();
    }

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

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

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

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

 

📌레코드 타입
java 14부터 도입된 데이터 저장용 타입으로, 불변성과 패턴 매칭 기능을 제공한다.
UserDetailsImpl 클래스의 필드와 관련된 게터와 생성자 등을 제거할 수 있어 코드가 좀 더 간결해진다.


사용자의 세부 정보를 데이터베이스에서 가져와 UserDetailsImpl 객체로 변환하여 반환

@Service
@Setter
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + email));

        return new UserDetailsImpl(user);
    }
}

사용자의 로그인 요청을 처리하고 JWT 토큰을 생성하여 응답 바디에 담아 전송

 

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

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/auth/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.getEmail(),
                            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();

        String token = jwtUtil.createToken(username);

        // JWT 토큰을 응답 바디에 담아 전송
        try {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getWriter().write("{\"token\": \"" + token + "\"}");
            response.getWriter().flush();
            response.getWriter().close();
        } catch (IOException e) {
            log.error("Failed to write token to response body: {}", e.getMessage());
            throw new RuntimeException("Failed to write token to response body");
        }
    }

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

 

  • setFilterProcessesUrl() 메서드를 호출하여 로그인 요청 URL을 "/auth/login"로 설정
  • attemptAuthentication(): 사용자의 로그인 요청을 처리. 전달받은 HttpServletRequest에서 입력 스트림으로부터 LoginRequestDto 객체를 읽어 오고 그 후, AuthenticationManager로 UsernamePasswordAuthenticationToken 객체를 생성하고 인증을 시도
  • successfulAuthentication(): 인증에 성공한 경우 호출. Authentication 객체에서 인증된 사용자의 이메일(username) 정보를 가져와 JwtUtil.createToken() 메서드를 사용하여 JWT 토큰을 생성. 그리고 응답 바디에 JWT 토큰 값을 JSON 형식으로 담아 전송
  • unsuccessfulAuthentication():  인증에 실패한 경우 호출. 응답 상태 코드(401 Unauthorized)를 설정하여 클라이언트에게 실패했음을 알림

클래스 요청마다 실행되며, 전달된 JWT 토큰을 검증하고 인가된 사용자의 보안 컨텍스트에 인증 정보를 설정

@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {

        String tokenValue = jwtUtil.getJwtFromHeader(req);

        if (StringUtils.hasText(tokenValue)) {

            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

            try {
                setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }

    // 인증 처리
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    // 인증 객체 생성
    private Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

 

  • doFilterInternal(): 요청마다 호출되어 JWT 토큰을 검증하고 인증 처리를 수행한다. 먼저, HttpServletRequest에서 JWT 토큰 값을 가져오고 그 후, jwtUtil.validateToken() 메서드를 사용하여 토큰의 유효성을 검사. 유효하지 않으면 로그에 오류 메시지를 기록하고 종료
  • getUserInfoFromToken():  jwtUtil.getUserInfoFromToken() 메서드를 호출하여 JWT 토큰에서 클레임(Claims) 정보(예: 사용자 이름)를 추출
  • setAuthentication():  SecurityContextHolder에서 보안 컨텍스트(SecurityContext)와 인증(Authentication) 객체를 생성하여 설정
  • createAuthentication(): userDetailsService.loadUserByUsername(username)을 호출하여 주어진 username으로부터 UserDetails 객체(인증에 필요한 세부 정보)를 가져온다. 그리고 UsernamePasswordAuthenticationToken 객체로 인증(Authentication) 객체를 생성하여 반환