-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
6차 세미나 실습 과제 (#13) #15
base: main
Are you sure you want to change the base?
Changes from all commits
5bdf659
5833c27
76e4c89
e4d1f5a
f7d80fc
ae8e95e
1b7f8a0
58c6864
712ec85
7602719
1e3ebc3
aa8f297
6227587
faa1ea4
b62dd35
192c4ed
3d70327
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package org.sopt.springFirstSeminar.common; | ||
|
||
public class Constant { | ||
public static final String AUTHORIZATION = "Authorization"; | ||
public static final String BEARER = "Bearer "; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package org.sopt.springFirstSeminar.common.jwt; | ||
|
||
import io.jsonwebtoken.*; | ||
import io.jsonwebtoken.security.Keys; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
import javax.crypto.SecretKey; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
|
||
@Component | ||
public class JwtTokenGenerator { | ||
|
||
@Value("${jwt.secret}") | ||
private String secretKey; | ||
|
||
@Value("${jwt.access-token-expire-time}") //1분 | ||
private long ACCESS_TOKEN_EXPIRE_TIME; | ||
|
||
@Value("${jwt.refresh-token-expire-time}") //1시간 | ||
private long REFRESH_TOKEN_EXPIRE_TIME; | ||
|
||
public String generateToken(final Long userId, boolean isAccessToken) { | ||
final Date presentDate = new Date(); | ||
final Date expireDate = generateExpireDateByToken(isAccessToken, presentDate); | ||
|
||
return Jwts.builder() | ||
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) | ||
.setSubject(String.valueOf(userId)) | ||
.setIssuedAt(presentDate) | ||
.setExpiration(expireDate) | ||
.signWith(getSigningKey(), SignatureAlgorithm.HS256) //여기서 어떤 알고리즘을 사용할 지를 명시적으로 적어주는게 좋음, 안 적어주면 라이브러리 기본 설정에 의존하게됨 | ||
.compact(); | ||
} | ||
|
||
public JwtParser getJwtParser() { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(getSigningKey()) | ||
.build(); | ||
} | ||
|
||
private Date generateExpireDateByToken(final boolean isAccessToken, Date presentDate) { | ||
return new Date(presentDate.getTime() + setExpireTimeByToken(isAccessToken)); | ||
} | ||
|
||
//토근에 따라 만료시간 다름 | ||
private long setExpireTimeByToken(final boolean isAccessToken) { | ||
if (isAccessToken) { | ||
return ACCESS_TOKEN_EXPIRE_TIME; | ||
} else { | ||
return REFRESH_TOKEN_EXPIRE_TIME; | ||
} | ||
} | ||
|
||
public SecretKey getSigningKey() { | ||
String encodedKey = Base64.getEncoder().encodeToString(secretKey.getBytes()); //SecretKey 통해 서명 생성 | ||
return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,84 +1,26 @@ | ||
package org.sopt.springFirstSeminar.common.jwt; | ||
|
||
import io.jsonwebtoken.*; | ||
import io.jsonwebtoken.security.Keys; | ||
import io.jsonwebtoken.JwtParser; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.security.core.Authentication; | ||
import org.springframework.stereotype.Component; | ||
import org.sopt.springFirstSeminar.common.jwt.dto.Token; | ||
import org.springframework.stereotype.Service; | ||
|
||
import javax.crypto.SecretKey; | ||
import java.util.Base64; | ||
import java.util.Date; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
@Service | ||
public class JwtTokenProvider { | ||
|
||
private static final String USER_ID = "userId"; | ||
|
||
private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 100L * 14; | ||
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; | ||
|
||
|
||
@Value("${jwt.secret}") | ||
private String JWT_SECRET; | ||
|
||
|
||
public String issueAccessToken(final Authentication authentication) { | ||
return generateToken(authentication, ACCESS_TOKEN_EXPIRATION_TIME); | ||
} | ||
|
||
// public String issueRefreshToken(final Authentication authentication) { | ||
// return issueToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME); | ||
// } | ||
|
||
|
||
public String generateToken(Authentication authentication, Long tokenExpirationTime) { | ||
final Date now = new Date(); | ||
final Claims claims = Jwts.claims() | ||
.setIssuedAt(now) | ||
.setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간 | ||
|
||
claims.put(USER_ID, authentication.getPrincipal()); | ||
|
||
return Jwts.builder() | ||
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header | ||
.setClaims(claims) // Claim | ||
.signWith(getSigningKey()) // Signature | ||
.compact(); | ||
} | ||
|
||
private SecretKey getSigningKey() { | ||
String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성 | ||
return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 | ||
} | ||
|
||
public JwtValidationType validateToken(String token) { | ||
try { | ||
final Claims claims = getBody(token); | ||
return JwtValidationType.VALID_JWT; | ||
} catch (MalformedJwtException ex) { | ||
return JwtValidationType.INVALID_JWT_TOKEN; | ||
} catch (ExpiredJwtException ex) { | ||
return JwtValidationType.EXPIRED_JWT_TOKEN; | ||
} catch (UnsupportedJwtException ex) { | ||
return JwtValidationType.UNSUPPORTED_JWT_TOKEN; | ||
} catch (IllegalArgumentException ex) { | ||
return JwtValidationType.EMPTY_JWT; | ||
} | ||
} | ||
private final JwtTokenGenerator jwtTokenGenerator; | ||
|
||
private Claims getBody(final String token) { | ||
return Jwts.parserBuilder() | ||
.setSigningKey(getSigningKey()) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
public Token issueTokens(final Long userId) { | ||
return Token.of( | ||
jwtTokenGenerator.generateToken(userId, true), | ||
jwtTokenGenerator.generateToken(userId, false)); | ||
} | ||
|
||
public Long getUserFromJwt(String token) { | ||
Claims claims = getBody(token); | ||
return Long.valueOf(claims.get(USER_ID).toString()); | ||
public Long getSubject(String accessToken) { | ||
JwtParser jwtParser = jwtTokenGenerator.getJwtParser(); | ||
return Long.valueOf(jwtParser.parseClaimsJws(accessToken) | ||
.getBody() | ||
.getSubject()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package org.sopt.springFirstSeminar.common.jwt; | ||
|
||
import io.jsonwebtoken.*; | ||
import lombok.RequiredArgsConstructor; | ||
import org.sopt.springFirstSeminar.common.dto.ErrorMessage; | ||
import org.sopt.springFirstSeminar.exception.UnauthorizedException; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.stereotype.Service; | ||
|
||
|
||
@RequiredArgsConstructor | ||
@Service | ||
public class JwtTokenValidator { | ||
|
||
private final JwtTokenGenerator jwtTokenGenerator; | ||
|
||
public void validateAccessToken(String accessToken) { | ||
try { | ||
parseToken(accessToken); | ||
} catch (ExpiredJwtException e) { | ||
throw new UnauthorizedException(ErrorMessage.EXPIRED_ACCESS_TOKEN); | ||
} catch (MalformedJwtException ex) { | ||
throw new UnauthorizedException(ErrorMessage.INVALID_ACCESS_TOKEN); | ||
} catch (UnsupportedJwtException ex) { | ||
throw new UnauthorizedException(ErrorMessage.UNSUPPORTED_ACCESS_TOKEN); | ||
} catch (IllegalArgumentException ex) { | ||
throw new UnauthorizedException(ErrorMessage.EMPTY_ACCESS_TOKEN); | ||
} catch (SignatureException ex) { | ||
throw new UnauthorizedException(ErrorMessage.JWT_SIGNATURE_EXCEPTION); | ||
} | ||
} | ||
|
||
public void validateRefreshToken(String refreshToken) { | ||
try { | ||
parseToken(refreshToken); | ||
} catch (ExpiredJwtException e) { | ||
throw new UnauthorizedException(ErrorMessage.EXPIRED_REFRESH_TOKEN); | ||
} catch (Exception e) { | ||
throw new UnauthorizedException(ErrorMessage.INVALID_REFRESH_TOKEN_VALUE); | ||
} | ||
} | ||
|
||
public void equalsRefreshToken(String refreshToken, String storedRefreshToken) { | ||
if (!refreshToken.equals(storedRefreshToken)) { | ||
throw new UnauthorizedException(ErrorMessage.MISMATCH_REFRESH_TOKEN); | ||
} | ||
} | ||
|
||
private void parseToken(String token) { | ||
JwtParser jwtParser = jwtTokenGenerator.getJwtParser(); | ||
jwtParser.parseClaimsJws(token); | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package org.sopt.springFirstSeminar.common.jwt.auth; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
@Target(ElementType.PARAMETER) | ||
@Retention(RetentionPolicy.RUNTIME) | ||
public @interface MemberId { | ||
} | ||
Comment on lines
+8
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오... 어노테이션 활용하는 거 처음 봤습니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package org.sopt.springFirstSeminar.common.jwt.auth; | ||
|
||
|
||
import jakarta.persistence.Id; | ||
import lombok.AccessLevel; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Getter; | ||
import org.springframework.data.redis.core.RedisHash; | ||
import org.springframework.data.redis.core.index.Indexed; | ||
|
||
@RedisHash(value = "", timeToLive = 60 * 60 * 24 * 1000L) | ||
@AllArgsConstructor(access = AccessLevel.PRIVATE) | ||
@Getter | ||
@Builder(access = AccessLevel.PRIVATE) | ||
public class RefreshToken { | ||
|
||
@Id | ||
private Long id; | ||
|
||
@Indexed | ||
private String refreshToken; | ||
|
||
public static RefreshToken of(final Long memberId, final String refreshToken) { | ||
return RefreshToken.builder() | ||
.id(memberId) | ||
.refreshToken(refreshToken) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package org.sopt.springFirstSeminar.common.jwt.auth; | ||
|
||
import org.springframework.core.MethodParameter; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.bind.support.WebDataBinderFactory; | ||
import org.springframework.web.context.request.NativeWebRequest; | ||
import org.springframework.web.method.support.HandlerMethodArgumentResolver; | ||
import org.springframework.web.method.support.ModelAndViewContainer; | ||
|
||
//Argument Resolver를 사용하면 컨트롤러 메서드의 파라미터 중 특정 조건에 맞는 파라미터가 있다면, | ||
// 요청에 들어온 값을 이용해 원하는 객체를 만들어 바인딩해줄 수 있다. | ||
@Component | ||
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver { | ||
@Override | ||
public boolean supportsParameter(MethodParameter parameter) { | ||
|
||
//요청받은 메소드의 파라미터에 @UserId 어노테이션이 붙어있는지 확인 | ||
boolean hasUserIdAnnotation = parameter.hasParameterAnnotation(MemberId.class); | ||
|
||
//타입이 같은지 확인 | ||
// boolean isLongType = Long.class.isAssignableFrom(parameter.getParameterType()); | ||
boolean isLongType = Long.class.equals(parameter.getParameterType()); | ||
|
||
//둘 다 true면 아래 resolveArgument 메서드 실행 | ||
return hasUserIdAnnotation && isLongType; | ||
} | ||
|
||
//바인딩할 객체로 리턴하는 메서드 | ||
@Override | ||
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { | ||
return SecurityContextHolder.getContext() | ||
.getAuthentication() | ||
.getPrincipal(); | ||
} | ||
Comment on lines
+31
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아마 여기서 매개변수를 파싱하는 것 같은데, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기서는 바인딩할 객체를 리턴해주는 코드입니다!! |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
atk와 rtk의 내용이 다르지 않다면 같은 validate를 쓰는 것도 나쁘지 않을 것 같은데 따로 분리한 이유가 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
에러 메세지가 다르기 때문에 나눠주었습니다!!