// JWT 핵심 API
// jjwt-api -> JWT 토큰 생성 및 파싱 API
implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
// JWT 내부 구현체 (필수)
// jjwt-impl -> JWT 내부 동작 구현체
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
// JSON 처리 (Jackson 기반)
// jjwt-jackson -> JWT에서 아용할 JSON 변환 처리 (Jackson 기반)
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
// Spring Security 추가 되어있어야 됨
JWT (Json Web Token)는 3부분으로 나뉩니다.
[Header].[Payload].[Signature]
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. // Header
eyJzdWIiOiJ1c2VyMSIsInJvbGUiOiJVU0VSIn0. // Payload
Rk8lwXwFErvRtL6bZ3eQwzYlQa0Ym4Dlybkj8oxAkgQ // Signature
JwtProperties)시크릿 키나 토큰 만료 시간을 관리하는 설정 파일
✅ JwtProperties.java
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String issuer; // 토큰 발급자
private String secretKey; // 시크릿 키
}
✅ application.properties 또는 application.yml
# JWT 설정
jwt.issuer=원하는 이름
jwt.secret-key=원하는 키
issuer: 토큰 발급자(이메일, 이름 등)secret-key: 토큰을 서명할 비밀 키(가장 중요함!!!!!!!!!!!!!)secret-key가 중요한 이유는 해당 키를 뺏기는 순간 모든 권한이 넘어간다
맥북 기준 터미널에 해당 명령어 입력하면 토큰의 암복호화에 사용될 랜덤 암호가 나온다.
HS256 알고리즘을 사용하기 위해 32글자 이상으로 설정
openssl rand -hex 32
윈도우라면 새로고침 될 때마다 랜덤으로 키를 던저주는 사이트가 있음
https://randomkeygen.com/
사이트에 들어가 codelgniter Encryption Keys 부분 중 아무거나 사용하면 됨

✅ TokenProvider.java
@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
/* 토큰 생성 */
public String generateToken(UsersDTO usersDTO, Duration expiration) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiration.toMillis()), usersDTO);
}
/* 실제 토큰 생성 로직 */
private String makeToken(Date expiry, UsersDTO usersDTO) {
SecretKey key = Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
return Jwts.builder()
.setIssuer(jwtProperties.getIssuer()) // 발급자
.setIssuedAt(new Date()) // 발급 시간
.setExpiration(expiry) // 만료 시간
.setSubject(usersDTO.getUserid()) // 사용자 ID
.signWith(key, SignatureAlgorithm.HS256) // 서명
.compact();
}
/* 토큰 유효성 검사 */
public boolean validToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
Jwts.parser()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
/* 인증 객체 반환 */
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(claims.getSubject(), null, authorities);
}
/*토큰에서 Claims 추출*/
private Claims getClaims(String token) {
SecretKey key = Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8));
return Jwts.parser()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
}
jjwt 라이브러리 Jwts.parse(), Jwts.parserBuilder()0.12.* 버전부터는 parserBuilder() 이걸 사용하라고 되어있는데 아무리해도 찾을 수 없어서
Jwts 들어가서 관련된 메서드 찾아보니까
public static JwtParserBuilder parser() {
return (JwtParserBuilder)Classes.newInstance("io.jsonwebtoken.impl.DefaultJwtParserBuilder");
}
해당 메서드 하나 밖에 없다...책도 그렇고 gpt도 그렇고 아무도 안알려줘서 찾음
기존 시큐리티 설정에서 Form Login 비활성화 시켜줘야된다.
JWT 인증이 적용되도록 기존 Security 설정을 수정해야됨
@EnableWebSecurity
public class SecurityJwtConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // JWT는 Stateless(무상태)이기 때문에 CSRF가 필요없음
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/auth/signup").permitAll()
.anyRequest().permitAll() // ✅ 모든 요청 허용
)
return http.build();
}
}
CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)는
사용자의 인증 정보를 이용해 공격자가 의도하지 않은 요청을 보내는 보안 공격이다.
✅ CSRF 공격 예시
✅ JWT 인증의 특징
🔥 CSRF가 필요 없는 이유
🔎 즉, JWT 기반 인증에서는 CSRF 토큰이 필요하지 않아 불필요한 검사를 비활성화 처리한다.
요청이 올 때마다 JWT 토큰을 검사하는 필터를 만듬
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
public JwtAuthenticationFilter(TokenProvider tokenProvider) {
this.tokenProvider = tokenProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// ✅ 요청 헤더에서 토큰 추출
String token = resolveToken(request);
// ✅ 토큰이 유효하면 인증 처리
if (token != null && tokenProvider.validToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
// ✅ Authorization 헤더에서 Bearer 토큰 추출
private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
🔎 설명
Authorization 헤더에서 Bearer 토큰을 꺼냅니다.
토큰 유효성 검사 후, 인증 정보를 SecurityContext에 저장합니다.
토스페이먼츠의 Basic 인증과 Bearer 인증 설명
아까 만든 JwtAuthenticationFilter를 SecurityConfig에 연결.
@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final TokenProvider tokenProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // ✅ CSRF 완전 비활성화
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login", "/auth/signup").permitAll()
.anyRequest().permitAll() // ✅ 모든 요청 허용
)
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider), UsernamePasswordAuthenticationFilter.class); // ✅ JWT 필터 추가
return http.build();
}
}
🔎 설명
사용자가 로그인하면 JWT 토큰을 발급해서 응답하는지 테스트
@Slf4j
@RestController
@RequiredArgsConstructor
public class SecurityController {
private final UsersService usersService;
private final TokenProvider tokenProvider;
@PostMapping("/auth/login")
public ResponseEntity<String> login(@RequestBody UsersDTO user) {
System.out.println("로그인 요청 받음: " + user.getUserid() + " / " + user.getPassword()); // ✅ 요청 확인
if ("user".equals(user.getUserid()) && "password".equals(user.getPassword())) {
String token = tokenProvider.generateToken(user, Duration.ofHours(1));
System.out.println("토큰 발급: " + token); // ✅ 토큰 발급 확인
return ResponseEntity.ok("Bearer " + token);
} else {
System.out.println("로그인 실패"); // ✅ 실패 확인
return ResponseEntity.status(401).body("Invalid Credentials");
}
}
}
🔎 설명
{
"username": "user",
"password": "password"
}
성공적이라면 랜덤으로 저런식으로 나온다
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
오늘은 일단 여기까지 지금까지 분량은 총 6시간짜리 공부