JWT를 사용한 토큰 기반 인증
토큰 기반 인증이 어떤 식으로 이루어지는지 간단히 정리함.
이 과정을 통해 클라이언트와 서버 간의 인증이 이루어짐.
JWT를 사용해 인증을 구현하는 과정임. JWT는 JSON Web Token의 약자로, 클라이언트와 서버 간에 정보를 안전하게 전달하기 위한 표준임.
우선 build.gradle
파일에 JWT 관련 라이브러리를 추가함.
dependencies {
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'javax.xml.bind:jaxb-api:2.3.1'
}
JWT 토큰을 생성하고, 서명하며, 검증할 때 사용됨.
JWT 설정을 application.yml
파일에 추가함. application-dev.yml
파일에 다음과 같은 내용을 추가함.
jwt:
issuer: your-email@example.com
secret-key: study-springboot-sideproject-first
이 설정값을 사용하기 위해 JwtProperties
클래스를 작성함.
package com.mylittleproject.firstlittleproject.config.jwt;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String issuer;
private String secretKey;
}
이 클래스는 Spring Boot의 @ConfigurationProperties
애노테이션을 사용해 application.yml
에 정의된 jwt
속성값을 읽어옴.
토큰을 생성하고 검증하는 역할을 하는 TokenProvider
를 작성함.
package com.mylittleproject.firstlittleproject.config.jwt;
import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import com.mylittleproject.firstlittleproject.user.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class TokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user, Duration expiredAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
private String makeToken(Date expiredAt, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiredAt)
.setSubject(user.getEmail())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
public Authentication getAuthentication(String token) {
Claims claims = getClaims(token);
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(new org.springframework.security.core.userdetails.User(claims.getSubject(), "", authorities), token, authorities);
}
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
public Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
generateToken
: 사용자 정보를 바탕으로 JWT 토큰을 생성함.validToken
: 전달받은 토큰이 유효한지 검증함.getAuthentication
: 토큰을 바탕으로 인증 정보를 가져옴.getUserId
: 토큰에서 사용자 ID를 추출함.JWT를 사용할 때 보통 Access Token과 Refresh Token을 함께 사용함. Access Token의 유효기간은 짧게, Refresh Token은 더 길게 설정함. 먼저 DTO를 record 타입으로 작성함.
package com.mylittleproject.firstlittleproject.user.dto;
public record CreateAccessTokenRequest(
String refreshToken
) {
}
public record CreateAccessTokenResponse(
String accessToken
) {
}
RefreshToken을 데이터베이스에 저장하기 위한 엔티티를 작성함.
package com.mylittleproject.firstlittleproject.user.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.NoArgsConstructor;
@NoArgsConstructor
@Getter
@Entity
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "refresh_token", nullable = false)
private String refreshToken;
public RefreshToken(Long userId, String refreshToken) {
this.userId = userId;
this.refreshToken = refreshToken;
}
public RefreshToken update(String refreshToken) {
this.refreshToken = refreshToken;
return this;
}
}
그리고 RefreshToken을 관리할 리포지토리도 작성함.
package com.mylittleproject.firstlittleproject.user.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.mylittleproject.firstlittleproject.user.entity.RefreshToken;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByUserId(Long userId);
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
서비스를 구현해서 JWT 인증을 처리하는 코드를 작성함.
package com.mylittleproject.firstlittleproject.user.service;
import java.time.Duration;
import org.springframework.stereotype.Service;
import com.mylittleproject.firstlittleproject.config.jwt.TokenProvider;
import com.mylittleproject.firstlittleproject.user.entity.User;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class TokenService {
private final TokenProvider tokenProvider;
private final RefreshTokenService refreshTokenService;
private final UserService userService;
public String createNewAccessToken(String refreshToken) {
// 토큰 유효성 검사 실패 -> 예외 발생
if (!tokenProvider.validToken(refreshToken)) {
throw new IllegalArgumentException("Invalid refresh token");
}
Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
User user = userService.findById(userId);
return tokenProvider.generateToken(user, Duration.ofHours(2));
}
}
이 서비스는 유효한 Refresh Token을 바탕으로 새로운 Access Token을 생성함.
그리고 컨트롤러도 작성해서 API 엔드포인트를 만듦.
package com.mylittleproject.firstlittleproject.user.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.mylittleproject.firstlittleproject.user.dto.CreateAccessTokenRequest;
import com.mylittleproject.firstlittleproject.user.dto.CreateAccessTokenResponse;
import com.mylittleproject.firstlittleproject.user.service.TokenService;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@RestController
public class TokenApiController {
private final TokenService tokenService;
@PostMapping("/api/token")
public ResponseEntity<CreateAccessTokenResponse> createNewAccessToken(@RequestBody CreateAccessTokenRequest request) {
String newAccessToken = tokenService.createNewAccessToken(request.refreshToken());
return ResponseEntity.status(HttpStatus.CREATED).body(new CreateAccessTokenResponse(newAccessToken));
}
}
이 컨트롤러는 /api/token
경로로 새로운 Access Token을 요청하는 API를 제공함.
Refresh Token을 관리하는 서비스를 작성함.
package com.mylittleproject.firstlittleproject.user.service;
import org.springframework.stereotype.Service;
import com.mylittleproject.firstlittleproject.user.entity.RefreshToken;
import com.mylittleproject.firstlittleproject.user.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
public RefreshToken findByRefreshToken(String refreshToken) {
return refreshTokenRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new IllegalArgumentException("Unexpected Token"));
}
}
이 서비스는 Refresh Token을 데이터베이스에서 조회하고 관리함.