Notice
Recent Posts
Recent Comments
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

Spring & Java

Spring Security 인가 구조 및 실습 본문

카테고리 없음

Spring Security 인가 구조 및 실습

dev.hyuck 2026. 1. 23. 11:10

 

인가란 무엇인지 또 나오는데 이건 계속 학습해야 되는 중요한 부분이라 여러번 나오는 것이니 꼭 암기하고 학습 해야 됩니다.

 

개념 정리

  • 인증 ( Authenication ) 사용자가 누구인지 확인하는 절차
  • 인가 ( Autheorization ) : 인증된 사용자가 무엇을 할 수 있는지 결정하는 절차
예시 : 로그인은 되었지만, 관리자만 접근 가능한 페이지에 들어가려 하면 권한 검사를 통해 차단됨

 

우리가 구성한 인가 흐름과 Spring Security 구조 비교

Spring Security의 인가 흐름 이론

[SecurityContext에 저장된 Authentication]
    ↓
[FilterSecurityInterceptor]
    ↓
[AccessDecisionManager]
    ↓
[권한 정보와 비교 → 접근 허용 or 거부]

 

코드 간소화 

		@Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)  // .csrf().disable() 방식은 더 이상 사용 안함.
            .httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화
            .formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
            .addFilterBefore(jwtFilter, SecurityContextHolderAwareRequestFilter.class)
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/user/login").permitAll()
                .anyRequest().authenticated()
            )
            .build();
    }
}

 

● 우리가 구성한 Spring Security 설정은 이론 흐름과 완벽히 일치하게 됩니다.

JwtFilter에서 인증을 끝내고 SecurityContext에 저장해두었기 때문에, 이후의 모든 인가 과정은 Spring Security가 자동으로 처리하게 됩니다.

 

인가 방법 2가지

1) URL 기반 설정 ( 권장 : 관리 편의성 )

http.authorizeHttpRequests()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().permitAll();

 

2) 메서드 기반 어노테이션 ( 서비스 레이어 권한 제어 { 컨트롤러 레이어도 가능함 } )

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }

● 사용 전 @EnableMethodSecurity(prePostEnabled = true) 필요

 

 

자 여기까진 이론이였고 실습을 통해서 학습을 해보도록 하겠습니다.

 

실습전 몇가지 구성 요소가 추가 될 예정입니다. 생각보다 특정한 기능을 접속할때 권한이 있는지 없는지  체크하는 기능은 생각 보다 많이 없습니다.  자 이제 시작해 볼게요.

 

예를들어서 API 메서드가 100라고 한다면 .. 이걸 다 추가할 필요가 있을까? X 절대로 그러지 말자.

.anyRequest().authenticated()

이 설정을 달아주면 이거(.requestMatchers ( URL ). permitAll() 이외에는 다 허락해줘라 라는 설정 값이 되는겁니다. 자 조금 재미있는 과정이 될 겁니다.

자 이걸 다 지우고 포스트맨에서 어떻게 작동 하는지 한번 볼까요 ?

 

분명 저번에는 403 Forbidden 에러가 발생 했는데 이젠 메서드들이 전부다 개방 됐음을 확인 할 수 있습니다.  다른것도 한번만 더 해볼까요? 

어떤가요? 

.anyRequest().authenticated()

이렇게 이 코드 하나로 URL이 개방 됐습니다. 이는 제한 된 URL 말고 모든 URL을 개방할때 사용하는 아주 간편한 코드라는 것을 알수 있게 되었습니다.

 

반대로 또 지운다면 어떨까요 ? 한번 해보면 아실겁니다 !! 403 Forbidden 에러가 발생 할겁니다.

 

자 다음으로 몇 가지 좀 보고 넘어 갑시다.

 

@GetMapping("/get")
public String getUserInfo(HttpServletRequest request) {
    String username = (String) request.getAttribute("username");
    log.info(username);
    return username;
}

기존에 있던 getUserInfo  메서드 입니다. 우리는 더 이상 이 방식을 사용하지 않게 됐습니다. 왜 ?

 

이미지 처럼 우리는 앞 시간에 코드를 바꿧습니다. 더 이상 사용하지 않기 때문에 주석 처리를 하고 다른 방식으로 변경 했습니다.

// equest.setAttribute("username", username);

 

 

제가 넣었던 정보를 어떻게 뽑아 올지를 합리적 의심을 하는 순간이 오게 되었습니다. Spring Security 에서는 이런 기능을 제공하는 특정 어노테이션이 있습니다.

 

일단 getUserInfo로 찾아가서 이제 값을 바꿔 봅시다.

 

이렇게 값을 바꾸고 다시 한번 해볼까요 ? 

아주 잘 작동 하는것을 확인할 수 있었습니다.

 

중간 점검을 한번 하자면 URL 메서드를 제한하지 않으면 전체적으로 개방 해주는  .anyRequest().authenticated() 사용 할 수 있고, 

// equest.setAttribute("username", username);

사용하지 않게 되면서  

User user = new User(username, "", List.of());
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));

filterChain.doFilter(request,response);

값을 바꾸고 HttpServletRequest를 사용하지 않게 되면서 전용 어노테이션을 사용해서 간편하게 쓸수 있게 됐습니다. 

@GetMapping("/get")
public String getUserInfo(@AuthenticationPrincipal User user) {

    log.info(user.getUsername());
    return user.getUsername();
}

코드를 이렇게 바꿔 쓸수 있게 되었고, 포스트맨에서도 정상적으로 작동 하는 것을 확인할 수 있었습니다.

 

지금 근데 구조가 너무 단순해요, 그래서 프로젝트를 고도화 해보겠습니다.

 

User user 객체를 로그인 인증/인가 해보고, 특정 URL, 특정 컨트롤러 접근해 권한별로 체크해보는 것 까지 해보겠습니다.

 

enums 패키지를 생성하고 UserRolenum을 만들어 각 객체를 만들어 사용해보는 것을 해보겠습니다.

어노테이션 형식

기본적으로 이렇게 맞춰봣습니다. 이제 User라는 Entity를 생성해보겠습니다.

 

 

기본 엔티티 구성 요소나 어노테이션 기억 하시죠? 일단 다 때려 넣으시고 값들을 추가 해봅시다.

이렇게 Entity를 만들었으면 이젠 등록을 몇가지 해줘 보겠습니다.

 

일단 Repository를 하나 생성 했는데 여기서 중요한거 ! User은 내가 만든 User를 불러 오는것이지 스프링에서 쓰는 User가 절대로 아닙니다!! 오류 나니까 꼭 체크 하세요 다음은 UserService를 생성 합니다.

 

이제 어느정도 구조를 갖춰 볼겁니다. 사용자는 회원가입 하고 매번 킬때마다 너무 힘들겠죠?

그래서 서버를 킬때마다 몇명의 유저의 정보를 넣어주는 역할을 만들까 합니다.

 

 

폴더 제일 상위에 InitData라는 클래스를 생성하고 추가해 보겠습니다. 

이렇게 값을 넣고 DB를 확인해 봅시다. DB가 성공적으로 만들어질까요 ?

 

성공적으로 DB를 생성 했음을 알 수 있습니다.

ADMIN, NORMAR  권한을 가지고 인증, 인가 검사를 해보겠습니다.

 

이제 로그인 기능도 하나 만들어 보겠습니다.

<< 기능 >>

 

 

<< 결과 >> 

코드 값을 바꿔주면서 쭉 쫓아 왔는데 지금 보니까 .. 원래 기존에 username과 password를 넣으니까 어떤 일이 생겼냐! 

에러가 발생 했습니다 왜 이럴까요 ? 

 

우리는 코드 값에서 이미 지정해준 코드가 있습니다 그 코드만 로그인이 가능하도록 되어 있습니다.

List에 userRoleEnum :: getRole를 넣어 사용할 수 있게 되었습니다.

 

 

AdminController

 

+ api/admin/** 모든것들 ( 메서드들은 ) Role이 있어야 합니다 무슨 Role?? ADMIN 있어야 합니다.

 

이제 결과 값을 테스트 해볼까요 ? 권한이 있는 사람만 관리자 권한 페이지에 접속 성공 했다고 성공적으로 응답 하는 것을 확인할 수 있었습니다.

 

이번에는 다른 사람도 한번 로그인 해봐야겠죠 ? 

 

이건 박민수 토큰으로 접근 해봣습니다.

이제는 박민수 조회도 해봐야겠죠 ?

 

권한을 열어 주고 normal을 가진 모든 유저들에게 권한을 부여 합니다.

 

짜잔 Role = Normal 권한자 조회도 이상 없이 됐음을 확인할 수 있습니다 !!

 

이번에는 부분 조회나 권한을 부여 하는 역할을 가지고 있어야 합니다. 지금 권한 URL 확인해보면 ADMIN을 부분적으로 나눌수 없고 NORMAL을 부분적으로 나눌수 없습니다. 예를들어 세부적인 각 권한을 나누고 싶다면 어떻게 해야 될까요 ? 

그럴때 쓰는게 바로 아래 방법 입니다.

 

메스드 기반 어노테이션 (서비스 레이어 권한 제어 {컨트롤러 레이어도 가능함})

@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { ... }

사용법이 아주 간단해요. 타겟을 지정하고 메서드에 어노테이션을 붙히면 됩니다. 

 

저는 여기 메서드에 어노테이션을 사용해서 테스트 해볼게요 NORMAL 권한을 가진 유저가 과연 사용이 될까요 ?

사용이 되지 않습니다. 부분 권한을 제어 하고 타겟 하는 어노테이션 꼭 기억하면 되겠습니다.

 

결과적으로 '인가'를 전역적으로 사용 할 것인지 아니면 메서드 별로 관리할 것인지는 보통은 SecurityConfig에서 다 제어 하지만 특이한 케이스에 사용하면 아주 좋습니다

 

@EnableMethodSecurity(securedEnabled = true)

사용 하고싶다면 꼭 이 어노테이션을 활성화 해줘야 됩니다 오늘은 Spring Security '인가' 였습니다.

 

📘 오늘의 TIL

📌 날짜 : 2026년 01월 23일
📌 주제 : SecurityContextHolder로 JWT 인증 흐름 만들기

 

오늘 학습한 내용

오늘은 JWT 기반 인증 구조에서 SecurityContextHolder가 어떤 역할을 하는지를 중심으로 학습했습니다.

단순히 토큰을 검증하는 것을 넘어서, Spring Security가 인증 정보를 어떻게 유지하고 전달하는지 흐름 전체를 이해하는 것이 핵심 목표였습니다. 

 

🔐 JWT 인증 방식의 핵심 개념

JWT 인증은 Stateless(무상태) 인증 방식으로,

  • 서버가 로그인 상태(Session)를 저장하지 않고
  • 클라이언트가 매 요청마다 JWT 토큰을 전달하여
  • 토큰의 유효성으로 인증을 수행한다.

따라서 서버는 요청마다 다음 과정을 반복한다.

요청 → 토큰 검증 → 사용자 인증 → 권한 확인

 

인증 흐름 전체 구조

Client Request
    ↓
Authorization Header (Bearer Token)
    ↓
JWT Filter (OncePerRequestFilter)
    ↓
토큰 유효성 검증
    ↓
username 추출
    ↓
UserDetailsService 사용자 조회
    ↓
Authentication 객체 생성
    ↓
SecurityContextHolder 저장
    ↓
Spring Security 인증 완료

🔥 가장 중요했던 포인트

❗ JWT 인증의 핵심은 토큰 검증이 아니다

JWT 인증에서 진짜 중요한 부분은 다음 한 줄이었다.

 

🎯 @AuthenticationPrincipal의 정체

컨트롤러에서 다음과 같이 사용자 정보를 받을 수 있다.

@GetMapping("/get")
public String getUserInfo(
        @AuthenticationPrincipal UserDetails userDetails
) {
    return userDetails.getUsername();
}

이 값은 어디서 오는것일까 ?

SecurityContextHolder
    ↓
Authentication
        ↓
principal(UserDetails)

정리하면

  • JWT는 인증 수단일 뿐이다.
  • 실제 인증 상태 관리는 SecurityContextHolder가 담당한다.
  • Authentication 객체가 Security의 핵심 데이터 구조다.
  • JWT 필터에서 인증 정보를 SecurityContext에 저장해야
    이후 인가(Authorization)가 정상적으로 동작한다.

긴글 읽어주셔서 감사합니다.