403 인증 토큰을 가져오지 못하고 리다이렉트 실패하는 에러.

24.03.10 22:05 작성 조회수 92

0

403 에러로 고생을 겪고 있습니다. fetch에러로, 인증 문제 로그인 페이지로 리다이렉트 되야 되는데 서버에서 받지 못해 로그에는 [nio-8080-exec-7] c.e.d.security.JwtAuthenticationFilter : Received token from request: null 이렇게 뜹니다.

서버측 코드

package com.example.demo.security;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            // 요청에서 토큰 추출 및 로깅
            String token = parseBearerToken(request);
            log.info("Received token from request: {}", token);

            if (token != null && !token.equalsIgnoreCase("null")) {
                // 토큰 검증 및 로깅
                String userId = tokenProvider.validateAndGetUserId(token);
                log.info("Authenticated user ID: {}", userId);

                // 인증 완료; SecurityContextHolder에 등록
                AbstractAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userId,
                        null,
                        AuthorityUtils.NO_AUTHORITIES
                );
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
                securityContext.setAuthentication(authentication);
                SecurityContextHolder.setContext(securityContext);
            }
        } catch (Exception ex) {
            log.error("Could not set user authentication in security context", ex);
        }

        filterChain.doFilter(request, response);
    }

    private String parseBearerToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");

        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}
package com.example.demo.security;

import com.example.demo.model.UserEntity;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.SignatureAlgorithm;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.security.Key;

@Slf4j
@Service
public class TokenProvider {
	private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);

    public String create(UserEntity userEntity) {
        // 기한을 현재부터 1일로 설정
        Date expiryDate = Date.from(
                        Instant.now()
                        .plus(1, ChronoUnit.DAYS));

        // JWT Token 생성
        return Jwts.builder()
                // header에 들어갈 내용 및 서명을 하기 위한 시크릿 키
                .signWith(key)
                // payload에 들어갈 내용
                .setSubject(userEntity.getId()) // sub
                .setIssuer("demo app") // iss
                .setIssuedAt(new Date()) // iat
                .setExpiration(expiryDate) // exp
                .compact();
    }

    public String validateAndGetUserId(String token) {
        // parseClaimsJws 메서드가 Base64로 디코딩 및 파싱.
        // 즉, 헤더와 페이로드를 setSigningKey로 넘어온 시크릿을 이용해 서명 후, token의 서명과 비교.
        // 위조되지 않았다면 페이로드(Claims) 리턴
        // 그 중 우리는 userId가 필요하므로 getBody를 부른다.
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        return claims.getSubject();
    }
}

 

 

package com.example.demo.config;

import com.example.demo.security.JwtAuthenticationFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.web.SecurityFilterChain;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.cors.CorsConfiguration;

@EnableWebSecurity
@Slf4j
@Configuration
public class WebSecurityConfig {

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
        .cors(cors -> cors.configurationSource(request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true);
            config.addAllowedOriginPattern("http://localhost:3000"); // 변경된 부분
            config.addAllowedHeader("*");
            config.addAllowedMethod("*");
            return config;
        }))// CORS 설정
            .csrf(csrf -> csrf.disable()) // CSRF 설정 비활성화
            .httpBasic(httpBasic -> httpBasic.disable()) // HTTP Basic 인증 비활성화
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 관리 설정

.authorizeHttpRequests((authorizeRequests) ->
                authorizeRequests
                    .requestMatchers("/", "/auth/**","/login").permitAll()
                    .anyRequest().authenticated()
            )

            .addFilterAfter(jwtAuthenticationFilter, CorsFilter.class);

        return http.build();
    }
}

 

프론트엔드 측 코드

// ApiService.js

import { API_BASE_URL, ACCESS_TOKEN } from "../app-config";

export function call(api, method, request) {
  let headers = new Headers({
    "Content-Type": "application/json",
  });

  const accessToken = localStorage.getItem(ACCESS_TOKEN);
  if (accessToken && accessToken !== null) {
    headers.append("Authorization", "Bearer " + accessToken);
  }

  let options = {
    headers: headers,
    url: API_BASE_URL + api,
    method: method,
  };

  if (request) {
    options.body = JSON.stringify(request);
  }

  return fetch(options.url, options)
    .then((response) => {
      if (!response.ok) {
        throw new Error(response.status);
      }
      return response.json();
    })
    .catch((error) => {
      console.log(error);
      if (error.message === "403") {
        window.location.href = "/login";
      }
      return Promise.reject(error);
    });
}

export function signin(userDTO) {
  return call("/auth/signin", "POST", userDTO).then((response) => {
    if (response.token) {
      localStorage.setItem(ACCESS_TOKEN, response.token);
      window.location.href = "/";
    }
  });
}

export function signout() {
  localStorage.setItem(ACCESS_TOKEN, null);
  window.location.href = "/login";
}

export function signup(userDTO) {
  return call("/auth/signup", "POST", userDTO);
}

 

프론트엔드에서 토큰을 생성하고 서버와 통신하는데, 403에러로 인증이 되지 않은 사용자는 login 페이지로 가는 것인데 처음부터 로그인 페이지가 뜨질 않습니다.

 

답변 1

답변을 작성해보세요.

0

인프런 AI 인턴님의 프로필

인프런 AI 인턴

2024.03.11

안녕하세요, 인프런 AI 인턴입니다.

왓챠가짱님, 문제를 해결하기 위해 몇 가지 확인해야 할 점이 있습니다.

  • 서버 측 JwtAuthenticationFilter 작동 여부: 토큰 파싱이 정상적으로 이루어지고 있는지 로그를 통해 확인합니다. log.info("Received token from request: {}", token); 이 부분에서 null 이 로그로 찍히고 있다면, 헤더에서 토큰을 제대로 가져오지 못하는 조건입니다.

  • 토큰의 유효성 검증: 서버 측 TokenProvider에서 토큰이 유효한지 확인하는 메서드 validateAndGetUserId를 통해 토큰이 올바른지 검증합니다. 이 과정에서 예외가 발생한다면, 인증이 실패할 수 있습니다.

  • CORS 설정: WebSecurityConfig에서 CORS 정책을 설정하셨습니다. config.addAllowedOriginPattern("http://localhost:3000"); 이 부분에서 허용된 요청 소스가 맞는지 확인합니다.

  • 프론트엔드의 토큰 전송: 클라이언트 측에서 Authorization 헤더에 Bearer 토큰을 담아 서버에 요청하는지 확인합니다. 서버로 요청을 보낼 때 토큰이 localStorage에 올바르게 저장되어 있고, 요청 헤더에 추가되었는지 확인해야 합니다.

서버 쪽 설정이 문제가 없다고 판단되면, 클라이언트 측에서 토큰을 정상적으로 가지고 있음에도 서버로 전송하지 못하는 문제일 수 있으니, 네트워크 탭을 통해 헤더에 Authorization 값이 실제로 포함되어 전송되는지 확인해보시길 권장합니다. 로그인 시에 생성되는 토큰이 있어야 정상적으로 통신할 수 있으니, 프론트엔드에서 토큰을 정상적으로 발급받고 있는지, 그리고 해당 토큰을 요청 헤더에 포함시켜 보내고 있는지를 점검해 주세요.

위 방법으로 문제를 해결할 수 없다면, 추가적인 오류 메시지나 로그 정보를 통해 더 정밀한 분석이 필요할 수 있습니다.