인증(Authentication) vs 인가(Authorization)

최정윤·2026년 8월 20일

Spring

목록 보기
33/38

Spring Security로 JWT 로그인을 붙이면서 "인증"과 "인가"를 자꾸 뭉뚱그려 생각했다.

이번 과정의 핵심은 "로그인 기능을 만드는 것"이 아니라,
"너 누구야(인증)"와 "너 이거 해도 돼?(인가)"가 서로 다른 단계라는 걸 이해하는 것이었다.


1. 핵심 원리

두 개념은 이름도 비슷하고(둘 다 Auth...) 401/403이 헷갈리지만, 질문 자체가 다르다.

구분인증 (Authentication)인가 (Authorization)
던지는 질문"너 누구야?""너 이거 해도 돼?"
하는 일신원 확인 (JWT 토큰 검증)권한 확인 (접근 허용/차단)
실패 시401 Unauthorized403 Forbidden
우리 코드JwtAuthenticationFilterauthorizeHttpRequests의 규칙

요청이 컨트롤러까지 가는 흐름으로 보면 인증이 먼저, 인가가 나중이다.

요청 (Authorization: Bearer xxx)
      │
① JwtAuthenticationFilter   ← 인증: 토큰 까서 "너 누구야" 확인
      │  (유효하면 SecurityContext에 사용자 정보 심음)
      ▼
② authorizeHttpRequests     ← 인가: 이 경로를 "너 자격으로" 접근 가능?
      │  permitAll → 통과 / authenticated → 인증돼 있어야 통과
      ▼
③ Controller

필터가 "신분증을 확인"(인증)하고, 그 결과를 규칙표가 보고 "출입 허가"(인가)를 내주는 구조다.


2. 우리 코드에서 인증과 인가가 각각 어디인가

인증 — JwtAuthenticationFilter (너 누구야)

토큰을 꺼내 검증하고, 통과하면 "이 사람이 누구인지"를 SecurityContext에 심는다.

if (token != null && jwtUtil.isValid(token)) {          // 신분증 진짜냐 = 인증
    Long memberId = jwtUtil.getMemberId(token);
    CustomUserPrincipal principal = new CustomUserPrincipal(memberId, email, "USER");

    UsernamePasswordAuthenticationToken authentication =
            UsernamePasswordAuthenticationToken.authenticated(principal, null,
                    List.of(new SimpleGrantedAuthority("ROLE_USER")));

    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(authentication);
    SecurityContextHolder.setContext(context);          // "이 사람 누구다"를 등록
}
filterChain.doFilter(request, response);

인가 — SecurityConfig의 규칙표 (너 이거 해도 돼)

어느 경로를 누구에게 열지 결정한다. permitAllauthenticated가 인가의 핵심이다.

.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/auth/**").permitAll()                       // 인증 없이 OK
        .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()   // 상품 조회는 공개
        .anyRequest().authenticated()                                      // 그 외 전부 인증 필요
)
  • permitAll() = 인가 통과 — 인증 안 돼 있어도 접근 허용
  • authenticated() = 인증돼 있어야 인가 통과 — 신분증 확인된 사람만

3. 트러블슈팅 — "없는 카테고리"인데 401이 뜬 사건

문제원인해결
GET /api//products?category=... 가 401 UNAUTHORIZEDURL에 //(슬래시 2개) → permitAll/api/products/** 패턴에 매칭 실패 → anyRequest().authenticated()로 떨어짐 → 인증 안 된 요청이라 차단URL을 /api/products(슬래시 하나)로 고침

빈 검색 결과(200)를 기대했는데 401이 나서 "API가 깨졌나?" 했지만, 실제로는 인증/인가 흐름이 정확히 작동한 것이었다. 오타로 경로가 어긋나 "공개 경로 목록"에 안 걸렸고, 그러면 규칙표의 마지막 줄 authenticated()가 적용돼 토큰 없는 요청을 막은 것이다.

401은 "API 버그"가 아니라 "인가 규칙이 이 경로를 보호 대상으로 판단했다"는 신호일 수 있다.
인증/인가는 경로 매칭에 그대로 좌우된다.

그리고 이때 401 응답을 실제로 만들어주는 건 컨트롤러도 @RestControllerAdvice도 아니라 JwtAuthenticationEntryPoint였다.

.exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
// JwtAuthenticationEntryPoint
public void commence(..., AuthenticationException authException) {
    ErrorCode errorCode = ErrorCode.UNAUTHORIZED;          // 401
    response.setStatus(errorCode.getStatus().value());
    response.getWriter().write(jsonMapper.writeValueAsString(ErrorResponse.from(errorCode)));
}

인가 단계에서 "인증이 필요한데 인증이 안 됨"으로 막힌 요청은 컨트롤러에 도달하기 전에 EntryPoint가 401을 만들어 돌려보낸다.


4. 실무 활용

  • 화이트리스트 방식이 안전하다. 우리 설정은 "공개할 것(가입·로그인·상품GET)만 열고 anyRequest().authenticated()로 나머지는 전부 잠금"이다. 새 API를 추가하면 기본이 "인증 필요"라, 깜빡하고 보호를 안 걸어 뚫리는 사고를 막아준다.
  • 401 vs 403 구분 기준: 토큰이 없거나/틀렸다 = 인증 실패 = 401. 로그인은 됐는데 남의 리소스 접근 등 권한이 없다 = 인가 실패 = 403. (우리 결제 도메인의 "남의 결제 조회 차단"이 바로 403 케이스다.)
  • 경로 순서가 중요하다. authorizeHttpRequests는 위에서부터 먼저 매칭되는 규칙을 적용하므로, 구체적인 permitAll을 먼저 쓰고 anyRequest()를 맨 마지막에 둔다.

5. 마무리

인증은 "너 누구야"를 확인하는 단계(우리 코드에선 JwtAuthenticationFilter)이고,
인가는 "그 신분으로 이걸 해도 돼?"를 판단하는 단계(authorizeHttpRequestspermitAll/authenticated)다.
요청은 인증 → 인가 순으로 흐르고, 인가에서 막히면 JwtAuthenticationEntryPoint가 401을 만들어 돌려준다.
오늘 겪은 // 오타 401 사건이 이 흐름 전체를 한 번에 보여준 좋은 사례였다.

한 줄 요약

인증(누구야, 401)과 인가(해도 돼, 403)는 다른 단계다. 필터가 신분을 확인하고,
permitAll/authenticated 규칙표가 출입을 허가하며, 막히면 EntryPoint가 401을 낸다.

profile
콩떡

0개의 댓글