JWT 토큰 및 Spring Security 인증/인가 로직 복습
이 문서는 제공된 소스 코드를 기반으로 Spring Boot 애플리케이션에서 JWT(JSON Web Token)와 Spring Security를 사용한 인증 및 인가 메커니즘을 설명하고, 관련 핵심 개념을 정리합니다.
0. 시작하기 전에: 핵심 개념 정의
인증 (Authentication)
- 정의: 사용자가 누구인지 확인하는 과정입니다. (예: 아이디/비밀번호 로그인, 이메일 인증 코드 확인)
- 목표: 시스템에 접근하려는 주체의 신원을 증명하는 것입니다.
인가 (Authorization)
- 정의: 인증된 사용자가 특정 리소스나 기능에 접근할 권한이 있는지 확인하는 과정입니다.
- 목표: 신원이 증명된 사용자가 허용된 범위 내에서만 동작하도록 제어하는 것입니다.
1. 전체 인증/인가 흐름
애플리케이션은 상태 비저장(Stateless) 인증 방식을 사용합니다. 즉, 서버는 사용자 세션을 저장하지 않고 모든 요청에 포함된 JWT 토큰을 통해 사용자를 인증합니다.
사용자 로그인 (
/api/user/login):- 클라이언트는 아이디와 비밀번호로 로그인을 요청합니다.
- 서버는
UserService에서BCrypt를 사용해 비밀번호를 검증합니다. - 인증에 성공하면
JwtService를 통해 JWT(Access Token)를 생성합니다. 이 토큰에는 사용자의 아이디(uid)와 역할(urole)이 포함됩니다. - 생성된 토큰은
HttpOnly속성이 적용된 쿠키에 담겨 클라이언트에게 전송됩니다.
API 요청:
- 클라이언트는 API 요청 시 브라우저에 저장된 JWT 쿠키를 함께 전송합니다.
- 서버의
JwtAuthFilter가 모든 요청을 가로채 쿠키에서 JWT 토큰을 추출합니다. JwtService를 통해 토큰의 유효성을 검증합니다.- 토큰이 유효하면, 토큰에서 사용자 정보(아이디, 역할)를 추출하여 Spring Security가 이해할 수 있는
UsernamePasswordAuthenticationToken을 생성합니다. - 생성된 인증 객체를
SecurityContextHolder에 저장하여 현재 요청에 대한 사용자 인증을 완료합니다.
인가 (접근 제어):
SecurityConfig에 정의된 규칙에 따라,SecurityContextHolder에 저장된 사용자의 역할(ROLE_USER,ROLE_ADMIN등)을 기반으로 요청된 API에 대한 접근 허용 여부를 결정합니다.- 권한이 충분하면 컨트롤러의 메서드가 실행되고, 그렇지 않으면 403 Forbidden 오류가 반환됩니다.
로그아웃 (
/api/user/logout):- 클라이언트에게 만료 시간이 0인 동일한 이름의 쿠키를 전송하여 브라우저에서 토큰 쿠키를 즉시 삭제하도록 합니다.
2. 핵심 컴포넌트 분석
2.1. 비밀번호 암호화 (UserService.java)
- 목적: 사용자의 비밀번호를 안전하게 저장하기 위해 단방향 암호화(해싱)를 사용합니다.
- 구현:
BCryptPasswordEncoder- 회원가입 시:
bcrypt.encode(평문_비밀번호)를 호출하여 비밀번호를 해시 값으로 변환한 후 DB에 저장합니다.// UserService.java - signup() userDto.setUpwd( bcrypt.encode(userDto.getUpwd() ) ); userMapper.signup( userDto ); - 로그인 시:
bcrypt.matches(평문_비밀번호, DB에_저장된_해시)를 사용해 입력된 비밀번호가 올바른지 검증합니다.matches메서드는 내부적으로 동일한 알고리즘으로 평문을 해싱하여 DB의 해시와 비교합니다.// UserService.java - login() boolean result2 = bcrypt.matches(userDto.getUpwd(), result.getUpwd()); if (result2 == true ) { // 로그인 성공 return result; }
- 회원가입 시:
2.2. JWT (JSON Web Token) 관리 (JwtService.java)
정의: JSON 형식의 데이터를 안전하게 전송하기 위한 개방형 표준(RFC 7519)입니다. 주로 인증과 권한 부여에 사용됩니다.
장점:
- HTTP 친화적: HTTP 헤더에 포함시켜 쉽게 전송할 수 있습니다.
- 보안: 서명(Signature)을 통해 토큰의 변조 여부를 검증할 수 있습니다. (예: HS256 알고리즘)
- 무상태(Stateless): 서버가 사용자 상태를 저장할 필요 없이 클라이언트가 토큰을 소유하므로 확장성이 좋습니다.
JWT의 구조:
헤더.페이로드.서명- Header (헤더): 토큰의 타입(JWT)과 서명에 사용된 알고리즘(예: HS256) 정보를 담습니다.
- Payload (페이로드): 토큰에 담을 실제 정보(클레임)를 포함합니다. 사용자의 아이디, 역할, 토큰 발급 시간(
iat), 만료 시간(exp) 등이 여기에 해당합니다. - Signature (서명): 헤더와 페이로드를 합친 후, 지정된 비밀키(
secret key)로 암호화하여 생성합니다. 이 서명을 통해 토큰이 중간에 변경되지 않았음을 보장합니다.
구현:
io.jsonwebtoken(jjwt) 라이브러리- 의존성 추가:
// build.gradle implementation 'io.jsonwebtoken:jjwt-api:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' - 비밀키 설정: 토큰의 서명을 생성하고 검증하는 데 사용되는 32바이트 이상의 비밀키(
secret)를 정의합니다. 이 키는 외부에 노출되어서는 안 됩니다.// JwtService.java private String secret = "80419273645108293746518204937465"; // 예시 키 private Key secretKey = Keys.hmacShaKeyFor( secret.getBytes( StandardCharsets.UTF_8) ); - 토큰 생성 (
createToken): 로그인 성공 시 호출되며, 사용자의 아이디(uid)와 역할(urole)을 '클레임(claim)'으로 포함하여 토큰을 생성합니다.// JwtService.java - createToken() public String createToken( String uid , String urole ){ String token = Jwts.builder() .claim("uid" , uid ) .claim("urole" , urole) .setIssuedAt( new Date() ) // 발급 시간 .setExpiration( new Date( System.currentTimeMillis() + 1000 * 60 * 60) ) // 1시간 후 만료 .signWith( secretKey , SignatureAlgorithm.HS256) .compact(); return token; } - 토큰 검증 및 클레임 추출:
checkToken,getClaims등의 메서드를 통해 토큰의 유효성을 검사하고uid나urole같은 저장된 데이터를 추출합니다.
- 의존성 추가:
2.3. 요청 필터링 및 인증 (JwtAuthFilter.java)
목적: 모든 API 요청을 가로채서 JWT를 통한 인증을 수행하고, Spring Security 컨텍스트에 인증 정보를 설정합니다.
구현:
OncePerRequestFilter를 상속받아 구현합니다.쿠키에서 토큰 추출:
request.getCookies()를 통해loginUser라는 이름의 쿠키를 찾고, 그 값을 토큰으로 사용합니다.토큰 검증 및 정보 추출:
jwtService.checkToken(token)으로 토큰을 검증하고,jwtService.getUid(),jwtService.getUrole()로 정보를 가져옵니다.Spring Security 인증 객체 생성: 추출된
uid와urole을 사용하여UsernamePasswordAuthenticationToken을 생성합니다. 이 객체는 Spring Security에게 현재 사용자가 누구이며 어떤 권한을 가졌는지 알려주는 역할을 합니다.// JwtAuthFilter.java - doFilterInternal() String uid = jwtService.getUid( token ); String urole = jwtService.getUrole( token ); UsernamePasswordAuthenticationToken t = new UsernamePasswordAuthenticationToken( uid , null , // 비밀번호는 null 처리 List.of( new SimpleGrantedAuthority("ROLE_"+urole) ) );SecurityContext에 저장: 생성된 인증 객체를
SecurityContextHolder에 저장합니다. 이로써 현재 요청 스레드 내에서 사용자가 인증된 것으로 간주됩니다.// JwtAuthFilter.java - doFilterInternal() SecurityContextHolder.getContext().setAuthentication( t );
2.4. Spring Security 설정 및 인가 (SecurityConfig.java)
정의: Spring 기반 애플리케이션의 인증과 인가를 제공하는 라이브러리입니다. 로그인, 로그아웃, CSRF 방지, 토큰 기반 인증 등 다양한 보안 기능을 지원합니다.
의존성 추가:
// build.gradle implementation 'org.springframework.boot:spring-boot-starter-security'*의존성을 추가하는 즉시 다수의 보안 필터가 기본적으로 활성화됩니다. 따라서 커스텀 설정이 필요합니다.구현:
@Configuration과SecurityFilterChain빈을 사용해 보안 정책을 설정합니다.- 세션 정책 설정: JWT 기반의 상태 비저장(stateless) 인증을 위해 세션을 사용하지 않도록 설정합니다.
// SecurityConfig.java http.sessionManagement(session -> session.sessionCreationPolicy( SessionCreationPolicy.STATELESS ) ); - CSRF 보호 비활성화: 상태 비저장 API에서는 일반적으로 CSRF 보호를 비활성화합니다. (개발 단계에서 권장)
// SecurityConfig.java http.csrf( csrf -> csrf.disable()); - 커스텀 필터 등록: 직접 구현한
JwtAuthFilter를 Spring Security의 필터 체인에 추가합니다.UsernamePasswordAuthenticationFilter보다 먼저 실행되도록 설정하여 JWT 인증이 먼저 처리되게 합니다.// SecurityConfig.java http.addFilterBefore( jwtAuthFilter , UsernamePasswordAuthenticationFilter.class ); - 경로별 접근 권한 설정 (인가):
authorizeHttpRequests를 통해 각 URL 패턴에 필요한 역할을 정의합니다.// SecurityConfig.java .authorizeHttpRequests(auth -> auth .requestMatchers("/api/user/info").hasAnyRole("USER" , "ADMIN") .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/**").permitAll() ); // 그 외 모든 경로는 인증 없이 허용- 참고:
SecurityConfig에서 전역으로 설정하는 대신, 각 컨트롤러 메서드에@PreAuthorize("hasRole('ADMIN')")어노테이션을 사용하여 개별적으로 권한을 설정할 수도 있습니다.
- 참고:
- 세션 정책 설정: JWT 기반의 상태 비저장(stateless) 인증을 위해 세션을 사용하지 않도록 설정합니다.
3. JWT 확장 및 고려사항
Redis를 이용한 토큰 관리
- 목적: 여러 서버 인스턴스 간에 로그인 상태(토큰 정보)를 공유해야 할 때 사용됩니다.
- 예시: 8080 포트의 회원 서비스, 8081 포트의 게시판 서비스가 독립적으로 운영될 때, 한 곳에서 로그인하면 다른 서비스에서도 로그인 상태가 유지되도록 할 수 있습니다.
- 동작: 로그인 시 발급된 JWT(또는 Refresh Token)를 Redis와 같은 중앙화된 NoSQL DB에 저장합니다. 각 서버는 요청이 들어올 때마다 Redis를 조회하여 토큰의 유효성을 검증함으로써 상태를 공유합니다. 이는 토큰을 강제로 만료시키는 등 정교한 제어를 가능하게 합니다.
'백엔드 > 스프링' 카테고리의 다른 글
| [Spring] JPA 연관관계 및 참조 복습 (0) | 2025.11.06 |
|---|---|
| [Spring] JPA 실습 복습 자료 (0) | 2025.11.05 |
| [Spring] 암호화(BCrypt) 및 쿠키(Cookie) 복습 자료 (0) | 2025.10.21 |
| [Spring] Redis 개념 및 활용법 with Spring Boot (0) | 2025.10.21 |
| [Spring] MyBatis XML 연동 개요 (0) | 2025.10.13 |
| [Spring] 도서 대여 콘셉트 트랜잭션 실습과 피드백 마크다운 (1) | 2025.09.26 |
| [Spring] 13주차+: 마이크로서비스 아키텍처(MSA) 입문 (0) | 2025.09.22 |
| [Spring] 12주차: 설정 분리 및 비동기 처리 (0) | 2025.09.22 |