특정 사용자가 서버에 접근을 했을 때, 이 사용자가 인증된 사용자인지 구분하기 위해서는 여러 방법을 사용할 수 있는데요. 대표적인 방법으로는 Session과 Cookie, 그리고 Token 이 있습니다.
토큰을 사용한다는 것은 요청과 응답에 토큰을 함께 보내 이 사용자가 유효한 사용자인지를 검색하는 방법입니다.
JWT는 JSON 객체를 사용해서 토큰 자체에 정보를 저장하는 Web Token 입니다.
일반적으로는 Authorization: <type> <credentials>
형태로 Request Header 에 담겨져 오기 때문에 Header 값을 확인해서 가져올 수 있습니다.
JWT는 .을 기준으로 헤더(header) - 내용(payload) - 서명(signature)으로 이루어져있습니다.
Header
typ
: 토큰의 타입을 지정합니다. JWT라는 문자열이 들어가게 됩니다 alg
: Signature 를 해싱하기 위한 알고리즘을 지정합니다.Payload
토큰에 담을 정보가 들어갑니다. 정보의 한 덩어리를 클레임(claim)이라고 부르며, key-value의 한 쌍으로 이루어져있습니다. 클레임의 종류는 세 종류로 나눌 수 있습니다.
iss
: 토큰 발급자(issuer)sub
: 토큰 제목(subject)aud
: 토큰 대상자(audience)iat
: 토큰이 발급된 시간 (issued at)exp
: 토큰의 만료시간(expiraton). 시간은 NumericDate 형식이다. (예: 1480849147370)nbf
: 토큰의 활성 날짜(Not Before). NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.jti
: JWT의 고유 식별자로서, 주로 일회용 토큰에 사용한다.Signature
Authorization: <type> <credentials>
형태에서 <type>
부분에 들어갈 값입니다.
엄격한 규칙이 있는건 아니고 일반적으로 많이 사용되는 형태라고 생각하면 됩니다.
Basic | 사용자 아이디와 암호를 Base64 로 인코딩한 값을 토큰으로 사용 |
Bearer | JWT 또는 OAuth 에 대한 토큰을 사용 |
HOBA | 전자 서명 기반 인증 |
Digest | 서버에서 난수 데이터 문자열을 클라이언트에 보냄
클라이언트는 사용자 정보와 nonce 를 포함하는 해시값을 사용하여 응답 |
Mutual | 암호를 이용한 클라이언트-서버 상호 인증 |
AWS4-HMAC-SHA256 | AWS 전자 서명 기반 인증 |
https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
private final JwtProperties jwtProperties;
public String generateToken(User user) {
Date now = new Date();
return Jwts.builder()
.setHeader(createHeader()) // (1)
.setClaims(createClaims(user)) // (2)
.setIssuedAt(now) // (3)
.setExpiration(new Date(now.getTime()+ Duration.ofHours(3).toMillis())) // (4)
.signWith(SignatureAlgorithm.HS256, jwtProperties.getSecret()) // (5)
.compact();
}
private Map<String, Object> createHeader() {
Map<String, Object> header = new HashMap<>();
header.put("typ","JWT");
header.put("alg","HS256"); // 해시 256 암호화
return header;
}
private Map<String, Object> createClaims(User user) { // payload
Map<String, Object> claims = new HashMap<>();
claims.put("id",user.getId());
claims.put("email",user.getEmail());
return claims;
}
}
.setHeaderParam(Header.TYPE, Header.JWT_TYPE)
등을 통해 지정할 수 있습니다.compact()
를 통해 JWT 토큰을 만들 수 있습니다. private Claims getClaims(String token) {
try{
return Jwts.parser()
.setSigningKey(jwtProperties.getSecret())
.parseClaimsJws(token)
.getBody();
// 토큰 유효성 확인
} catch (SecurityException e) {
log.info("Invalid JWT signature.");
throw new CustomJwtRuntimeException();
} catch (MalformedJwtException e) {
log.info("Invalid JWT token.");
throw new CustomJwtRuntimeException();
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
throw new CustomJwtRuntimeException();
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
throw new CustomJwtRuntimeException();
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
throw new CustomJwtRuntimeException();
}
}
public String getUserEmailFromToken(String token) {
return (String) getClaims(token).get("email");
}
getBody()
를 호출하게 되면 Claim 객체를 반환하게 되는데, 여기에서 저장된 클레임 정보들을 확인할 수 있습니다. Jwts
모듈에서 호출될 수 있는 Exception은 다음과 같습니다.
UnsupportedJwtException
: 지원되지 않는 형식이거나 구성의 JWT 토큰MalformedJwtException
: 유효하지 않은 구성의 JWT 토큰ExpiredJwtException
: 만료된 JWT 토큰SignatureException
: 잘못된 JWT 서명IllegalArgumentException
: 잘못된 JWTpublic class UserJwtTest extends ServerApplicationTests {
@Autowired
private JwtTokenProvider jwtManager;
@Test
@DisplayName("토큰 생성 및 복호화 테스트")
void tokenTest() {
LocalDateTime now = LocalDateTime.now();
final User user = User.builder()
.user_name("gildong")
.password("password")
.email("gildong@gmail.com")
.userOauthType(UserOauthType.LOCAL)
.build();
final String token = jwtManager.generateToken(user);
String email = jwtManager.getUserEmailFromToken(token);
MatcherAssert.assertThat(email,is("gildong@gmail.com"));
}
}
서비스가 잘 동작하는 것을 확인했으니 controller에 로그인시 토큰 발급을 추가해줍니다.
UserController.java
@PostMapping("/login")
public ResponseEntity<?> loginUser(@RequestBody LoginRequestDto loginRequestDto){
LoginResponseDto loginResponseDto = userService.loginUser(loginRequestDto);
if (loginResponseDto!=null) return ResponseEntity.ok(loginResponseDto);
else return new ResponseEntity(HttpStatus.NO_CONTENT);
}
UserService.java
public LoginResponseDto loginUser(LoginRequestDto loginRequestDto) {
User user = searchUserByEmail(loginRequestDto.getEmail());
if (user==null||!user.getPassword().equals(loginRequestDto.getPassword())){
return null;
}
String token = jwtTokenProvider.generateToken(user);
return new LoginResponseDto(token);
}
LoginRequestDto.java
@Getter
@Setter
public class LoginRequestDto {
private String email;
private String password;
public LoginRequestDto(String email, String password) {
this.email = email;
this.password = password;
}
}
LoginResponseDto.java
@Getter
public class LoginResponseDto {
private final String accessToken;
public LoginResponseDto(String accessToken) {
this.accessToken = accessToken;
}
}
리퀘스트를 보내면 엑세스토큰을 발급해줍니다.
https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
https://velopert.com/2350
https://memostack.tistory.com/200#toc-JWT%20%ED%86%A0%ED%81%B0%20%EC%83%9D%EC%84%B1%ED%95%98%EA%B8%B0
https://shinsunyoung.tistory.com/110?category=327358
Everything is very open and honest, and the challenges are broken down in great detail. The information is without a doubt beneficial to have. Is my website extremely successful in terms of making money? Please take a look at this: wheel spinner
감사합니다