본문 바로가기
프로젝트/USports

[USports] Login 구현

by JayAlex07 2023. 12. 5.

[USports] Login 구현

 

MemberController

 

로그인

  • 로그인을 하면 access token과 refresh token을 받게 된다
  • MemberDto에 UserDetail을 넣어 활용했다
  • memberService.loginMember()에서는 간단하게 비밀번호가 일치하는지 그리고 유저가 DB에 존재하는 확인하면 된다
  • 확인 후 MemberDto를 리턴하고, tokenProvider.saveTokenInRedis() 를 진행한다

 

재발급

  • access token이 만료되었을 경우 reissue, 재발급을 통해 refresh token과 access token을 받게 된다
  • tokenProvider.regenerateToken(refreshToken) 을 통해 재발급을 진행

 

로그아웃

  • 로그아웃을 하면 refresh token은 삭제 그리고 access token은 blacklist로 돌린다
  • tokenProvider.resolveTokenFromRequest(accessToken) 을 하고 access token을 리턴한다
  • memberService.logoutMember()
    • refresh token을 삭제 (refresh token을 redis에서 못 찾을 경우, 로그아웃 실패)
    • access token을 블랙 리스트로 넣음
@RestController
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberContoller {

    private final MemberService memberService;
    private final TokenProvider tokenProvider;

    @PostMapping("/login")
    @ApiOperation(value = "회원 로그인 하기", notes = "access token과 refresh token 생성")
    public ResponseEntity<MemberLogin.Response> login(
            @RequestBody MemberLogin.Request request
    ){

        MemberDto memberDto = memberService.loginMember(request);

        return ResponseEntity.ok(MemberLogin.Response.builder()
                        .tokenDto(tokenProvider.saveTokenInRedis(memberDto.getEmail()))
                .build());
    }

    @PostMapping("/login/reissue")
    @ApiOperation(value = "Access token 재발급하기", notes = "refresh token 확인 후, Access token 재발급하기")
    public ResponseEntity<TokenDto> reissueAccessToken(
        @RequestHeader("RefreshToken") String refreshToken
    ){
        return ResponseEntity.ok(tokenProvider.regenerateToken(refreshToken));
    }

    @PostMapping("/logout")
    @ApiOperation(value="로그아웃", notes="refreshToken을 삭제하고, access token을 blackList로 돌린다")
    @PreAuthorize("hasAnyRole('ROLE_UNAUTH', 'ROLE_ADMIN', 'ROLE_USER')")
    public ResponseEntity<String> memberLogout(
            @AuthenticationPrincipal MemberDto memberDto,
            @RequestHeader("Authorization") String accessToken
    ) {
        String token = tokenProvider.resolveTokenFromRequest(accessToken);

        return ResponseEntity.ok(memberService.logoutMember(token, memberDto.getEmail()));
    }
}

 

TokenProvider

  • generateToken(String email, Long expireTime)
    • 토큰을 생성하는 메서드다
    • refresh token 그리고 access token의 차이점은 만료 시간이기 때문에, expireTime을 넣어준다
  • regenerateToken(String refreshToken)
    • refreshToken을 확인하고, accessToken 그리고 refreshToken을 재발급해준다
    • 여기서 refreshToken도 새로 redis에 저장해준다
  • saveTokenInRedis(String email)
    • key는 email 그리고 value는 refresh token 으로 redis에 저장해준다
  • isAccessTokenDenied(String accessToken)
    • 인증을 할 때에, 해당 access token이 redis의 블랙리스트로 저장되어 있는지 확인하는 것이다
    • blacklist라는 것은 로그아웃을 했지만, 아직 access token의 만료기간이 남아 있는 경우의 토큰을 추가하는 리스트다
@Component
@RequiredArgsConstructor
public class TokenProvider {

    private final MemberServiceImpl memberServiceImpl;
    private final TokenRepository refreshTokenRepository;

    @Value("${spring.jwt.secret.key}")
    private String secretKey;

    // 토큰 생성 매서드
    public String generateToken(String email, Long expireTime) {
        Claims claims = Jwts.claims().setSubject(email);

        Date now = new Date();
        Date expiredDate = new Date(now.getTime() + expireTime);

        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now) // 토큰 생성시간
                .setExpiration(expiredDate) // 토큰 만료 시간
                .signWith(SignatureAlgorithm.HS512, this.secretKey) // 사용할 암호화 알고리즘, 비밀키
                .compact();
    }

    public TokenDto regenerateToken(String refreshToken){

        if(!validateToken(refreshToken)) {
            throw new JwtException(ErrorCode.JWT_EXPIRED.getDescription());
        }

        Claims claims = parseClaims(refreshToken);

        String email = claims.getSubject();

        String findToken = refreshTokenRepository.getToken(email);

        if (!refreshToken.equals(findToken)) {
            throw new JwtException(ErrorCode.JWT_REFRESH_TOKEN_NOT_FOUND.getDescription());
        }

        return saveTokenInRedis(email);
    }

    public TokenDto saveTokenInRedis(String email){
        String accessToken = generateToken(email, TokenConstant.ACCESS_TOKEN_VALID_TIME);

        String refreshToken = generateToken(email, TokenConstant.REFRESH_TOKEN_VALID_TIME);

        refreshTokenRepository.saveToken(refreshToken, email);

        return TokenDto.builder()
                .tokenType(TokenConstant.BEARER)
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

~~~~~~~~~~~~~~~

    /**
     * access token이 redis denied map에 포함되었는지 확인
     */
    public boolean isAccessTokenDenied(String accessToken) {
        return refreshTokenRepository.existsBlackListAccessToken(accessToken);
    }

~~~~~~~~~~~~~~~

}

 

RefreshTokenRepository

@Component
@RequiredArgsConstructor
public class RefreshTokenRepository implements TokenRepository {

    private final RedisTemplate redisTemplate;

    @Override
    public void saveToken(String refreshToken, String email) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(TokenConstant.REFRESH_TOKEN_PREFIX + email,
                refreshToken,
                Duration.ofMillis(TokenConstant.REFRESH_TOKEN_VALID_TIME));
    }

    @Override
    public String getToken(String email) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        return values.get(TokenConstant.REFRESH_TOKEN_PREFIX + email);
    }

    // 로그아웃 할 때
    @Override
    public boolean deleteToken(String email) {
        boolean result = redisTemplate.hasKey(TokenConstant.REFRESH_TOKEN_PREFIX + email);

        if (result) redisTemplate.delete(TokenConstant.REFRESH_TOKEN_PREFIX + email);

        return result;
    }

    /**
     * 로그아웃 할 때에, accessToken 만료 시간이 남아 있을 수 있음으로, 레디스에 저장
     * @param accessToken
     */
    @Override
    public void addBlackListAccessToken(String accessToken) {
        ValueOperations<String, String> values = redisTemplate.opsForValue();
        values.set(TokenConstant.ACCESS_TOKEN_PREFIX + accessToken,
                TokenConstant.BLACK_LIST,
                TokenConstant.ACCESS_TOKEN_VALID_TIME);
    }

    /**
     * 로그아웃 할 때에, accessToken 만료 시간이 남아 있을 수 있음으로, 레디스에 저장
     * 해당 accessToken은 사용하지 못 한다
     * @param accessToken
     */
    @Override
    public boolean existsBlackListAccessToken(String accessToken) {
        return redisTemplate.hasKey(TokenConstant.ACCESS_TOKEN_PREFIX + accessToken);
    }
}

 

JwtAuthenticationFilter

  • doFilterInternal에서 유효성 검증을 위해 tokenProvider.isAccessTokenDenied()을 추가해줬다
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    public static final String TOKEN_HEADER = "Authorization";

    private final TokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, IOException {

        String token = getTokenFromRequest(request);

        if (StringUtils.hasText(token) && tokenProvider.validateToken(token)
                && !tokenProvider.isAccessTokenDenied(token)) {
            // 토큰 유효성 검증
            Authentication auth = tokenProvider.getAuthentication(token);

            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        filterChain.doFilter(request, response);
    }

~~~~~~~~~~~~~~~
}

'프로젝트 > USports' 카테고리의 다른 글

[USports] Websocket 소개  (0) 2023.12.18
[USports] Member OAuth2  (0) 2023.12.09
[USports] Member 단위 테스트  (1) 2023.12.07
[USports] Email 전송  (1) 2023.12.06
[USports] Redis + Login  (9) 2023.12.05