[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);
}
~~~~~~~~~~~~~~~
}