| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- ArrayList
- Java
- javascript
- CSS
- math
- html
- 배열
- 자바
- Array
- java테스트
- 자바스크립트
- 정규식
- Visual Studio Code
- Eclipse
- 인텔리제이
- junit
- js
- json
- junit5
- input
- vscode
- 테스트자동화
- 문자열
- HashMap
- string
- 이클립스
- 자바문법
- 단위테스트
- IntelliJ
- list
- Today
- Total
어제 오늘 내일
[Spring Security] 7편 - JWT 로그인을 위한 준비 & TokenProvider 만들기 본문
지난 6편(이론편)에서 JWT는 "서버가 발급해 주는 위조 불가능한 출입증"이라고 배웠습니다.
이제부터 실전입니다!
기존 세션 방식을 버리고 JWT 방식을 적용하기 위해 가장 먼저 해야 할 일은
'출입증을 찍어내는 기계(Provider)'를 만드는 것입니다.
이 클래스 하나만 잘 만들어두면, 로그인할 때 토큰을 만들고, 요청이 올 때 토큰을 검사하는 모든 곳에서 핵심 부품으로 사용됩니다.
Step 1. 의존성 추가 (build.gradle)
JWT 기능을 구현하려면 외부 라이브러리가 필요합니다. Java 진영에서 가장 널리 쓰이는 jjwt 라이브러리를 추가합니다.
- 파일:
build.gradle
dependencies {
// ... 기존 의존성 ...
// JWT 라이브러리 (0.11.5 버전 사용)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
(추가 후 우측 상단 코끼리 아이콘(Load Gradle Changes) 클릭 필수!)
Step 2. 설정 파일에 비밀키 등록
토큰을 만들 때 가장 중요한 건 '서버만 아는 도장(Secret Key)'입니다.
이 키가 털리면 누구나 관리자 토큰을 위조할 수 있으므로 아주 중요합니다.
사용 중인 파일 형식(properties vs yml)에 맞춰 하나만 등록하세요.
🅰️ application.properties 사용하는 경우
# 기존 설정들...
# JWT Secret Key (임의의 긴 문자열을 입력하세요)
# 주의: HS256 알고리즘을 사용하려면 256비트(32바이트) 이상이어야 안전합니다.
jwt.secret=VlwEyVlPKZ6yV9yH3Yf0wz5w9z4x8y7a6b5c4d3e2f1g0h9i8j7k6l5m4n3o2p1
🅱️ application.yml 사용하는 경우
# 기존 설정들...
jwt:
secret: VlwEyVlPKZ6yV9yH3Yf0wz5w9z4x8y7a6b5c4d3e2f1g0h9i8j7k6l5m4n3o2p1
Step 3. 토큰 정보를 담을 DTO 만들기 (JwtToken)
TokenProvider를 만들기 전에, 생성된 토큰 정보를 담아서 반환할 포장지(DTO)를 먼저 만듭니다.
- 위치:
src/main/java/com/example/board/jwt/JwtToken.java
(패키지가 없으면jwt패키지를 새로 만들어주세요)
package com.example.board.jwt;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
@AllArgsConstructor
public class JwtToken {
private String grantType; // JWT 권한 인증 타입 (Bearer 등)
private String accessToken; // 우리가 만든 진짜 토큰
private String refreshToken; // (추후 구현 예정)
}
Step 4. ★핵심★ JwtTokenProvider 만들기
이제 '토큰 발급기'를 만듭니다. 이 클래스는 다음 3가지 핵심 기능을 수행합니다.
- 토큰 생성: 사용자의 정보를 받아서 JWT를 만들어준다.
- 토큰 복호화: 토큰을 받아서 "이거 누구 거야?"(사용자 정보)를 꺼낸다.
- 토큰 검증: "이 토큰 유효기간 안 지났어? 위조 안 됐어?"를 검사한다.
- 위치:
src/main/java/com/example/board/jwt/JwtTokenProvider.java
package com.example.board.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.stream.Collectors;
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
// application.properties에서 secret 값 가져와서 key에 저장
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// 1. 토큰 생성 메서드
public JwtToken createToken(Authentication authentication) {
// 권한 가져오기
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
long now = (new Date()).getTime();
// Access Token 생성
// 1일: 24 * 60 * 60 * 1000 = 86400000 밀리초
Date accessTokenExpiresIn = new Date(now + 86400000);
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim("auth", authorities)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
return JwtToken.builder()
.grantType("Bearer")
.accessToken(accessToken)
.build();
}
// 2. 토큰에서 사용자 정보 추출
public Authentication getAuthentication(String accessToken) {
// 토큰 복호화
Claims claims = parseClaims(accessToken);
if (claims.get("auth") == null) {
throw new RuntimeException("권한 정보가 없는 토큰입니다.");
}
// 클레임에서 권한 정보 가져오기
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("auth").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// UserDetails 객체를 만들어서 Authentication 리턴
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}
// 3. 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT Token", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT Token", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT Token", e);
} catch (IllegalArgumentException e) {
log.info("JWT claims string is empty.", e);
}
return false;
}
// 토큰 파싱 (내부용)
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}
Step 5. 💡 코드 상세 분석 (Line by Line)
이 클래스는 JWT 인증의 심장과도 같은 곳입니다. 코드가 조금 길지만, 각 부분이 어떤 역할을 하는지 뜯어보면 명확해집니다.
0. 클래스 및 필드 선언
@Slf4j
@Component
public class JwtTokenProvider {
private final Key key;
@Slf4j: 로그를 출력하기 위한 기능(Lombok)을 활성화합니다.@Component: 이 클래스를 스프링 빈(Bean)으로 등록합니다. 이제 다른 곳에서@Autowired나 생성자 주입을 통해 가져다 쓸 수 있습니다.private final Key key: 토큰을 암호화하고 복호화할 때 사용할 비밀키를 담을 변수입니다.
1. 생성자 (초기화)
public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}
@Value("${jwt.secret}"): 설정 파일(application.properties등)에 적어둔 암호문을 가져옵니다.Decoders.BASE64.decode: 가져온 비밀키가 Base64로 인코딩되어 있으므로, 이를 원래의 바이트 배열로 복원(디코딩)합니다.Keys.hmacShaKeyFor: 복원된 바이트 배열을 기반으로 HMAC-SHA 알고리즘에 사용할 수 있는 진짜 키 객체를 만듭니다.
2. createToken (토큰 생성)
로그인에 성공한 사용자 정보(authentication)를 받아서 JWT를 생성합니다.
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
- 사용자가 가진 권한 목록(List)을 꺼내서, 콤마(
,)로 구분된 하나의 문자열로 합칩니다. - 예:
[ROLE_USER, ROLE_ADMIN](List) ➡️"ROLE_USER,ROLE_ADMIN"(String)
long now = (new Date()).getTime();
Date accessTokenExpiresIn = new Date(now + 86400000);
now: 현재 시간을 밀리초 단위로 가져옵니다.accessTokenExpiresIn: 현재 시간(now)에 86,400,000 밀리초(24시간)를 더해 토큰의 유효기간을 설정합니다. 이 시간이 지나면 토큰은 무효가 됩니다.
String accessToken = Jwts.builder()
.setSubject(authentication.getName())
.claim("auth", authorities)
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
.setSubject: 토큰의 주인(Subject)으로 사용자 ID(이메일 등)를 설정합니다..claim("auth", ...): "auth"라는 이름으로 위에서 만든 권한 문자열을 토큰에 몰래 넣어둡니다. (나중에 이것만 보고 권한을 알 수 있습니다.).setExpiration: 방금 계산한 만료 시간을 토큰에 기록합니다..signWith(...): [핵심] 우리 서버만 아는 비밀키(key)로 서명을 합니다. 이 서명 덕분에 토큰이 위조되지 않았음을 증명할 수 있습니다.
3. getAuthentication (토큰에서 인증 정보 추출)
복호화된 토큰을 받아 스프링 시큐리티가 사용할 수 있는 인증 객체를 만듭니다.
Claims claims = parseClaims(accessToken);
- 암호화된 토큰을 복호화하여 그 안에 담긴 정보(Claims)를 꺼냅니다.
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
User: DB를 조회하지 않고, 토큰에 있던 정보만으로 사용자 객체를 만듭니다. (비밀번호는 빈 문자열로 둡니다.)UsernamePasswordAuthenticationToken: 최종적으로 시큐리티가 사용할 수 있는 인증 객체(Authentication)를 반환합니다.
4. validateToken (토큰 유효성 검사)
토큰이 이상한지 아닌지 검사하는 문지기입니다.
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
parseClaimsJws(token): [핵심] 토큰을 파싱해봅니다. 만약 위조되었거나 만료되었다면 여기서 예외(Exception)가 발생합니다.- 예외 없이 이 라인을 통과했다면 "정상적인 토큰"이므로
true를 반환합니다. - 예외가 발생하면
catch블록에서 로그를 남기고false를 반환합니다.
5. parseClaims (내부 유틸 메서드)
private Claims parseClaims(String accessToken) {
try {
return Jwts.parserBuilder()...parseClaimsJws(accessToken).getBody();
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
try-catch: 만료된 토큰이어도(ExpiredJwtException), 그 안에 들어있던 정보(Claims)는 필요할 때가 있습니다. (예: 토큰 재발급 시 사용자 ID 확인 등) 그래서 예외가 발생해도 정보를 꺼내서 반환하도록 처리했습니다.
마무리
자, 이제 '출입증(JwtToken)' 과 '출입증 발급기(Provider)'까지 모두 완성했습니다.
하지만 아직 이 기계를 사용하는 사람이 아무도 없습니다.
다음 8편에서는 이 발급기를 Spring Security의 필터(Filter)에 장착해서,
"요청이 들어올 때마다 토큰을 검사하고, 없으면 막아버리는" 보안 문지기를 만들어보겠습니다.
고생하셨습니다! 다음 편에서 만나요. 🚀
'IT > SpringBoot' 카테고리의 다른 글
| [Spring Security] 9편 - JWT 로그인 API 구현 & Postman 테스트 (0) | 2026.02.20 |
|---|---|
| [Spring Security] 8편 - SecurityConfig 대수술 (FormLogin 삭제 & JWT 필터 적용) (0) | 2026.02.20 |
| [Spring Security] 6편 - 세션(Session) vs 토큰(JWT), 도대체 뭐가 다를까? (0) | 2026.02.19 |
| [Spring Security] 5편 - 회원가입 & 비밀번호 암호화 (BCrypt) (0) | 2026.02.18 |
| [Spring Security] 4편 - 로그인 페이지 커스텀 & 로그아웃 처리 (0) | 2026.02.18 |
