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

[feat] 세미나 6주차 #9

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

[feat] 세미나 6주차 #9

wants to merge 1 commit into from

Conversation

hyerinhwang-sailin
Copy link
Contributor

@hyerinhwang-sailin hyerinhwang-sailin commented May 31, 2024

closes #8

To reviewers

브랜치를 잘못 생성한 이슈로 rebase하면서 main에 과제 내용이 머지돼버렸습니다..ㅠ 코드 인용해서 설명하겠습니다..

구현 기능 명세

1. 멤버 생성 시 AccessToken, RefreshToken 함께 반환

  • JwtTokenProvider에 RefreshToken TTL, Indexed 설정
     private static final Long REFRESH_TOKEN_EXPIRATION_TIME = 24 * 60 * 60 * 1000L * 14;
    public String issueRefreshToken(final Authentication authentication){
        return generateToken(authentication, REFRESH_TOKEN_EXPIRATION_TIME);
    }
  • UserJoinResponse에 refreshToken 추가 후 MemberService의 createMember에서 accessToken, refreshToken, memberId 모두 반환하도록 수정
    @Transactional
    public UserJoinResponse createMember(
            MemberCreateDto memberCreateDto
    ) {
        Member member = memberRepository.save(
                Member.create(memberCreateDto.name(), memberCreateDto.part(), memberCreateDto.age())
        );
        Long memberId = member.getId();
        UserAuthentication userAuthentication = UserAuthentication.createUserAuthentication(memberId);
        String accessToken = jwtTokenProvider.issueAccessToken(userAuthentication);
        String refreshToken = jwtTokenProvider.issueRefreshToken(userAuthentication);
//        redisTokenService.saveRefreshToken(memberId, refreshToken);
        Token refreshTokenEntity = new Token(memberId, refreshToken);
        redisTokenRepository.save(refreshTokenEntity);
        return UserJoinResponse.of(accessToken, refreshToken, memberId.toString());
    }

2. Redis를 활용해 Refresh Token으로 Access Token을 재발급

  • RedisTemplate 사용을 위해 RedisConfig 생성
@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;
    }
}
  • 재발급된 accessToken을 담을 CreateTokenByRefreshTokenResponse 생성 후, RedisTokenService에 Redis를 활용해 RefreshToken으로 AccessToken 재발급 받는 로직 및 RedisTemplate 관련 메소드 추가
public class RedisTokenService {

    private final RedisTokenRepository redisTokenRepository;
    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate<String, Object> redisTemplate;

    public CreateTokenByRefreshTokenResponse refreshToken(String token, HttpServletRequest request){
        JwtValidationType jwtValidationType = jwtTokenProvider.validateToken(token);

        if(jwtValidationType == JwtValidationType.VALID_JWT){
            Long memberId = jwtTokenProvider.getUserFromJwt(token);
            Token refreshToken = redisTokenRepository.findById(memberId).orElseThrow(
                    () -> new RuntimeException("refresh token expired or not exists")
            );
            UserAuthentication authentication = UserAuthentication.createUserAuthentication(memberId);
            authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authentication);
            return CreateTokenByRefreshTokenResponse.of(jwtTokenProvider.issueAccessToken(authentication));
        }
        throw new RuntimeException("invalid token");
    }

    private Optional<Token> findById(Long memberId) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        Token token = (Token) valueOperations.get(memberId.toString());
        return Optional.ofNullable(token);
    }

    public <S extends Token> S save(S token) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        valueOperations.set(token.getId().toString(), token);
        return token;
    }

    public void deleteById(Long memberId) {
        redisTemplate.delete(memberId.toString());
    }
}

토큰 검증 로직도 포함했습니다

  • TokenController 생성
@Controller
@RequiredArgsConstructor
@RequestMapping("/api/v1/token")
public class TokenController {

    private final RedisTokenService redisTokenService;

    @PostMapping("/refresh")
    public ResponseEntity<CreateTokenByRefreshTokenResponse> refreshToken(HttpServletRequest request) {
        final String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
        final String token;

        if (authHeader == null || !authHeader.startsWith("Bearer ")){
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }
        token = authHeader.substring(7);
        CreateTokenByRefreshTokenResponse response = redisTokenService.refreshToken(token, request);

        return ResponseEntity.status(HttpStatus.CREATED)
                .header("Location", response.accessToken())
                .body(response);
    }
}

헤더 검증 로직도 포함했습니다

실행 결과

  1. 멤버 생성
    twoTokenResponse

  2. 토큰 재발급
    tokenrefresh

구현 고민 사항

  1. RefreshToken 저장할 때 RedisTokenService에서 메소드를 만드는 게 나은가 CrudRepository의 save 메소드를 사용하는 게 나은가 고민이 됐습니다. 주석이 전자 밑이 후자입니다!
//        redisTokenService.saveRefreshToken(memberId, refreshToken);
        Token refreshTokenEntity = new Token(memberId, refreshToken);
        redisTokenRepository.save(refreshTokenEntity);
  1. 토큰과 관련된 다른 api가 추가될 가능성을 고려해서 토큰 재발급을 TokenController로 따로 분리했는데 그냥 MemberController에서 하는게 더 나았을지 궁금합니다.
  2. RedisTemplate 사용해보고 싶어서 넣어봤는데 잘 된건지 모르겠네요..ㅠㅠ 피드백 마구마구 부탁드립니다!

@junggyo1020
Copy link

junggyo1020 commented Jun 3, 2024

PR 내용 잘 확인했습니다...! 전체적인 코드 피드백보다 구현 고민사항에 대해 좀 더 생각해보고, 제 생각을 적어보겠습니다..!!!:)

  1. RefreshToken 저장할 때 RedisTokenService에서 메소드를 만드는 게 나은가 CrudRepository의 save 메소드를 사용하는 게 나은가 고민이 됐습니다. 주석이 전자 밑이 후자입니다!
  • 현재 코드를 보면 RedisTokenService에 여러 메서드들이 정의되어 있어, RedisTokenService의 'save' 메서드를 사용하는 것이 일관성이 있어 더 좋은거 같다고 생각합니다.
redisTokenService.save(Token.of(memberId, refreshToken));
  1. 토큰과 관련된 다른 api가 추가될 가능성을 고려해서 토큰 재발급을 TokenController로 따로 분리했는데 그냥 MemberController에서 하는게 더 나았을지 궁금합니다.
  • 토큰과 관련된 모든 api를 TokenController에서 한번에 처리하는 것이 가독성과 유지보수 면에서 장점이 있는 것 같습니다..! 저라면 TokenController에서 처리하다가 토큰 로직이 복잡해지는 경우에 MemberController에서 처리하는 부분도 고려해볼 것 같습니다:)

추가질문!! )

  1. 아래의 메서드가 사용되고 있는 부분이 어디인지 궁금하네요!!! PR내용에는 해당부분이 사용되고 있지 않은 것 같아서요!
    private Optional<Token> findById(Long memberId) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        Token token = (Token) valueOperations.get(memberId.toString());
        return Optional.ofNullable(token);
    }
  1. 멤버 생성 API -> 이 부분은 회원가입에 대한 부분이라 로그인 시 아이디와 비밀번호가 일치하는지 확인하는 검증과정이 추가로 필요하기 떄문에 로그인 관련 API를 따로 구현을 해야한다는 생각을 하게 되었는데 구현하실 때 이부분에 대해 다른 생각이 있으셨는지가 궁금합니다!

과제하시느라 정말 고생 많으셨습니다!!:)

@hyerinhwang-sailin
Copy link
Contributor Author

hyerinhwang-sailin commented Jun 3, 2024

추가질문!! )

  1. 아래의 메서드가 사용되고 있는 부분이 어디인지 궁금하네요!!! PR내용에는 해당부분이 사용되고 있지 않은 것 같아서요!
    private Optional<Token> findById(Long memberId) {
        ValueOperations<String, Object> valueOperations = redisTemplate.opsForValue();
        Token token = (Token) valueOperations.get(memberId.toString());
        return Optional.ofNullable(token);
    }

redisTemplate 사용하려면 메소드 필수로 정의해야하더라구요.. 그래서 일단 사용처는 없지만 정의해뒀습니다

  1. 멤버 생성 API -> 이 부분은 회원가입에 대한 부분이라 로그인 시 아이디와 비밀번호가 일치하는지 확인하는 검증과정이 추가로 필요하기 떄문에 로그인 관련 API를 따로 구현을 해야한다는 생각을 하게 되었는데 구현하실 때 이부분에 대해 다른 생각이 있으셨는지가 궁금합니다!

확장성을 생각하면 따로 구현하는 게 더 좋을 것 같아요! 열심히 코멘트 남겨주셔서 감사합니당

@jun3327
Copy link

jun3327 commented Jun 5, 2024

과제하시느라 고생하셨습니다!

고민 사항에 대해서 제 개인적인 의견은

  1. 토큰 관련 서비스 레이어가 정의되어 있다면 (redisService 같은) 정의되어 있는 곳에 crud 하는 것이 좋을 것 같습니다!
  2. 저는 MemberController에 해놨는데 생각해보니 분리해놓는게 나중에 확인할 때 편할 것 같아요
  3. 전 RedisTemplate 사용하지 않고 구현해서 이 부분은 잘 모르겠네요..!

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.

세미나 6주차
3 participants