김쥬르에 개발일지
Spring Boot JWT 본문
JWT?
JSON Web Token 의 줄임말로 JSON 객체로 정보를 주고 받을때
토큰 기반 인증으로 안전하게 전송하기 위한 방식입니다.
토큰 기반 인증?
사용자가 서버에 접근할 때 이 사용자가 인증된 사용자인지 확인하는 방법은 다양합니다.
대표적인 사용자 인증 확인 방법에는 서버 기반 인증과 토큰 기반 인증이 있습니다.
스프링 시큐리티에서는 기본적으로 세션 기반 인증을 사용해
사용자마다 사용자의 정보를 담은 세션을 생성하고 저장해서 인증을 합니다.
토큰 기반 인증은 토큰을 사용하는 방법입니다.
토큰은 서버에서 클라이언트를 구분하기 위한 유일한 값인데
서버가 토큰을 생성해서 클라이언트에게 제공하면
클라이언트는 이 토큰을 갖고 있다가 여러 요청을 이토큰과 함께 신청합니다.
그럼 서버는 토큰만 보고 유효한 사용자인지 검증합니다.
클라이언트와 서버가 토큰을 주고받으며 통신하는 과정입니다.

1. 클라이언트가 아이디와 비밀번호를 서버에게 전달하면서 인증을 요청하면
2. 서버는 아이디와 비밀번호를 확인해 유효한 사용인지 검증합니다.
유요한 사용자면 토큰을 생성해서 응답합니다.
3. 클라이언트는 서버에서 준 토큰을 저장합니다.
4. 이후 인증이 필요한 API를 사용할 때 토큰을 함께 보냅니다.
5.서버는 토큰이 유요한지 검증합니다.
6. 토큰이 유효하다면 클라이언트가 요청한 내용을 처리합니다.
토큰 기반 인증의 특징
무상태성
무상태성은 사용자의 인증 정보가 담겨 있는 토큰이 서버가 아닌 클라이언트에 있으므로
서버에 저장할 필요가 없습니다. 서버가 뭔가 데이터를 유지하고 있으려면 그만큼 자원을
소비해야 하죠 그런데 토큰 기반 인증에서는 클라이언트에서 인증 정보가 담긴 토큰을
생성하고 인증합니다. 따라서 클라이언트에서는 사용자의 인증 상태를 유지하면서 이후
요청을 처리해야 하는데 이것을 상태 관리한다고 합니다. 이렇게 하면
서버 입장에서는 클라이언트의 인증 정보를 저장하거나 유지하지 않아도 되기 때문에
완전한 무상태로 효율적인 검증을 할 수 있습니다.
확장성
무상태성은 확장성에 영향을 줍니다. 서버를 확장할 때 상태 관리를 신켱 쓸 필요가 없으니
서버 확장에도 용이한 것이죠. 예를 들어 물건을 파는 서비스가 있고
결제를 위한 서버와 주문을 위한 서버가 분리되어 있다고 가정했을때
세션 인증 기반은 각각 API에서 인증을 해야되는것과는 달리
토큰 기반 인증에서는 토큰을 가지는 주체는 서버가 아니라 클라이언트이기
때문에 가지고 있는 하나의 토큰으로 결제 서버와 주문 서버에게 요청을 보낼 수 있습니다.
추가로 페이스북 , 구글 로그인 같이 토큰 기반 인증을 사용하는 다른 시스템에 접근해
로그인 방식을 확장할 수도 있고 이를 활용해 다른 서비스에 권한을 공유할 수도 있습니다.
무결성
토큰 방식은 HMAC 기법이라고도 부르며 토큰을 발급한 이후에는
토큰 정보를 변경하는 행위를 할 수 없습니다. 즉 토큰의 무결성이 보장됩니다.
만약 누군가 토큰을 한 글자라도 변경하면 서버에서는 유효하지 않은 토큰이라고 판단합니다.
JWT 구조
발급받은 JWT를 이용해 인증을 하려면 HTTP 요청 헤더중에 Authorization 키값에
Bearer + JWT 토큰값을 넣어 보내야 합니다.
JWT는 .을 기준으로 헤더 , 내용 , 서명 으로 이루어져 있습니다.

헤더
토큰의 타입과 해싱 알고리즘을 지정하는 정보를 담습니다.
토큰 타입과 해싱 알고리즘 지정 예
{
"typ" : "JWT"
"alg" : "HS256"
}
typ : 토큰의 타입을 지정하며 JWT라는 문자열이 들어가게됩니다.
alg : 해싱 알고리즘을 지정합니다.
(서명을 암호화합니다.)
해싱?
가장 많이 사용하는 암호화 방식 중 하나로 , 복호화가 가능한 다른 암호화 방식들과 달리
해싱은 암호화만 가능하며 대표적으로 HS256, RS256이 있다.
SHA256
HS256 , RS256 알고리즘에서 공통적으로 쓰이는 단어인 S256 이라는 단어는 SHA256
알고리즘을 의미하며 SHA256은 데이터 무결성을 위해 사용되는 암호화 해쉬 알고리즘입니다.
SHA256 알고리즘은 SHA 알고리즘의 한 종류로서 256비트로 구성되며
64자리 문자열을 반환합니다. SHA-256은 미국의 국립표준기술연구소에 읜해
공표된 표준 해시 알고리즘인 SHA-2 계열 중 하나이며 블록체인에서 가장 많이 채택하여 사용되고있습니다.
이름에 내포되어 있듯 2^256만큼 경우의 수를 만들 수 있고 개인용 컴퓨터로 무차별 대입을 수행해 해시 충돌 사례를
찾으려고 할 때 많은 시간이 소요될 정도로 큰 숫자이므로 충돌로부터 비교적 안전하다고 평가됩니다.
HS256
JWT 토큰의 암호화 알고리즘으로 많이 쓰이는 암호화 알고리즘으로
HS256에서 S256은 SHA256을 의미한다고 했습니다. H는 무엇일까요?
HMAC 알고리즘을 의미합니다. HMAC 알고리즘은 대칭키 암호화 알고리즘입니다.
대칭키 암호화는 암호화 , 복호화 키가 같은 암호화 방식입니다.
RS256
RS256에서 R은 RSA를 의미하며 RSA 알고리즘은 비대칭키 암호화 알고리즘입니다.
비대칭키 암호화 알고리즘은 공개키 (public key) 와 개인키 (private key) 를 이용해 암호화를 진행합니다.
RS256 알고리즘은 HS256 과는 달리 , secret값이 따로 필요하지 않습니다.
내용
토큰과 관련된 정보를 담습니다.
내용의 한 조각을 클레임이라고 부르며 클레임은 키값의 한 쌍으로 이루어져 있습니다.
클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눠집니다.
등록된 클레임은 토큰에 대한 정보를 담는데 사용합니다.
등록된 클레임
| 이름 | 설명 |
| iss | 토큰 발급자(issuer) |
| sub | 토큰 제목(subject) |
| aud | 토큰 대상자(audience) |
| exp | 토큰의 만료 시간, 시간은 NumericDate 형식으로 하며 항상 현재 시간 이후로 설정합니다. |
| nbf | 토큰의 활성 날짜와 비슷한 개념으로 nbf는 Not Before를 의미합니다. NumericDate 형식으로 날짜를 지정하며 이 날짜가 지나기 전까지는 토큰이 처리되지 않습니다. |
| iat | 토큰이 발급된 시간으로 iat은 issued at을 의미합니다. |
| jti | JWT의 고유 식별자로서 주로 일회용 토큰에 사용합니다. |
공개 클레임은 공개되어도 상관없는 클레임을 의미합니다.
충돌을 방지할 수 있는 이름을 가져야하며, 보통 클레임 이름을 URI로 짓습니다.
비공개 클레임은 공개되면 안되는 클레임을 의미합니다. 클라이언트와 서버 간의 통신에 사용됩니다.
JWT 내용 예
{
"iss" : "kimjure@naver.com", //등록된 클레임
"iat" : 1622370878, //등록된 클레임
"exp" : 1622372678, //등록된 클레임
"https://kimjure.com/jwt_claims/is_admin" : true, //공개 클레임
"email" : "kimjure@naevr.com", //비공개 클레임
"hello" : "안녕하세요" //비공개 클레임
}
iss, iat, exp는 JWT 자체에서 등록된 클레임이고 , URI로 네이밍된
https://kimjure.com/jwt_claims/is_admin은 공개 클레임입니다.
그 외에 등록된 클레임은 비공개 클레임 값입니다.
서명
해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용하며,
헤더의 인코딩 값과 내용의 인코딩값을 합친 후에 주어진 비밀키를 사용해 해시값을 생성합니다.
토큰 유효기간
토큰을 주고받는 환경이 보안에 취약해서 토큰 자체가 노출되면 어떻게 될까요?
이를테면 영화를 보기 위해 산 영화 티켓의 정보가 노출되어서 다른 사람이 이 티켓으로영화를 보려고 한다고 했을때 어떻게하면 이 문제를 막을 수 있을까요?토큰은 이미 발급되면 그 자체로 인증 수단이 되므로 서버는 토큰과함께 들어온 요청이 토큰을 탈취한 사람의 요청인지 확인할 수 없습니다.
리프레시 토큰
토큰의 유효기간이 하루라면 어떨까요? 하루 동안은 그 토큰으로 무엇이든 할 수 있을 테니큰일입니다. 그러면 토큰의 유효기간이 짧으면 되겠지만 토큰의 유효기간이 짧으면사용자 입장에서는 받은 토큰을 너무 짧은 시간만 활용할 수 있으니 불편합니다.이러한 불편한 지점을 해결하기 위해 리프레시 토큰이 등장했습니다.리프레시 토큰은 엑세스 토큰과 별개의 토큰입니다.사용자를 인증하기 위한 용도가 아닌 엑세스 토큰이 만료되었을 때 새로운 엑세스 토큰을 발급하기 위해 사용합니다.엑세스 토큰의 유효 기간은 짧게 설정하고 리프레시 토큰의 유효 기간은 길게 설정하여공격자가 엑세스 토큰을 탈취해도 몇 분 뒤에는 사용할 수 없는 토큰이 되므로 안전해집니다.

1. 클라이언트가 서버에게 인증을 요청합니다.
2. 서버는 클라이언트에서 전달한 정보를 바탕으로 인증 정보가 유효한지 확인한뒤
엑세스 토큰과 리프레시 토큰을 만들어 클라이언트에게 전달합니다. 클라이언트는 전달받은 토큰을 저장합니다.
3. 서버에서 생성한 리프레시 토큰은 DB에도 저장해둡니다.
4. 인증을 필요로 하는 API를 호출할 때 클라이언트에서 저장된 엑세스 토큰과 함께 API를 요청합니다.
5.서버에서 전달받은 엑세스 토큰이 유효한지 검사한 뒤에 유효하다면 클라이언트에서 요청한 내용을 처리합니다.
6. 시간이 지나고 엑세스 토큰이 만료된 뒤에 클라이언트에서 원하는 정보를 얻기 위해 서버에게 API 요청을 보냅니다.
7. 서버에서 엑세스 토큰이 유효한지 검사를합니다.
만료된 토큰이면 유효하지 않기 때문에 토큰이 만료되었다는 에러를 전달합니다.
8.클라이언트에서는 이 응답을 받고 저장해둔 리프레시 토큰과 함께 새로운 엑세스 토큰을 발급하는 요청을 전송합니다.
9. 서버에서는 전달받은 리프레시 토큰이 유효한지 DB에서 리프레시 토큰을 조회한 후
저장해둔 리프레시 토큰과 같은지 확인합니다.
10. 만약 유효한 리프레시 토큰이라면 새로운 엑세스 토큰을 생성한 뒤 응답합니다.
그 이후에 클라이언트는 4번과 같이 다시 API를 요청합니다.
이제 실제로 JWT를 생성하고 검증하는 서비스를 구현해보겠습니다.
build.gradle에 필요한 의존성을 추가합니다.
implementation 'io.jsonwebtoken:jjwt:0.9.1' // 자바 JWT 라이브러리
implementation 'javax.xml.bind:jaxb-api:2.3.1' // XML 문서와 Java객체 간 매핑을 자동화
JWT 토큰을 만들려면 이슈 발급자 , 비밀키를 필수로 설정해야합니다.
application.properties에 코드를 추가합니다.
# 본인 email
jwt.issuer=kjjproject1@gmail.com
jwt.secret_key=study-springboot
해당 값들을 변수로 접근하는데 사용할 JwtProperties 클래스를 만듭니다.
config 패키지에 jwt 패키지를 만들고 JwtProperties.java 파일을 만들어 코드를 작성합니다.
@Setter
@Getter
@Component
@ConfigurationProperties("jwt")
public class JwtProperties {
private String issuer;
private String secretKey;
}
토큰을 생성하고 옳바른 토큰인지 유효성 검사를 하고,
토큰에서 필요한 정보를 가져오는 클래스를 작성합니다.
같은 위치에 TokenProvider.java 파일을 생성하고 코드를 작성합니다.
@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);
}
// 1 jwt토큰 생성 메서드
private String makeToken(Date expiry, User user) {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // 헤더 typ : JWT
.setIssuer(jwtProperties.getIssuer()) // 내용 iss : kjjproject1@gmail.com(properties 파일에서 설정한 값)
.setIssuedAt(now) // 내용 iat : 현재 시간
.setExpiration(expiry) // 내용 exp : expiry 멤버 변수값
.setSubject(user.getEmail()) // 내용 sub : 유저의 이메일
.claim("id", user.getId()) // 클레임 id : 유저 ID
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
// 서명 : 비밀값과 함께 해시값을 HS256 방식으로 암호화
.compact(); // JWT를 문자열로 압축
}
// 2 jwt 토큰 유효성 검증 메서드
public boolean validToken(String token) {
try {
Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey()) // 비밀값으로 복호화
.parseClaimsJws(token);
return true;
} catch (Exception e) { // 복호화 과정에서 에러가 나면 유효하지 않은 토큰
return false;
}
}
// 3 토큰 기반으로 인증 정보를 가져오는 메서드
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);
}
// 4 토큰 기반으로 유저 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();
}
}
1. 토큰을 생성하는 메서드입니다.
인자는 만료 시간, 유저 정보를 받습니다.
이 메서드에서는 set 계열의 메서드를 통해 여러값을 지정합니다.
헤더는 typ , 내용은 iss , iat , exp , sub , 클레임은 유저 ID를 지정합니다.
토큰을 만들때는 프로퍼티즈 파일에 선언해둔 비밀값과 함께 HS256 방식으로 암호화합니다.
2. 토큰이 유효한지 검증하는 메서드입니다.
프로퍼티즈 파일에 선언한 비밀값과 함께 토큰 복호화를 진행합니다.
만약 복호화 과정에서 에러가 발생하면 유효하지 않은 토큰이므로
false를 반환하고 아무 에러도 발생하지 않으면 true를 반환합니다.
3. 토큰을 받아 인증 정보를 담은 객체 Authentication를 반환하는 메서드입니다.
프로퍼티즈 파일에 저장한 비밀 값으로 토큰을 복호화한 뒤 클레임을 가져오는
private 메서드인 getClaims()를 호출해서 클레임 정보를 반환받아 사용자 이메일이 들어 있는
토큰 제목 sub와 토큰 기반으로 인증 정보를 생성합니다.
이때 UsernamePasswordAuthenticationToken의 첫 인자로 들어가는
User는 프로젝트에서 만든 User 클래스가 아닌 , 스프링 시큐리티에서 제공하는 객체인 User 클래스를 임포트합니다.
4. 토큰 기반으로 사용자 ID를 가져오는 메서드입니다.
프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화한 다음 클레임을 가져오는
private 메서드인 getClaims()를 호출해서 클에임 정보를 반환받고
클레임에서 id 키로 저장된 값을 가져와 반환합니다.
리프레시 토큰 도멘인을 구현하겠습니다.
리프레시 토큰은 데이터베이스에 저장하는 정보이므로 엔티티와 리포지터리를 추가해야 합니다.
| 컬럼명 | 자료형 | null 허용 | 키 | 설명 |
| id | INT | N | 기본키 | 일련번호_기본키 |
| user_id | INT | N | 유저 ID | |
| refresh_token | VARCHAR(255) | N | 토큰값 |
domain 디렉터리에 RefreshToken.java 파일을 추가한후 코드를 작성합니다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@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 newRefreshToken) {
this.refreshToken = newRefreshToken;
return this;
}
}
repository 디렉터리에 RefreshTokenRepository.java 파일을 만든후 코드를 작성합니다.
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByUserId(Long userId);
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
이제 토큰 필터를 구현할 차례입니다.
필터는 실제로 각종 요청이 요청을 처리하기 위한 로직으로
전달되기 전후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공합니다.
요청이 오면 헤더값을 비교해서 토큰이 있는지 확인하고 유효 토큰이라면
시큐리티 콘텍스트 홀더에 인증 정보를 저장합니다.

시큐리티 컨텍스트는 인증 객체가 저장되는 보관소입니다.
여기서 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내 사용합니다.
이 클래스는 스레드마다 공간을 할당하는 스레드 로컬에 저장되므로
코드의 아무곳에서나 참조할 수 있고 다른 스레드와 공유하지 않으므로
독립적으로 사용할 수 있습니다. 이러한 시큐리티 컨텍스트 객체를 저장하는
객체가 시큐리티 컨텍스트 홀더 입니다.
config 디렉터리에 TokenAuthenticationFilter.java 파일을 만듭니다.
이 필터는 엑세스 토큰값이 담긴 Authorization 헤더값을 가져온 뒤 엑세스 토큰이 유효하다면 인증 정보를 설정합니다.
@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;
}
}
요청 헤더에서 키가 Authorization인 필드의 값을 가져온 다음 토큰의 접두사 Bearer를 제외한 값을 얻습니다.
만약 값이 null이거나 Bearer로 시작하지 않으면 null을 반환합니다.
이어서 가져온 토큰이 유효한지 확인하고 , 유효하다면 인증 정보를 관리하는
시류키티 컨텍스트에 인증 정보를 설정합니다.
위에서 작성한 코드가 실행되며 인증 정보가 설정된 이후에 컨텍스트 홀더에서
getAuthentication() 메서드를 사용해 인증 정보를 가져오면 유저 객체가 반환됩니다.
유저 객체에는 유저 이름과 권한 목록과 같은 인증 정보가 포함됩니다.
리프레시 토큰을 전달받아 검증하고 , 유효한 리프레시 토큰이라면
새로운 엑세스 토큰을 생성하는 토큰 API를 구현합니다.
UserService.java 파일을 열어 전달받은 유저 ID로 유저를 검색해서 전달하는
findByid() 메서드를 추가로 구현합니다.
public User findById(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("Unexpected user"));
}
service 디렉터리에 RefreshTokenService.java 파일을 새로 만들어
전달받은 리프레시 토큰으로 리프레시 토큰 객체를 검색해서 전달하는
findByRefreshToken() 메서드를 구현합니다.
@RequiredArgsConstructor
@Service
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;
public RefreshToken findByRefreshToken(String refreshToken) {
return refreshTokenRepository.findByRefreshToken(refreshToken)
.orElseThrow(() -> new IllegalArgumentException("Unexpected token"));
}
}
service 디렉터리에 TokenService.java 파일을 생성한후 코드를 입력합니다.
@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("Unexpected token");
}
Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
User user = userService.findById(userId);
return tokenProvider.generateToken(user, Duration.ofHours(2));
}
}
createNewAccessToken() 메서드는 전달받은 리프레시 토큰으로
토큰 유효성 검사를 진행하고 , 유효한 토큰인 때 리프레시 토큰으로 사용자 ID를 찾습니다.
마지막으로는 사용자 ID로 사용자를 찾은 후에 토큰 제공자의
generateToken() 메서드를 호출해서 새로운 엑세스 토큰을 생성합니다.
dto 패키지에 토큰 생성 요청 및 응답을 담당할
CreateAccessTokenRequest를 생성하고 코드를 작성합니다.
@Getter
@Setter
public class CreateAccessTokenRequest {
private String refreshToken;
}
같은 위치에 CreateAccesTokenResponse.java 파일을 만들고 코드를 작성합니다.
@AllArgsConstructor
@Getter
public class CreateAccessTokenResponse {
private String accessToken;
}
controller 패키지에 TokenApiController.java 파일을 만들고 코드를 작성합니다.
@RequiredArgsConstructor
@RestController
public class TokenApiController {
private final TokenService tokenService;
@PostMapping("/api/token")
public ResponseEntity<CreateAccessTokenResponse> createNewAccessToken
(@RequestBody CreateAccessTokenRequest request) {
String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());
return ResponseEntity.status(HttpStatus.CREATED)
.body(new CreateAccessTokenResponse(newAccessToken));
}
}
/api/token POST 요청이 오면 토큰 서비스에서 리프레시 토큰을 기반으로
새로운 엑세스 토큰을 만들어줍니다.
코드가 많아 어떻게 동작하는지 감이 안올수 있는데
다음 포스팅엔 테스트 코드를 통해 실제로 어떻게 동작하는지 확인해보겠습니다.
'Spring boot' 카테고리의 다른 글
| Spring Boot OAuth2 (0) | 2024.01.08 |
|---|---|
| Spring Boot JWT TestCode (1) | 2024.01.06 |
| Spring Boot 스프링 시큐리티 (0) | 2024.01.01 |
| Spring Boot 각종 뷰 (1) | 2023.12.31 |
| Spring Boot 화면구성 (1) | 2023.12.29 |