인증(Authentication) vs 인가(Authorization) — 같은 듯 다른 두 개념

최병현·2026년 6월 14일

spring boot

목록 보기
21/34

1. 개념 소개

Spring Security를 공부하다 보면 Authentication, Authorization이라는 단어가 계속 나온다. 한글로 옮기면 둘 다 "인증"처럼 보이는데, 사실은 완전히 다른 개념이다.

인증(Authentication)은 "이 사용자가 누구인가"를 확인하는 과정이다. 로그인이 대표적인 인증 과정이다. 아이디와 비밀번호, 또는 JWT 토큰으로 "이 요청을 보낸 사람이 진짜 그 사람인지"를 검증한다.

인가(Authorization)는 "이 사용자가 이 작업을 할 권한이 있는가"를 확인하는 과정이다. 로그인한 사용자라도 모든 기능을 쓸 수 있는 건 아니다. 일반 사용자는 본인 글만 수정할 수 있고, 관리자만 회원 목록을 조회할 수 있는 것처럼 권한에 따라 접근 가능한 범위를 다르게 제어하는 과정이다.

한 문장으로 정리하면 이렇다.

인증 (Authentication) → "누구세요?"
인가 (Authorization)  → "그래서 이걸 할 수 있나요?"

2. 왜 필요한가

이 둘이 왜 분리되어 있어야 할까?

인증만 있고 인가가 없다면 어떻게 될까? 로그인한 사용자라면 누구나 모든 API에 접근할 수 있게 된다. 일반 사용자가 다른 사람의 주문 정보를 보거나, 관리자 페이지에 접근할 수 있다는 뜻이다.

인가만 있고 인증이 없다면 어떻게 될까? "이 사람이 ADMIN인지 확인하자"라고 했을 때, 그 "이 사람"이 누구인지조차 확인이 안 된 상태다. 권한 검사를 할 대상 자체가 불명확하다.

인증은 인가의 전제 조건이다. 먼저 누구인지 확인되어야, 그 사람에게 어떤 권한이 있는지 판단할 수 있다. 이 두 단계를 분리해서 처리하는 것이 보안 설계의 기본이다.

실무에서 이 둘이 적용되는 대표적인 상황이다.

  • 인증 — 로그인, JWT 토큰 검증, 세션 확인, OAuth2 소셜 로그인
  • 인가 — 관리자 전용 API 접근 제한, 본인 데이터만 수정 가능하도록 제한, 유료 회원 기능 제한

3. 전체 동작 흐름

PUT /api/posts/1 (게시글 수정) 요청으로 인증과 인가가 어떻게 순서대로 일어나는지 보자.

[Client]
    |
    | PUT /api/posts/1 + Authorization: Bearer {JWT}
    ↓
[JwtAuthFilter] ← 인증 단계
    |
    | ① JWT 토큰 추출
    | ② 토큰 유효성 검증 (서명, 만료시간)
    | ③ 토큰에서 사용자 정보(userId, role) 추출
    | ④ Authentication 객체 생성
    | ⑤ SecurityContextHolder에 저장
    |
    | → 토큰이 없거나 유효하지 않으면 401 Unauthorized
    ↓
[AuthorizationFilter] ← 인가 단계
    |
    | ⑥ SecurityContext에서 Authentication 꺼냄
    | ⑦ 요청 URL/Method에 필요한 권한 확인
    | ⑧ 사용자의 권한(Authorities)과 비교
    |
    | → 인증은 됐지만 권한이 부족하면 403 Forbidden
    ↓
[DispatcherServlet → Controller]
    |
    | ⑨ 메서드 레벨 인가 (@PreAuthorize 등)
    |   "이 게시글이 본인 글인가?" 같은 비즈니스 레벨 권한 체크
    |
    | → 비즈니스 로직상 권한 없으면 403 또는 예외
    ↓
[Service → DB 처리]
    ↓
[Client]
    HTTP Response

인증은 한 번 확인되면 끝이지만, 인가는 요청마다, 심지어 한 요청 안에서도 여러 레벨로 검사될 수 있다. URL 레벨, 메서드 레벨, 비즈니스 로직 레벨까지 단계적으로 적용된다.


4. 핵심 구성 요소

4-1. Authentication — 인증 정보 객체

인증이 완료되면 SecurityContext에 저장되는 객체다.

  • Principal — 인증된 사용자 자체 (보통 사용자 ID, 또는 UserDetails 객체)
  • Credentials — 비밀번호 등 자격증명. 인증 후엔 보통 제거됨
  • Authorities — 부여된 권한 목록 (GrantedAuthority 컬렉션)

4-2. GrantedAuthority — 권한

인가 판단의 기준이 되는 권한 정보다. 보통 ROLE_USER, ROLE_ADMIN 같은 역할(Role) 기반으로 부여한다.

new SimpleGrantedAuthority("ROLE_ADMIN")

4-3. AuthenticationManager / AuthenticationProvider

인증을 실제로 수행하는 컴포넌트다. 아이디/비밀번호가 일치하는지, JWT가 유효한지 등을 검증해서 인증 성공 시 Authentication 객체를 만들어 반환한다.

4-4. AuthorizationManager / AccessDecisionVoter

인가를 판단하는 컴포넌트다. 현재 사용자의 Authorities와 요청에 필요한 권한을 비교해서 접근 허용 여부를 결정한다. Spring Security 6.x부터는 AuthorizationManager가 이 역할을 담당한다.

4-5. URL 기반 인가 vs 메서드 기반 인가

  • URL 기반SecurityFilterChain에서 requestMatchers()로 경로별 권한 설정
  • 메서드 기반@PreAuthorize, @PostAuthorize, @Secured로 메서드 단위 권한 설정

5. Spring Boot에서 어떻게 연결되는가

Spring Security에서 인증과 인가는 각각 별도의 Filter가 처리한다.

FilterChain 순서 (일부):

JwtAuthFilter (커스텀)
    → 인증 담당. SecurityContext에 Authentication 저장
    ↓
AuthorizationFilter
    → 인가 담당. SecurityContext의 Authentication을 보고 권한 검사

URL 기반 인가는 SecurityFilterChain 설정에서 한다.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 메서드 레벨 보안 활성화 (@PreAuthorize 등)
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthFilter jwtAuthFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                // 인증 없이 접근 가능 (인증 자체가 필요 없음)
                .requestMatchers("/api/login", "/api/signup").permitAll()

                // 인증만 필요 (로그인한 사용자 누구나)
                .requestMatchers("/api/posts/**").authenticated()

                // 특정 권한 필요 (인가)
                .requestMatchers("/api/admin/**").hasRole("ADMIN")

                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

6. 간단한 예제 코드

6-1. 인증 — JWT Filter에서 Authentication 생성

@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

    private final JwtProvider jwtProvider;

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

        String token = resolveToken(request);

        if (token != null && jwtProvider.validate(token)) {
            Long userId = jwtProvider.getUserId(token);
            String role = jwtProvider.getRole(token); // 예: "ROLE_USER"

            List<GrantedAuthority> authorities =
                    List.of(new SimpleGrantedAuthority(role));

            // 인증 완료 — Authentication 객체 생성 및 저장
            Authentication authentication =
                    new UsernamePasswordAuthenticationToken(userId, null, authorities);

            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        // 토큰이 없거나 유효하지 않으면 인증되지 않은 상태로 그냥 통과
        // → 이후 인가 단계에서 401 처리됨

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        if (bearer != null && bearer.startsWith("Bearer ")) {
            return bearer.substring(7);
        }
        return null;
    }
}

6-2. URL 레벨 인가 — 역할 기반 접근 제한

.authorizeHttpRequests(auth -> auth
    .requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()   // 누구나 조회 가능
    .requestMatchers(HttpMethod.POST, "/api/posts").authenticated() // 로그인만 하면 작성 가능
    .requestMatchers("/api/admin/**").hasRole("ADMIN")               // ADMIN만 접근
    .requestMatchers("/api/manager/**").hasAnyRole("ADMIN", "MANAGER") // 둘 중 하나
    .anyRequest().authenticated()
)

6-3. 메서드 레벨 인가 — @PreAuthorize

@RestController
@RequestMapping("/api/admin")
public class AdminController {

    // ROLE_ADMIN이 아니면 메서드 실행 전 차단 (403)
    @PreAuthorize("hasRole('ADMIN')")
    @GetMapping("/users")
    public ResponseEntity<List<UserResponse>> getAllUsers() {
        return ResponseEntity.ok(userService.findAll());
    }
}

6-4. 비즈니스 레벨 인가 — 본인 데이터만 접근 허용

URL이나 Role만으로는 "이 게시글이 내 글인가?" 같은 검사를 할 수 없다. 이런 건 비즈니스 로직 안에서 직접 검사해야 한다.

@Service
@RequiredArgsConstructor
@Transactional
public class PostService {

    private final PostRepository postRepository;

    public void updatePost(Long postId, PostUpdateRequest request) {
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new EntityNotFoundException("게시글 없음"));

        Long currentUserId = SecurityUtils.getCurrentUserId(); // 인증 정보에서 추출

        // 인가: 본인 글인지 확인
        if (!post.getAuthorId().equals(currentUserId)) {
            throw new AccessDeniedException("본인 게시글만 수정할 수 있습니다.");
        }

        post.update(request.title(), request.content());
    }
}

@PreAuthorize에서도 직접 작성한 빈을 호출해서 검사할 수 있다.

@PreAuthorize("@postSecurity.isOwner(#postId, principal)")
@PutMapping("/{postId}")
public ResponseEntity<Void> updatePost(@PathVariable Long postId,
                                       @RequestBody PostUpdateRequest request) {
    postService.updatePost(postId, request);
    return ResponseEntity.ok().build();
}

7. 자주 헷갈리는 부분

401 vs 403 — 어떤 에러가 인증이고 어떤 게 인가인가

  • 401 Unauthorized인증 실패. "당신이 누구인지 확인할 수 없다." 토큰이 없거나, 만료되거나, 유효하지 않은 경우
  • 403 Forbidden인가 실패. "당신이 누구인지는 알지만, 이건 할 수 없다." 인증은 됐지만 권한이 부족한 경우

이름 자체가 헷갈리기 쉬운데, 401은 "신원 확인 실패", 403은 "권한 부족"으로 기억하면 된다.

인증된 사용자 = 모든 권한을 가진 사용자가 아니다

로그인에 성공했다고 모든 작업이 가능한 건 아니다. 인증은 "신원 확인"이고, 인가는 "그 신원으로 무엇을 할 수 있는가"다. 이 둘을 같은 거라고 생각하면 권한 체크를 빠뜨리는 보안 취약점이 생긴다.

URL 레벨 인가만으로는 부족하다

hasRole("USER")로 로그인한 사용자만 접근하게 했더라도, "이 리소스가 본인 것인가"는 별도로 검사해야 한다. URL/Role 기반 인가는 "이 종류의 작업을 할 수 있는가"를 검사하고, 비즈니스 레벨 인가는 "이 특정 리소스에 대해 할 수 있는가"를 검사한다. 두 레벨 모두 필요하다.


8. 실무에서 중요한 포인트

최소 권한 원칙 (Principle of Least Privilege)

사용자에게 필요한 최소한의 권한만 부여하는 것이 기본 원칙이다. 기본값을 차단(denyAll)으로 두고, 필요한 경로만 명시적으로 허용하는 방식이 실수로 보안이 뚫리는 걸 방지한다.

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/login", "/api/signup", "/api/health").permitAll()
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated() // 명시 안 된 나머지는 최소한 인증 필요
)

역할(Role)과 권한(Permission)을 구분해서 설계하기

작은 서비스에서는 ROLE_USER, ROLE_ADMIN 정도로 충분하지만, 서비스가 커지면 역할(Role)과 세부 권한(Permission)을 분리하는 게 유연하다.

Role: MANAGER
  - Permission: POST_READ
  - Permission: POST_UPDATE
  - Permission: USER_READ

역할이 여러 권한의 묶음이 되도록 설계하면, 새로운 역할이 추가되어도 기존 권한 조합을 재사용할 수 있다.

인증 실패와 인가 실패의 응답을 구분해서 처리하기

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .exceptionHandling(exception -> exception
                // 인증 실패 → 401
                .authenticationEntryPoint((request, response, authException) -> {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write("{\"message\": \"인증이 필요합니다.\"}");
                })
                // 인가 실패 → 403
                .accessDeniedHandler((request, response, accessDeniedException) -> {
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.setContentType("application/json;charset=UTF-8");
                    response.getWriter().write("{\"message\": \"접근 권한이 없습니다.\"}");
                })
            );

        return http.build();
    }
}

클라이언트가 두 상황을 구분할 수 있어야 적절한 처리를 할 수 있다. 401이면 로그인 페이지로 리다이렉트하고, 403이면 권한 부족 안내를 보여주는 식이다.


9. 정리

  • 인증(Authentication)은 "누구인가"를 확인하는 과정이고, 인가(Authorization)는 "무엇을 할 수 있는가"를 확인하는 과정이다
  • 인증이 인가의 전제 조건이다. 누구인지 모르면 권한을 판단할 수 없다
  • Spring Security에서는 인증 → SecurityContext 저장 → 인가 검사 순서로 처리된다
  • 401은 인증 실패, 403은 인가 실패 — 응답 코드로도 구분된다
  • 인가는 URL 레벨, 메서드 레벨, 비즈니스 로직 레벨로 다층적으로 적용될 수 있다
  • "본인 데이터인지" 같은 검사는 Role 기반 인가로는 부족하고, 비즈니스 로직에서 직접 검사해야 한다
  • 최소 권한 원칙에 따라 기본값을 차단으로 두고, 필요한 경로만 명시적으로 허용하는 것이 안전하다

10. 느낀 점

처음엔 그냥 "로그인하면 다 되는 거 아닌가?" 정도로 생각했는데, 인증과 인가를 분리해서 보니까 보안 설계의 기본 틀이 보였다.

"누구인지 확인하는 것"과 "그 사람이 무엇을 할 수 있는지 확인하는 것"은 완전히 다른 질문이다. 이 둘을 분리해서 생각하면 401과 403의 차이도 명확해지고, 왜 URL 레벨 인가만으로는 부족하고 비즈니스 로직에서 추가 검사가 필요한지도 이해된다. 보안은 한 곳에서 한 번에 끝나는 게 아니라 여러 레이어에서 단계적으로 검증되는 거라는 걸 느꼈다.

profile
Develop

0개의 댓글