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

Refactor #81 jwt role 추가 #81

Merged
merged 13 commits into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package leets.weeth.domain.user.application.exception;

import leets.weeth.global.common.exception.BusinessLogicException;

public class EmailNotFoundException extends BusinessLogicException {
public EmailNotFoundException() {
super(404, "Redis에 저장된 email이 없습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package leets.weeth.domain.user.application.exception;

import leets.weeth.global.common.exception.BusinessLogicException;

public class RoleNotFoundException extends BusinessLogicException {
public RoleNotFoundException() {
super(404, "Redis에 저장된 role이 없습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
import static leets.weeth.domain.user.application.dto.request.UserRequestDto.refreshRequest;

public interface UserManageUseCase {
JwtDto refresh(refreshRequest dto, HttpServletRequest request);
JwtDto refresh(HttpServletRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ public class UserManageUseCaseImpl implements UserManageUseCase {

@Override
@Transactional
public JwtDto refresh(refreshRequest dto, HttpServletRequest request) {
public JwtDto refresh(HttpServletRequest request) {

JwtDto token = jwtManageUseCase.reIssueToken(dto.email(), request);
JwtDto token = jwtManageUseCase.reIssueToken(request);

log.info("RefreshToken 발급 완료: {}", dto.email());
log.info("RefreshToken 발급 완료: {}", token);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RefreshToken이 발급되는 로직을 확인하기 위해 log를 사용하신걸로 생각했습니다.
토큰 값과같이 민감한 정보는 출력되지 않도록하고 token.substring(0, 10) 이런식으로 일부만 확인하는건 어떨까요 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

개발상 용이하게 확인 목적으로 로그를 찍어놨습니당 외부로 노출되는 부분이 아니고, 현재 개발 상태니까 별도의 로직을 넣기보단 그냥 두는 것은 어떨까요??

return new JwtDto(token.accessToken(), token.refreshToken());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,15 @@ private SocialLoginResponse registerUser(String email) {
.build();
userSaveService.save(user);

JwtDto dto = jwtManageUseCase.create(user.getId(), email);
JwtDto dto = jwtManageUseCase.create(user.getId(), email, user.getRole());

return new SocialLoginResponse(user.getId(), REGISTER, dto.accessToken(), dto.refreshToken());
}

private SocialLoginResponse login(String email) {
User user = userGetService.find(email);

JwtDto dto = jwtManageUseCase.create(user.getId(), email);
JwtDto dto = jwtManageUseCase.create(user.getId(), email, user.getRole());

return new SocialLoginResponse(user.getId(), LOGIN, dto.accessToken(), dto.refreshToken());
}
Expand Down Expand Up @@ -158,20 +158,21 @@ public void accept(Long userId) {
public void update(Long userId, String role) {
User user = userGetService.find(userId);
userUpdateService.update(user, role);
jwtRedisService.updateRole(user.getId(), role);
}

@Override
public void leave(Long userId) {
User user = userGetService.find(userId);
// 탈퇴하는 경우 리프레시 토큰 삭제
jwtRedisService.delete(user.getEmail());
jwtRedisService.delete(user.getId());
userDeleteService.leave(user);
}

@Override
public void ban(Long userId) {
User user = userGetService.find(userId);
jwtRedisService.delete(user.getEmail());
jwtRedisService.delete(user.getId());
userDeleteService.ban(user);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public CommonResponse<Void> leave(@Parameter(hidden = true) @CurrentUser Long us

@PostMapping("/refresh")
@Operation(summary = "JWT 토큰 재발급 API")
public CommonResponse<JwtDto> refresh(@Valid @RequestBody refreshRequest dto, HttpServletRequest request) {
return CommonResponse.createSuccess(JWT_REFRESH_SUCCESS.getMessage(), userManageUseCase.refresh(dto, request));
public CommonResponse<JwtDto> refresh(HttpServletRequest request) {
return CommonResponse.createSuccess(JWT_REFRESH_SUCCESS.getMessage(), userManageUseCase.refresh(request));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import leets.weeth.domain.user.domain.entity.enums.Role;
import leets.weeth.global.auth.jwt.application.dto.JwtDto;
import leets.weeth.global.auth.jwt.service.JwtRedisService;
import leets.weeth.global.auth.jwt.service.JwtProvider;
Expand All @@ -20,11 +21,11 @@ public class JwtManageUseCase {
private final JwtRedisService jwtRedisService;

// 토큰 발급
public JwtDto create(Long id, String email){
String accessToken = jwtProvider.createAccessToken(id, email);
String refreshToken = jwtProvider.createRefreshToken(id);
public JwtDto create(Long userId, String email, Role role){
String accessToken = jwtProvider.createAccessToken(userId, email, role);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

role까지 추가한거 확인했습니다!

String refreshToken = jwtProvider.createRefreshToken(userId);

updateToken(email, refreshToken);
updateToken(userId, refreshToken, role, email);

return new JwtDto(accessToken, refreshToken);
}
Expand All @@ -35,23 +36,26 @@ public void sendToken(JwtDto dto, HttpServletResponse response) throws IOExcepti
}

// 토큰 재발급
public JwtDto reIssueToken(String email, HttpServletRequest request){
public JwtDto reIssueToken(HttpServletRequest request){
String requestToken = jwtService.extractRefreshToken(request);
jwtProvider.validate(requestToken);

Long userId = jwtService.extractId(requestToken).get();

jwtRedisService.validateRefreshToken(email, requestToken);
jwtRedisService.validateRefreshToken(userId, requestToken);

JwtDto token = create(userId, email);
jwtRedisService.set(email, token.refreshToken());
Role role = jwtRedisService.getRole(userId);
String email = jwtRedisService.getEmail(userId);

JwtDto token = create(userId, email, role);
jwtRedisService.set(userId, token.refreshToken(), role, email);

return token;
}

// 리프레시 토큰 업데이트
private void updateToken(String email, String refreshToken){
jwtRedisService.set(email, refreshToken);
private void updateToken(long userId, String refreshToken, Role role, String email){
jwtRedisService.set(userId, refreshToken, role, email);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import leets.weeth.domain.user.domain.entity.User;
import leets.weeth.domain.user.domain.entity.enums.Role;
import leets.weeth.domain.user.domain.service.UserGetService;
import leets.weeth.global.auth.jwt.exception.TokenNotFoundException;
import leets.weeth.global.auth.jwt.service.JwtProvider;
Expand Down Expand Up @@ -44,7 +45,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
String accessToken = jwtService.extractAccessToken(request)
.orElseThrow(TokenNotFoundException::new);
if (jwtProvider.validate(accessToken)) {
saveAuthentication(find(accessToken));
saveAuthentication(accessToken);
}
} catch (RuntimeException e) {
log.info("error token: {}", e.getMessage());
Expand All @@ -54,12 +55,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

}

public void saveAuthentication(User myUser) {
public void saveAuthentication(String accessToken) {

String email = jwtService.extractEmail(accessToken).get();
Role role = Role.valueOf(jwtService.extractRole(accessToken).get());

UserDetails userDetailsUser = org.springframework.security.core.userdetails.User.builder()
.username(myUser.getEmail())
.username(email)
.password(DUMMY)
.roles(myUser.getRole().name())
.roles(role.name())
.build();

UsernamePasswordAuthenticationToken authentication =
Expand All @@ -68,9 +72,4 @@ public void saveAuthentication(User myUser) {

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

private User find(String accessToken) {
String email = jwtService.extractEmail(accessToken).get();
return userGetService.find(email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import leets.weeth.domain.user.domain.entity.enums.Role;
import leets.weeth.global.auth.jwt.exception.InvalidTokenException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand All @@ -17,6 +18,7 @@ public class JwtProvider {
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String EMAIL_CLAIM = "email";
private static final String ID_CLAIM = "id";
private static final String ROLE_CLAIM = "role";

@Value("${weeth.jwt.key}")
private String key;
Expand All @@ -25,13 +27,14 @@ public class JwtProvider {
@Value("${weeth.jwt.refresh.expiration}")
private Long refreshTokenExpirationPeriod;

public String createAccessToken(Long id, String email) {
public String createAccessToken(Long id, String email, Role role) {
Date now = new Date();
return JWT.create()
.withSubject(ACCESS_TOKEN_SUBJECT)
.withExpiresAt(new Date(now.getTime() + accessTokenExpirationPeriod))
.withClaim(ID_CLAIM, id)
.withClaim(EMAIL_CLAIM, email)
.withClaim(ROLE_CLAIM, role.toString())
.sign(Algorithm.HMAC512(key));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package leets.weeth.global.auth.jwt.service;

import leets.weeth.domain.user.application.exception.EmailNotFoundException;
import leets.weeth.domain.user.application.exception.RoleNotFoundException;
import leets.weeth.domain.user.domain.entity.enums.Role;
import leets.weeth.global.auth.jwt.exception.InvalidTokenException;
import leets.weeth.global.auth.jwt.exception.RedisTokenNotFoundException;
import lombok.RequiredArgsConstructor;
Expand All @@ -23,31 +26,58 @@ public class JwtRedisService {

private final RedisTemplate<String, String> redisTemplate;

public void set(String email, String refreshToken) {
String key = getKey(email);
redisTemplate.opsForValue().set(key, refreshToken, expirationTime, TimeUnit.MILLISECONDS);
public void set(long userId, String refreshToken, Role role, String email) {
Copy link
Member

@huncozyboy huncozyboy Nov 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

set 메서드가 expirationTime, email, role, refreshToken 등의 여러 역할을 한번에 처리해주고 있는데
setRefreshToken, setUserMetadata, setExpiration 이런식으로 분리를 해주는게
유지보수 측면에서 좋을거같다는 생각이 있습니다

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

메서드로 분리해두는게 확장성이 좋을 것 같아요!
수정하겠습니당

String key = getKey(userId);
redisTemplate.opsForHash().put(key, "token", refreshToken);
redisTemplate.opsForHash().put(key, "role", role.toString());
redisTemplate.opsForHash().put(key, "email", email);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hash 타입을 사용해서 여러개 필드를 그룹화해서 관리한 것이 좋아보입니다

redisTemplate.expire(key, expirationTime, TimeUnit.MINUTES);
log.info("Refresh Token 저장/업데이트: {}", key);
}

public void delete(String email) {
String key = getKey(email);
public void delete(Long userId) {
String key = getKey(userId);
redisTemplate.delete(key);
}

public void validateRefreshToken(String email, String requestToken) {
if (!find(email).equals(requestToken)) {
public void validateRefreshToken(long userId, String requestToken) {
if (!find(userId).equals(requestToken)) {
throw new InvalidTokenException();
}
}

private String find(String email) {
String key = getKey(email);
return Optional.ofNullable(redisTemplate.opsForValue().get(key))
.orElseThrow(RedisTokenNotFoundException::new);
public String getEmail(long userId) {
String key = getKey(userId);
String roleValue = (String) redisTemplate.opsForHash().get(key, "email");

return Optional.ofNullable(roleValue)
.orElseThrow(EmailNotFoundException::new);
}

private String getKey(String email){
return PREFIX + email;
public Role getRole(long userId) {
String key = getKey(userId);
String roleValue = (String) redisTemplate.opsForHash().get(key, "role");

return Optional.ofNullable(roleValue)
.map(Role::valueOf)
.orElseThrow(RoleNotFoundException::new);
Comment on lines +64 to +66
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

두개의 예외처리 발생시 RoleNotFoundException로 한번에 처리해주는 것도 좋지만,
null인 경우와 잘못된 Role 값이 저장된 경우의 예외처리를 나눠서 서비스단에서 처리해주는 방식은 어떨까요 ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

유저의 역할이 저장되기 때문에 레디스에 저장될 때 잘못된 값이 저장될 일이 없다고 생각합니다.
이전 로직에서 Role에 대한 검증은 이루어진 후에 저장이 된다고 생각해요!
그렇다면 조회의 책임을 지는 메서드에서 잘못된 값을 검증할 필요는 없지 않을까요?? 어떻게 생각하시는지 궁금합니다!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 이중으로 검증을 해줄 필요는 없으니까 해당 방식으로 간결하게 구현하는게 좋을거같습니다 !

}

public void updateRole(long userId, String role) {
String key = getKey(userId);

if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
redisTemplate.opsForHash().put(key, "role", role);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"role"로 겹치는데 JwtProvider의 private static final String ROLE_CLAIM = "role";처럼 상수화 해주시면 좋을거같습니다!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상수처리를 깜빡했네요! 수정하겠습니다

}
}

private String find(long userId) {
String key = getKey(userId);
return Optional.ofNullable((String) redisTemplate.opsForHash().get(key, "token"))
.orElseThrow(RedisTokenNotFoundException::new);
}

private String getKey(long userId) {
return PREFIX + userId;
}
}
14 changes: 14 additions & 0 deletions src/main/java/leets/weeth/global/auth/jwt/service/JwtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class JwtService {

private static final String EMAIL_CLAIM = "email";
private static final String ID_CLAIM = "id";
private static final String ROLE_CLAIM = "role";
private static final String BEARER = "Bearer ";
private static final String LOGIN_SUCCESS_MESSAGE = "자체 로그인 성공.";

Expand Down Expand Up @@ -70,6 +71,19 @@ public Optional<Long> extractId(String token) {
}
}

public Optional<String> extractRole(String token) {
try {
return Optional.ofNullable(JWT.require(Algorithm.HMAC512(key))
.build()
.verify(token)
.getClaim(ROLE_CLAIM)
.asString());
} catch (Exception e) {
log.error("액세스 토큰이 유효하지 않습니다.");
return Optional.empty();
}
}

// header -> body로 수정
public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) throws IOException {
response.setStatus(HttpServletResponse.SC_OK);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
Long userId = user.getId();

// 토큰 발급 및 레디스에 저장
JwtDto token = jwtManageUseCase.create(userId, email);
JwtDto token = jwtManageUseCase.create(userId, email, user.getRole());

// 바디에 담아서 보내기
jwtManageUseCase.sendToken(token, response); // 응답 헤더에 AccessToken, RefreshToken 실어서 응답
Expand Down