Skip to content
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

Hw/#4 6주차 세미나 실습 과제 #12

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

Hw/#4 6주차 세미나 실습 과제 #12

wants to merge 4 commits into from

Conversation

elive7
Copy link
Contributor

@elive7 elive7 commented Jun 2, 2024

이 주의 과제 📕

회원 가입시 access token과 함꼐 refresh token 반환하는 로직

Token

@RedisHash(value = "", timeToLive = 60 * 60 * 24 * 1000L * 14)
@AllArgsConstructor
@Getter
@Builder
public class Token {
@Id
private Long id;
@Indexed
private String refreshToken;
public static Token of(
final Long id,
final String refreshToken
) {
return Token.builder()
.id(id)
.refreshToken(refreshToken)
.build();
}
}

refreshToken을 redis에 저장하기 위해, token 클래스를 정의해두었습니다.

‎TokenRepository

public interface TokenRepository extends CrudRepository<Token, Long> {
Optional<Token> findByRefreshToken(final String refreshToken);
Optional<Token> findById(final Long id);
}

CrudRepository를 상속받아 토큰을 저장하기 위한 레포지토리를 만들어주었습니다.

‎JwtTokenProvider

public String issueRefreshToken(final Authentication authentication) {
return generateRefreshToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
}

public String generateRefreshToken(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();
}

JwtTokenProvider에 refreshToken을 생성하고, authentication에 따라 발행하는 로직을 작성해주었습니다.

private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 5 * 60 * 1000L; // 5 minutes
private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14; // 14 days

이때 accessToken과 refreshToken의 만료 시간을 각각 5분과 14일로 설정하여, accessToken이 만료되더라도 refreshToken으로 새 accessToken을 받아올 수 있도록 하였습니다.

UserJoinResponse과 MemberService

public record UserJoinResponse(
String accessToken,
String refreshToken,
String userId
) {
public static UserJoinResponse of(
String accessToken,
String refreshToken,
String userId
) {
return new UserJoinResponse(accessToken, refreshToken, userId);
}
}

@Transactional
public UserJoinResponse createMember(
MemberCreateDto memberCreate
) {
Member member = memberRepository.save(
Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())
);
Long memberId = member.getId();
//Access Token 생성
String accessToken = jwtTokenProvider.issueAccessToken(
UserAuthentication.createUserAuthentication(memberId)
);
//Refresh Token 생성
String refreshToken = jwtTokenProvider.issueRefreshToken(
UserAuthentication.createUserAuthentication(memberId)
);
//Refresh Token 저장
tokenRepository.save(Token.of(memberId, refreshToken));
return UserJoinResponse.of(accessToken, refreshToken, memberId.toString());
}

UserJoinResponse에 refreshToken을 추가하고, MemberService에서도 회원가입시 Refresh Token을 생성 및 레포지토리에 저장한 후 응답으로도 Refresh Token를 돌려주는 로직을 추가해주었습니다.

access token 만료시 메세지 반환하는 로직

‎JwtAuthenticationFilter

} else if (jwtTokenProvider.validateToken(token) == EXPIRED_JWT_TOKEN) {
SecurityContextHolder.clearContext();
//throw new AccessTokenExpiredException(ErrorMessage.JWT_ACCESS_TOKEN_EXPIRED_EXCEPTION);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(
ErrorResponse.of(ErrorMessage.JWT_ACCESS_TOKEN_EXPIRED_EXCEPTION.getStatus(),
ErrorMessage.JWT_ACCESS_TOKEN_EXPIRED_EXCEPTION.getMessage())));
return;

JWT_ACCESS_TOKEN_EXPIRED_EXCEPTION 이라는 에러 메세지를 만든 후, JwtAuthenticationFilter에서 accessToken이 이미 만료되었다면, JWT_ACCESS_TOKEN_EXPIRED_EXCEPTION에 대한 응답을 리턴할 수 있도록 다음과 같은 코드를 작성해주었습니다.

refresh token으로 access token 반환하는 로직

‎TokenController

@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class TokenController {
private final TokenService tokenService;
@PostMapping("/token/refresh")
public ResponseEntity<AccessTokenResponse> refreshAccessToken(
@RequestHeader String refreshToken) {
return ResponseEntity.ok(tokenService.refreshAccessToken(refreshToken));
}
}

헤더에 refreshToken을 담아서 특정 url로 요청을 보내면, accessToken을 재발급 해주는 controller를 작성했습니다.

‎TokenService

@Service
@RequiredArgsConstructor
public class TokenService {
private final TokenRepository tokenRepository;
private final JwtTokenProvider jwtTokenProvider;
public Token findByRefreshToken(String refreshToken) {
return tokenRepository.findByRefreshToken(refreshToken).orElseThrow(
() -> new NotFoundException(ErrorMessage.REFRESH_TOKEN_NOT_FOUND)
);
}
public AccessTokenResponse refreshAccessToken(String refreshToken) {
Token token = findByRefreshToken(refreshToken);
Long memberId = token.getId();
String accessToken = jwtTokenProvider.issueAccessToken(UserAuthentication.createUserAuthentication(memberId));
return AccessTokenResponse.of(accessToken);
}
}

인자로 전달된 refreshToken을 repository에서 찾아서 해당 refreshToken이 존재한다면, refreshToken의 memberId 정보를 가지고 새롭게 accessToken을 발급해주는 로직을 작성해주었습니다. refreshToken이 존재하지 않는다면 그에 맞게 REFRESH_TOKEN_NOT_FOUND라는 오류를 발생시켜 주었습니다.

SecurityConfig

private static final String[] AUTH_WHITE_LIST = {"/api/v1/member", "/api/v1/token/refresh"};

또한, SecurityConfig에서 accessToken을 재발급하는 url를 whitelist로 지정해두었습니다.

요구사항 분석 📙

  • localhost:8080/api/v1/member로 회원가입을 시도할 시, refresh Token도 함께 반환되도록 코드를 작성했습니다.
  • 요청이 들어왔을 때, access token이 만료되었다면 그에 해당하는 error를 반환할 수 있도록 작성해주었습니다.
  • localhost:8080/api/v1/token/refresh로 헤더에 refreshToken을 담아서 보내면 재발급된 accessToken을 리턴하도록 코드를 작성해주었습니다.

구현 고민 사항 📗

  • Refresh Token과 Access Token은 서로 다른 목적으로 사용되므로 일부 차이점을 두는 것이 좋습니다. Refresh Token은 사용자 인증을 갱신하는 데 사용되며, Access Token은 실제 리소스에 접근하는 데 사용됩니다. 따라서 Refresh Token은 최소한의 정보만 포함하고, Access Token은 필요한 클레임을 포함하는 것이 일반적입니다. 이에 따라 Access Token에는 사용자의 식별자와 권한 정보를 포함하고, Refresh Token은 사용자의 식별자만 포함하도록 코드를 작성했습니다.
    public String generateAccessToken(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());
    claims.put(ROLES, authentication.getCredentials()); //accessToken에 역할을 추가적으로 부여
    return Jwts.builder()
    .setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header
    .setClaims(claims) // Claim
    .signWith(getSigningKey()) // Signature
    .compact();
    }
    public String generateRefreshToken(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();
    }

질문있어요! 📘

  • 현재 access Token이 만료되었을 경우, 그에 해당하는 error를 반환할 수 있도록 작성하는 코드를 더 깔끔하게 할 수 있는 방법이 있는지 궁금합니다. 지금은 그냥 filter 안에서 응답을 생성해주고 있는데 가독성 면에서 좋지 않은 것 같습니다,,, 더 좋은 방안이 있다면 알려주세요! 👀
  • 또한 access token 재발급 로직에서 refresh token을 헤더와 바디 중 어디에 담아 보내는 것이 좋을지 궁금합니다! 🙋
  • 지금은 tokenRepository에서 refresh token를 찾지 못했을 때 not found 오류를 주고 있는데 인증 정보가 만료된 것이니 401 401 Unauthorized를 반환하는 것이 맞는지 헷갈립니다. ✋

API 명세서 📔

https://regular-cow-aa9.notion.site/6-API-48e45cca667549fe8ed63af375853723?pvs=4
실제 동작과정은 api 명세서 안에 담았습니다!

@elive7 elive7 self-assigned this Jun 2, 2024
@elive7 elive7 requested a review from sohyundoh June 2, 2024 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[hw#4] 6주차 실습 과제
1 participant