🗂️ 개인프로젝트/프로그
[개인프로젝트/프로그] 4. JWT로 로그인/로그아웃 구현하기
케로⸝⸝◜࿀◝ ⸝⸝
2024. 6. 8. 23:00
1. 개념
[스프링] 토큰 기반 인증과 JWT
프로젝트 생성 토큰 기반 인증이란?사용자가 서버에 접근할 때 이 사용자가 인증된 사용자인지 확인하는 방법은 다양하다.대표적인 사용자 인증 확인 방법은 2가지세션 기반 인증토큰 기반 인
progfrog.tistory.com
2. JWT 서비스 구현하기
실제로 JWT를 생성하고, 검증하는 서비스를 구현해 보자. 의존성과 토큰 제공자를 추가하고 나서, 리프레시 토큰 도메인과 토큰 필터를 구현하면 JWT 서비스를 사용할 준비가 된 것이다! 여기서 만든 클래스들은 OAuth 서비스에서 사용한다.
의존성 추가하기
implementation 'io.jsonwebtoken:jjwt:0.9.1' // 자바 JWT 라이브러리
implementation 'javax.xml.bind:jaxb-api:2.3.1' // XML 문서와 Java 객체 간 매핑 자동화
application-secret.properties
- resources에 application-secret.properties를 생성해서, 이슈 발급자(issuer)와 비밀 키(secret_key)를 설정해 준다.
- 이 두 값은 JWT 토큰을 만들기 위한 필수 설정 값이다.
- 깃허브에 업로드되어 노출되지 않도록 .gitignore 에도 등록해 둔다. 자세한 설명은 아래 링크 참고!
[Git] application-secret.properties 관리하기
토큰을 만들 때 사용할 비밀 키나, OAuth 클라이언트 Id, 보안 비밀번호 등 깃허브 같은 외부 사이트에 업로드하지 않아야하는 프로퍼티들을 관리해야할 때 .gitignore 파일을 사용하는 것이 효과적
progfrog.tistory.com
jwt.issuer=a@email.com
jwt.secret_key=123456789!@#
JwtProperties
해당 값들을 변수로 접근하는 데 사용할 클래스를 만든다.
package me.progfrog.flog.config.jwt;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties("jwt")
@Getter
@Setter
public class JwtProperties {
private String issuer;
private String secretKey;
}
@ConfigurationProperties("jwt")
- 자바 클래스에 프로퍼티 값을 가져와서 사용하는 애노테이션
TokenProvider
- 토큰 생성
- 올바른 토큰인지 유효성 검사
- 토큰에 필요한 정보 가져오기
package me.progfrog.flog.config.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.domain.User;
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 java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class TokenProvider {
private final JwtProperties jwtProperties;
/**
* JWT 토큰 생성 메서드
*/
public String generateToken(User user, Duration expiredAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getEmail())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
/**
* JWT 토큰 유효성 검증 메서드
*/
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);
}
/**
* 토큰 기반으로 유저 ID를 가져오는 메서드
*/
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
/**
* 클레임 조회
*/
private Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
}
JWT 토큰 생성 메서드
- 만료 시간, 유저 정보를 받아 토큰을 생성하는 메서드
- 헤더는 typ(타입)
- 내용은 iss(발급자), iat(발급일시), exp(만료일시), sub(토큰 제목)
- 클레임에는 유저 ID를 지정
- 토큰을 만들 때는 프로퍼티즈 파일에서 선언해 둔 비밀키와 함께 HS256 방식으로 암호화
public String generateToken(User user, Duration expiredAt) {
Date now = new Date();
return makeToken(new Date(now.getTime() + expiredAt.toMillis()), user);
}
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(now)
.setExpiration(expiry)
.setSubject(user.getEmail())
.claim("id", user.getId())
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
JWT 토큰 유효성 검증 메서드
- 프로퍼티즈 파일에 선언한 비밀키와 함께 토큰 복호화를 진행
- 만약 복호화 과정에서 에러가 발생하면, 유효하지 않은 토큰이므로 false를 반환하고 에러가 발생하지 않으면 true를 반환
public boolean validToken(String token) {
try {
// 비밀키로 복호화
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token);
return true;
} catch (Exception e) {
// 복호화 과정에서 에러가 나면 유효하지 않은 토큰
return false;
}
}
토큰 기반으로 인증 정보를 가져오는 메서드
- 토큰을 받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드
- 프로퍼티즈 파일에 저장한 비밀키로 토큰을 복호화한 뒤, 클레임 정보를 반환받아 사용자 이메일이 들어있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성
- 이때 UsernamePasswordAuthenticationToken의 첫 인자로 들어가는 User는 스프링 시큐리티에서 제공하는 객체인 User 클래스를 임포트 해야 한다.
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);
}
private Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody();
}
토큰 기반으로 유저 ID를 가져오는 메서드
- 토큰 기반으로 사용자 ID를 가져오는 메서드
- 프로퍼티즈 파일에 저장한 비밀키로 토큰을 복호화한 뒤, 클레임 정보를 반환받아 클레임에서 id 키로 저장된 값을 가져와 반환
public Long getUserId(String token) {
Claims claims = getClaims(token);
return claims.get("id", Long.class);
}
테스트 코드
테스트에서 롬복을 사용할 수 있도록 의존성 추가
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
테스트에서 application-secret.properties를 사용할 수 있도록 test용 application.properties 파일 수정
# JPA 설정
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.highlight_sql=true
# profile
spring.profiles.include=secret
JwtFactory
JWT 토큰 서비스를 테스트하는 데 사용할 모킹(mocking)*용 객체
*모킹: 테스트를 실행할 때 객체를 대신할 가짜 객체
package me.progfrog.flog.config.jwt;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Builder;
import lombok.Getter;
import java.time.Duration;
import java.util.Date;
import java.util.Map;
import static java.util.Collections.emptyMap;
@Getter
public class JwtFactory {
private String subject = "test@email.com";
private Date issuedAt = new Date();
private Date expiration = new Date(new Date().getTime() + Duration.ofDays(14).toMillis());
private Map<String, Object> claims = emptyMap();
/**
* 빌더 패턴을 사용해 설정이 필요한 데이터만 선택 정렬
* 빌더 패턴을 사용하지 않으면 필드 기본값을 사용
*/
@Builder
public JwtFactory(String subject, Date issuedAt, Date expiration, Map<String, Object> claims) {
this.subject = subject != null ? subject : this.subject;
this.issuedAt = issuedAt != null ? issuedAt : this.issuedAt;
this.expiration = expiration != null ? expiration : this.expiration;
this.claims = claims != null ? claims : this.claims;
}
public static JwtFactory withDefaultValues() {
return JwtFactory.builder().build();
}
/**
* jjwt 라이브러리를 사용해 JWT 토큰 생성
*/
public String createToken(JwtProperties jwtProperties) {
return Jwts.builder()
.setSubject(subject)
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
.setIssuer(jwtProperties.getIssuer())
.setIssuedAt(issuedAt)
.setExpiration(expiration)
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
.compact();
}
}
TokenProviderTest
package me.progfrog.flog.config.jwt;
import io.jsonwebtoken.Jwts;
import me.progfrog.flog.domain.User;
import me.progfrog.flog.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import java.time.Duration;
import java.util.Date;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
class TokenProviderTest {
@Autowired
private TokenProvider tokenProvider;
@Autowired
private UserRepository userRepository;
@Autowired
private JwtProperties jwtProperties;
@DisplayName("generateToken(): 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다.")
@Test
void generateToken() {
// given
User testUser = userRepository.save(new User("user@email.com", "test"));
// when
String token = tokenProvider.generateToken(testUser, Duration.ofDays(14));
// then
// jjwt 라이브러리를 사용해 토큰을 복호화하고, 토큰을 만들 때 클레임으로 넣어둔 id 값이 given 절에서 만든 유저 ID와 동일한지 확인
Long userId = Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey())
.parseClaimsJws(token)
.getBody()
.get("id", Long.class);
assertThat(userId).isEqualTo(testUser.getId());
}
@DisplayName("validToken(): 만료된 토큰이면 유효성 검증에 실패한다.")
@Test
void validToken_invalidToken() {
// given
// 현재 시간보다 7일 이전에 만료된 JWT 토큰을 생성
String token = JwtFactory.builder()
.expiration(new Date(new Date().getTime() - Duration.ofDays(7).toMillis()))
.build()
.createToken(jwtProperties);
// when
boolean result = tokenProvider.validToken(token);
// then
assertThat(result).isFalse();
}
@DisplayName("validToken(): 유효한 토큰이면 유효성 검증에 성공한다.")
@Test
void validToken_validToken() {
// given
String token = JwtFactory.withDefaultValues().createToken(jwtProperties);
// when
boolean result = tokenProvider.validToken(token);
// then
assertThat(result).isTrue();
}
@DisplayName("getAuthentication(): 검증 테스트")
@Test
void getAuthentication() {
// given
String userEmail = "user@email.com";
String token = JwtFactory.builder()
.subject(userEmail)
.build()
.createToken(jwtProperties);
// when
Authentication authentication = tokenProvider.getAuthentication(token);
// then
// 반환받은 인증 객체의 유저 이름을 가져와 given 절에서 설정한 subject 값인 user@email.com과 같은지 확인
assertThat(((UserDetails) authentication.getPrincipal()).getUsername()).isEqualTo(userEmail);
}
@DisplayName("getUserId(): 토큰으로 유저 ID를 가져올 수 있다.")
@Test
void getUserId() {
// given
Long userId = 1L;
String token = JwtFactory.builder()
.claims(Map.of("id", userId))
.build()
.createToken(jwtProperties);
// when
Long userIdByToken = tokenProvider.getUserId(token);
// then
assertThat(userIdByToken).isEqualTo(userId);
}
}
리프레시 토큰 도메인 구현하기
RefreshToken, RefreshTokenRepository
리프레시 토큰은 데이터베이스에 저장하는 정보이므로, 엔티티와 리포지터리를 추가해야 한다.
package me.progfrog.flog.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
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 newRefreshToken) {
this.refreshToken = newRefreshToken;
return this;
}
}
package me.progfrog.flog.repository;
import me.progfrog.flog.domain.RefreshToken;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByUserId(Long userId);
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
토큰 필터 구현하기
- 필터는 실제로 각종 요청을 처리하기 위한 로직으로 전달되기 전후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공
HTTP 요청 -> WAS -> 필터 -> 서블릿 -> 인터셉터 -> 컨트롤러
- 요청이 오면 헤더값을 비교해서 토큰이 있는지 확인하고, 유효 토큰이라면 시큐리티 컨텍스트 홀더(security context holder)에 인증 정보를 저장
- 시큐리티 컨텍스트는 인증 객체가 저장되는 보관소로, 여기서 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내 사용할 수 있다.
- 이 클래스는 스레드마다 공간을 할당하는 스레드 로컬(thread local)에 저장되므로, 코드의 아무 곳에서나 참조할 수 있고, 다른 스레드와 공유하지 않으므로 독립적으로 사용할 수 있다.
- 이러한 시큐리티 컨텍스트 객체를 저장하는 객체가 시큐리티 컨텍스트 홀더이다.
TokenAuthenticationFilter
- 이 필터는 액세스 토큰값이 담긴 Authorization 헤더값을 가져온 뒤, 액세스 토큰이 유효하다면 인증 정보를 생성
- 요청 헤더에서 키가 Authorization인 필드의 값을 가져온 다음, 토큰의 접두사 Bearer를 제외한 값을 얻는다.
- 만약 값이 null이거나 Bearer로 시작하지 않으면 null을 반환
- 가져온 토큰이 유효한지 확인하고, 유효하다면 인증 정보를 관리하는 시큐리티 컨텍스트에 인증 정보를 설정한다.
- 인증 정보가 설정된 이후에 컨텍스트 홀더에서 getAuthentication() 메서드를 사용해 인증 정보를 가져오면 유저 객체가 반환되며, 유저 객체에는 유저 이름(username)과 권한 목록(authorities)과 같은 인증 정보가 포함된다.
package me.progfrog.flog.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.config.jwt.TokenProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 요청 헤더의 Authorization 키의 값 조회
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
// 가져온 값에서 접두사 제거
String token = getAccessToken(authorizationHeader);
// 가져온 토큰이 유효한지 확인하고, 유효한 때는 인증 정보 설정
if (tokenProvider.validToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getAccessToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)) {
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
}
3. 토큰 API 구현하기
- 여기서는 리프레시 토큰을 전달받아 검증하고, 유효한 리프레시 토큰이라면 새로운 액세스 토큰을 생성하는 토큰 API를 구현
- 토큰 서비스, 컨트롤러를 차례대로 구현
토큰 서비스 추가하기
리프레시 토큰을 전달받아 토큰 제공자를 사용해 새로운 액세스 토큰을 만드는 토큰 서비스 클래스를 생성하자!
UserService
- 유저 ID로 유저를 검색해서 전달하는 findById() 메서드를 추가로 구현
package me.progfrog.flog.service;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.domain.User;
import me.progfrog.flog.dto.user.UserReqAddDto;
import me.progfrog.flog.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public void save(UserReqAddDto reqAddDto) {
userRepository.save(new User(
reqAddDto.email(),
bCryptPasswordEncoder.encode(reqAddDto.password()))
);
}
public User findById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("Unexpected user"));
}
}
RefreshTokenService
- 전달받은 리프레시 토큰으로 리프레시 토큰 객체를 검색해서 전달
package me.progfrog.flog.service;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.domain.RefreshToken;
import me.progfrog.flog.repository.RefreshTokenRepository;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
public RefreshToken findByRefreshToken(String refreshToken) {
return refreshTokenRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new IllegalArgumentException("Unexpected token"));
}
}
TokenService
- 전달받은 리프레시 토큰으로 토큰 유효성 검사를 진행하고, 유효한 토큰이면 리프레시 토큰으로 사용자 ID를 찾는다.
- 사용자 ID로 사용자를 찾은 후에, 토큰 제공자의 generateToken() 메서드를 호출해서 새로운 액세스 토큰을 생성한다.
package me.progfrog.flog.service;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.config.jwt.TokenProvider;
import me.progfrog.flog.domain.User;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
@RequiredArgsConstructor
public class TokenService {
private final TokenProvider tokenProvider;
private final RefreshTokenService refreshTokenService;
private final UserService userService;
private static final int DURATION_HOUR = 2;
public String createNewAccessToken(String refreshToken) {
// 토큰 유효성 검사에 실패하면 예외 발생
if (!tokenProvider.validToken(refreshToken)) {
throw new IllegalArgumentException("Unexpected token");
}
Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
User user = userService.findById(userId);
return tokenProvider.generateToken(user, Duration.ofHours(DURATION_HOUR));
}
}
컨트롤러 추가하기
AccessTokenReqCreateDto, AccessTokenResCreateDto
토큰 생성 요청 및 응답을 담당할 dto 생성
package me.progfrog.flog.dto.accesstoken;
public record AccessTokenReqCreateDto(
String refreshToken
) {
}
package me.progfrog.flog.dto.accesstoken;
public record AccessTokenResCreateDto(
String accessToken
) {
}
TokenApiController
- 실제로 요청을 받고 처리할 컨트롤러를 생성
- /api/token POST 요청이 오면 토큰 서비스에서 리프레시 토큰을 기반으로 새로운 액세스 토큰을 만들어주면 된다.
package me.progfrog.flog.api;
import lombok.RequiredArgsConstructor;
import me.progfrog.flog.dto.accesstoken.AccessTokenReqCreateDto;
import me.progfrog.flog.dto.accesstoken.AccessTokenResCreateDto;
import me.progfrog.flog.service.TokenService;
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;
@RestController
@RequiredArgsConstructor
public class TokenApiController {
private final TokenService tokenService;
@PostMapping("/api/token")
public ResponseEntity<AccessTokenResCreateDto> createNewAccessToken(@RequestBody AccessTokenReqCreateDto reqCreateDto) {
String newAccessToken = tokenService.createNewAccessToken(reqCreateDto.refreshToken());
return ResponseEntity.status(HttpStatus.CREATED)
.body(new AccessTokenResCreateDto(newAccessToken));
}
}
테스트
- given: 테스트 유저를 생성하고, jjwt 라이브러리를 이용해 리프레시 토큰을 만들어 데이터베이스에 저장한다. 토큰 생성 API의 요청 본문에 리프레시 토큰을 포함하여 요청 객체를 생성한다.
- when: 토큰 추가 API에 요청을 보낸다. 이때 요청 타입은 JSON이며, given 절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다.
- Then: 응답 코드가 201 Created 인지 확인하고, 응답으로 온 액세스 토큰이 비어 있지 않은지 확인한다.
package me.progfrog.flog.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import me.progfrog.flog.config.jwt.JwtFactory;
import me.progfrog.flog.config.jwt.JwtProperties;
import me.progfrog.flog.domain.RefreshToken;
import me.progfrog.flog.domain.User;
import me.progfrog.flog.dto.accesstoken.AccessTokenReqCreateDto;
import me.progfrog.flog.repository.RefreshTokenRepository;
import me.progfrog.flog.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.context.WebApplicationContext;
import java.util.Map;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class TokenApiControllerTest {
@Autowired
protected MockMvc mockMvc;
@Autowired
protected ObjectMapper objectMapper;
@Autowired
private WebApplicationContext context;
@Autowired
JwtProperties jwtProperties;
@Autowired
UserRepository userRepository;
@Autowired
RefreshTokenRepository refreshTokenRepository;
@BeforeEach
public void setUp() {
this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
@DisplayName("createNewAccessToken(): 새로운 액세스 토큰을 발급한다.")
@Test
public void createNewAccessToken() throws Exception {
// given
final String url = "/api/token";
User testUser = userRepository.save(
new User("user@email.com", "test"));
String refreshToken = JwtFactory.builder()
.claims(Map.of("id", testUser.getId()))
.build()
.createToken(jwtProperties);
refreshTokenRepository.save(new RefreshToken(testUser.getId(), refreshToken));
AccessTokenReqCreateDto reqCreateDto = new AccessTokenReqCreateDto(refreshToken);
final String requestBody = objectMapper.writeValueAsString(reqCreateDto);
// when
ResultActions resultActions = mockMvc.perform(post(url)
.contentType(MediaType.APPLICATION_JSON_VALUE)
.content(requestBody));
// then
resultActions
.andExpect(status().isCreated())
.andExpect(jsonPath("$.accessToken").isNotEmpty());
}
}
4. 정리
- 토큰 기반 인증은 인증에 토큰을 사용하는 방식
- 토큰은 클라이언트를 구분하는 데 사용하는 유일한 값
- 서버에서 생성해서 클라이언트에게 제공한 뒤, 클라이언트는 서버에 요청할 때마다 요청 내용과 함께 토큰을 전송
- 서버는 토큰으로 유효한 사용자인지 검증
- JWT는 토큰 기반 인증에서 주로 사용하는 토큰
- JSON 형식으로 사용자(클라이언트)의 정보를 저장
- JWT는 헤더, 페이로드, 서명의 구조로 이루어져 있음
- 헤더는 토큰의 타입과 해싱 알고리즘을 지정하는 정보를 포함
- 페이로드는 토큰에 담을 정보가 들어감
- 서명은 해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용
- 리프레시 토큰은 액세스 토큰과 별개의 토큰
- 액세스 토큰이 만료되었을 때, 새로운 액세스 토큰을 발급받는 용도로 사용함
- 필터는 실제로 요청이 전달되기 전과 후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공
- 시큐리티 컨텍스트는 인증 객체가 저장되는 보관소로, 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내어 사용하도록 제공되는 클래스
- 이러한 시큐리티 컨텍스트 객체를 저장하는 객체가 시큐리티 컨텍스트 홀더
반응형