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

[week6] 6차 세미나 실습 과제 #8

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

softmoca
Copy link
Member

@softmoca softmoca commented Jun 2, 2024

To Reviewer

  • 6차 세미나 자료를 통해 복습을 하며 과제를 하다가 코드와 깃이 너무 꼬여 week6 디렉토리를 통으로 복사한 뒤 추가 구현을 하여 PR을 올렸습니다. 리뷰어 분들이 chagned files 확인이 어려울꺼 같아 아래 "구현 사항" 부분에 추가 한 코드를 정리하였습니다.

6주차 실습 과제

  • 로그인을 진행할 때 AccessToken과 Refresh Token을 함께 반환하는 로직
  • Redis를 활용해 Refresh Token으로 Access Token을 재발급 받는 로직

구현 사항

[1] Redis를 설정하는 RedisConfig 클래스

  • Redis 서버에 연결하고 데이터를 읽고 쓸 수 있도록 RedisConnectionFactory와 RedisTemplate 빈을 생성.
@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

[2] Redis에 저장될 Token 엔티티를 정의한 클래스

  • Redis에 Token 객체를 해시 형태로 저장하기 위해 @RedisHash 어노테이션으로 TTL을 설정하고, @indexed 어노테이션으로 refreshToken 필드를 인덱싱.
@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();
    }
}

[3] Redis에 저장된 Token 객체를 관리하기 위한 리포지토리 인터페이스

  • findByRefreshToken: refreshToken 값을 기준으로 Token 객체를 검색
    Optional<Token> findByRefreshToken(final String refreshToken);
    Optional<Token> findById(final Long id);

[4] accessToken과refreshToken을 반환해 주기 위한 DTO 클래스

  • refreshToken을 추가
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);
    }
}

[5] JWT 토큰의 생성, 유효성 검사, 파싱 등의 기능을 제공하는 JwtTokenProvider 클래스

  • accessToken의 만료 시간을 줄이고 refreshToken에게 비교적 더 긴 만료시간을 세팅
    private static final Long ACCESS_TOKEN_EXPIRATION_TIME = 24 * 60 * 1000L;
    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 generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
    }

[6] 회원 가입시 accessToken과 refreshToken를 반환하는 MemberService의 createMember 메서드

  • 만료시간이 짧은 accessToken만 주는게 비교적 긴 refreshToken을 같이 반환.
    @Transactional
    public UserJoinResponse createMember(
            MemberCreateDto memberCreate
    ) {
        Member member = memberRepository.save(
                Member.create(memberCreate.name(), memberCreate.part(), memberCreate.age())
        );
        Long memberId = member.getId();
        String accessToken = jwtTokenProvider.issueAccessToken(
                UserAuthentication.createUserAuthentication(memberId)
        );
        String refreshToken = jwtTokenProvider.issueRefreshToken(
                UserAuthentication.createUserAuthentication(memberId)
        );
        redisTokenRepository.save(Token.of(memberId, refreshToken));

        return UserJoinResponse.of(accessToken, refreshToken, memberId.toString());
    }

[7] refreshToken을 통해 새로운 accessToken과 refreshToken을 발급해 주는 MemberController

  • 헤더의 Bearer 값인 refreshToken을 통해 새로운 accessToken과 refreshToken를 발급
    @GetMapping("/refresh")
    public ResponseEntity<UserJoinResponse> refreshToken(@RequestHeader("Authorization") String refreshToken){

        refreshToken = refreshToken.substring(7); // "Bearer " 부분을 제거


        UserJoinResponse userJoinResponse = memberService.refreshToken(refreshToken);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(userJoinResponse);
    

[8] 새로운 accessToken과 refreshToken를 반환하는 MemberService의 refreshToken 메서드

  • 요청으로 받은 refreshToken으로 레디스를 탐색하여 memberId를 찾은 후 새로운 accessToken과 refreshToken 발급
    @Transactional
    public UserJoinResponse refreshToken(String refreshToken) {
        // Refresh Token 유효성 검증
        if (jwtTokenProvider.validateToken(refreshToken) != JwtValidationType.VALID_JWT) {
            throw new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION);
        }

        // Refresh Token으로 사용자 ID 찾기
        Long memberId = redisTokenRepository.findByRefreshToken(refreshToken)
                .map(Token::getId)
                .orElseThrow(() -> new UnauthorizedException(ErrorMessage.JWT_UNAUTHORIZED_EXCEPTION));

        // 새로운 Access Token 및 Refresh Token 발급
        String newAccessToken = jwtTokenProvider.issueAccessToken(UserAuthentication.createUserAuthentication(memberId));
        String newRefreshToken = jwtTokenProvider.issueRefreshToken(UserAuthentication.createUserAuthentication(memberId));

        // 새로운 Refresh Token 저장
        redisTokenRepository.save(Token.of(memberId, newRefreshToken));

        return UserJoinResponse.of(newAccessToken, newRefreshToken, memberId.toString());
    }
    

API 테스트

AcessToken과 RefreshToken을 함께 반환하는 로직 ✅

image

image

  • 회원가입시 AcessToken과 RefreshToken이 잘 반환되며 Redis에도 해당 해시와 키값들이 잘 저장이 된다.

RefreshToken 유효성 검사 ✅

image
  • 유효하지 않은 이상한 문자열의 경우 검증 실패 메세지 반환한다.

RefreshToken을 통해해 새로운 AcessToken과 RefreshToken을 함께 반환하는 로직 ✅

image

image

  • 회원가입시 받은 RefreshToken을 사용해 요청시 새로운 AcessToken과 RefreshToken을 잘 반환하며 redis에도 잘 반영이 된다.

질문있어요 🙋🏻‍♂️

1. RefreshToken을 통해 새로운 토큰들을 발급 받는 controller와 service는 Redis 모듈에 구현하는게 좋은지 Member 모듈에 구현을 하는게 좋을지가 궁금합니다 !

2. Redis에 refreshToken을 저장하는 이유가 궁금합니다 !

  • 사용자의 요청 헤더의 refreshToken을 서버에서 가지고 있는 secretkey를 사용해서 디코딩하면 사용자의 정보를 찾을수 있을 꺼같다고 생각이 되는데 굳이 왜 Redis에 refreshToken과 사용자의 id를 저장하고 redis를 조회해서 사용자의 정보를 얻는지가 궁금합니다 !
  • Redis를 사용한 주 이유가 빠른 시간이라고 생각이 되는데 서버에서 디코딩을 하면 시간 측면에서 효율이 좋지 못해서....(?)

@softmoca softmoca linked an issue Jun 2, 2024 that may be closed by this pull request
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.

[feat] 6주차 실습과제
1 participant