이번에는 jwt를 이용한 로그인 인증을 만들려 한다. 일전에 @EnableAuthorizationserver deprecated 되면서 찾던 중 jwt가 있어 이걸 활용하기로 했다(DB로 토큰유효성 확인도 안해서 더 나은거 같기도 하고...)
암튼 시작.
JwtManager 를 만들어서 JWT를 통해 토큰을 생성, 토큰 검증 하는 클래스를 만들 것이다. 그전에 다음 dependency를 먼저 추가하자.(gradle 기준)
...
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
...
Jwt를 관리할 매니저를 만들것이다. 의존성을 없이 기능만 하는 클래스를 만들어 어디서든 활용할 수 있도록 할 것이다.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SignatureException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidParameterException;
import java.security.Key;
import java.util.Date;
public class JwtManager {
// 30분
private long ACCESS_TOKEN_VALIDATiON_SECOND = 60 * 30;
// 1개월
private long REFRESH_TOKEN_VALIDATiON_SECOND = 60 * 60 * 24 * 30;
private String secretKey;
public JwtManager(String secretKey) {
this.secretKey = secretKey;
}
public JwtManager(
String secretKey
, Long accessTokenValidationSecond
, Long refreshTokenValidationSecond
) {
this.secretKey = secretKey;
this.ACCESS_TOKEN_VALIDATiON_SECOND = accessTokenValidationSecond;
this.REFRESH_TOKEN_VALIDATiON_SECOND = refreshTokenValidationSecond;
}
private Key getSigninKey(String secretKey) {
byte[] keyBytes = secretKey.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
// 토큰 해석
public Claims validTokenAndReturnBody(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(getSigninKey(secretKey))
.build()
.parseClaimsJws(token)
.getBody();
} catch(ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | SignatureException | IllegalArgumentException e) {
e.printStackTrace();
throw new InvalidParameterException("유효하지 않은 토큰입니다");
}
}
// 유저id 조회
public String getName(String token) {
return validTokenAndReturnBody(token).get("username", String.class);
}
// 토큰 만료
private Boolean isTokenExpired(String token){
Date expiration = validTokenAndReturnBody(token).getExpiration();
return expiration.before(new Date());
}
// access token 생성
public String generateAccessToken(String username) {
return doGenerateToken(username, ACCESS_TOKEN_VALIDATiON_SECOND * 1000l);
}
// refresh token 생성
public String generateRefreshToken(String username) {
return doGenerateToken(username, REFRESH_TOKEN_VALIDATiON_SECOND * 1000l);
}
// accessToken 유효시간 알림(second)
public Long getValidationAccessTokenTime(){
return ACCESS_TOKEN_VALIDATiON_SECOND;
}
private String doGenerateToken(String username, Long expireTime) {
Claims claims = Jwts.claims();
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expireTime))
.signWith(getSigninKey(secretKey), SignatureAlgorithm.HS256)
.compact();
}
}
생성자를 보면 2개가 있는데 하나는 secretKey만 받는걸로, 하나는 accessToken과 refreshToken의 유효시간(초단위)을 조절하는 것이다. 보통 accessToken 30분이나 1시간 정도로 설정해두면 된다.
그럼 jwtManager를 테스트해보자. 두 테스트 모두 유효시간을 1초로 두고 테스트했다.
@ExtendWith(SpringExtension.class)
public class TestJwtSample {
// secret key가 짧으면 에러가 난다
private final String secretKey = "secretKey-test-authorization-jwt-manage-token";
@Test
@DisplayName("토큰 정상 발급")
void successTest() {
// given
JwtManager jwtManager = new JwtManager(secretKey, 1l, 60l);
// when
String username = "testuser-1";
String accessToken = jwtManager.generateAccessToken(username);
Claims claims = jwtManager.validTokenAndReturnBody(accessToken);
System.out.println("claims = " + claims);
String findUsername = claims.get("username", String.class);
// then
assertThat(username).isEqualTo(jwtManager.getName(accessToken));
assertThat(username).isEqualTo(findUsername);
}
@Test
@DisplayName("토큰유효시간 over")
void expireTokenTest() throws InterruptedException {
// given
JwtManager jwtManager = new JwtManager(secretKey, 1l, 60l);
String username = "testuser-1";
String accessToken = jwtManager.generateAccessToken(username);
// 2초 딜레이
Thread.sleep(2000l);
// when
InvalidParameterException ex = assertThrows(
InvalidParameterException.class
, () -> jwtManager.validTokenAndReturnBody(accessToken));
// then
assertThat(ex.getMessage()).isEqualTo("유효하지 않은 토큰입니다");
}
}
테스트 통과 된 것을 확인할 수 있다.
생성된 토큰을 해석하면 다음과 같은 값을 가지고 있다
claims = {username=testuser-1, iat=1617413941, exp=1617413942}
claims 안에 다른 정보도 추가할 수 있는데 그럴수록 토큰이 길어지니까 최소값만 넣는 것을 추천한다.
참고로 secretKey 글자가 작으면 다음같은 에러가 발생한다. 그러니 충분한 길이로 만들어주자.
io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 72 bits which is not secure enough for any JWT HMAC-SHA algorithm. The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size >= 256 bits (the key size must be greater than or equal to the hash output size). Consider using the io.jsonwebtoken.security.Keys#secretKeyFor(SignatureAlgorithm) method to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See https://tools.ietf.org/html/rfc7518#section-3.2 for more information. at io.jsonwebtoken.security.Keys.hmacShaKeyFor(Keys.java:96) |
끝.
'공부 > 프로그래밍' 카테고리의 다른 글
[react] material-ui, styled-components 같이 쓸때 테마 적용 안될때 (0) | 2021.04.09 |
---|---|
[springboot, jwt] jwt로 security 적용 & error 때 result form 지정해 리턴하기 (0) | 2021.04.07 |
[react, springboot] kakao 로그인 API 연동 (0) | 2021.04.02 |
[react, next.js, redux] material-ui 를 이용해 다크테마 적용하기(Layout.js 적용) (0) | 2021.04.01 |
[react] wrapper.getServerSideProps 의 문제점(store 값 초기화 문제) (0) | 2021.03.30 |
댓글