Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -10,7 +10,10 @@
import rootbox.rootboxApp.api.user.implementation.UserCommandAdapter;
import rootbox.rootboxApp.api.user.implementation.UserQueryAdapter;
import rootbox.rootboxApp.api.user.presentation.dto.JoinDto;
import rootbox.rootboxApp.api.user.presentation.dto.ReAuthDto;
import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto;
import rootbox.rootboxApp.global.common.exception.base.GlobalErrorCode;
import rootbox.rootboxApp.global.common.exception.base.UserException;
import rootbox.rootboxApp.global.entity.RefreshToken;
import rootbox.rootboxApp.global.entity.User;
import rootbox.rootboxApp.global.entity.enums.user.SocialType;
Expand Down Expand Up @@ -112,6 +115,42 @@ public JoinDto.JoinResponseDto join(JoinDto.JoinRequestDto request, User user) {
return UserMapper.toJoinResponseDto(joinedUser);
}

public ReAuthDto.ReGenerateAccessTokenDto reGenerateAccessToken(String socialId) {

Optional<RefreshToken> refreshTokenByUserId = userQueryAdapter.findRefreshTokenByUserId(socialId);
Optional<User> userBySocialId = userQueryAdapter.findUserBySocialId(socialId);

if (userBySocialId.isEmpty())
throw new UserException(GlobalErrorCode.USER_NOT_FOUND);
else {
String accessToken = tokenProvider.createAccessToken(userBySocialId.get(), List.of(new SimpleGrantedAuthority(UserRole.USER.name())));
return ReAuthDto.ReGenerateAccessTokenDto.builder()
.accessToken(accessToken)
.refreshToken(refreshTokenByUserId.get().getRefreshToken())
.build();

}
}

@Transactional
public ReAuthDto.ReGenerateRefreshTokenDto reGenerateRefreshToken(String socialId) {
Optional<User> userBySocialId = userQueryAdapter.findUserBySocialId(socialId);

if (userBySocialId.isEmpty())
throw new UserException(GlobalErrorCode.USER_NOT_FOUND);
else {
userCommandAdapter.deleteRefreshToken(socialId);

String accessToken = tokenProvider.createAccessToken(userBySocialId.get(), List.of(new SimpleGrantedAuthority(UserRole.USER.name())));
return ReAuthDto.ReGenerateRefreshTokenDto
.builder()
.accessToken(accessToken)
.refreshToken(userCommandAdapter.saveRefreshToken(tokenProvider.createRefreshToken(),
socialId).getRefreshToken())
.build();
}
}

private String generateUniqueNickname() {
String name = "";
do {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@ public RefreshToken saveRefreshToken(String refreshToken, String userSocialId){
public User joinUser(JoinDto.JoinRequestDto requestDto, User user){
return user.joinUser(requestDto);
}

public Void deleteRefreshToken(String socialId){
refreshTokenRepository.deleteByUserSocialId(socialId);
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {

Optional<RefreshToken> findByUserSocialId(String userId);

void deleteByUserSocialId(String userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.web.bind.annotation.*;
import rootbox.rootboxApp.api.user.business.UserService;
import rootbox.rootboxApp.api.user.presentation.dto.JoinDto;
import rootbox.rootboxApp.api.user.presentation.dto.ReAuthDto;
import rootbox.rootboxApp.api.user.presentation.dto.SocialLoginDto;
import rootbox.rootboxApp.global.common.CommonResponse;
import rootbox.rootboxApp.global.entity.User;
Expand Down Expand Up @@ -50,6 +51,16 @@ public CommonResponse<JoinDto.JoinNickNameCheckResponseDto> checkNickName(@Reque
JoinDto.JoinNickNameCheckResponseDto.builder().useYn(!userService.checkNickname(nickname)).build());
}

@GetMapping("/accessToken")
public CommonResponse<ReAuthDto.ReGenerateAccessTokenDto> reGenerateAccessToken(@RequestParam(name = "userSocialId") String userSocialId) {
return CommonResponse.onSuccess(userService.reGenerateAccessToken(userSocialId));
}

@GetMapping("/auth/refreshToken")
public CommonResponse<ReAuthDto.ReGenerateRefreshTokenDto> reGenerateRefreshToken(@RequestParam(name = "userSocialId") String userSocialId) {
return CommonResponse.onSuccess(userService.reGenerateRefreshToken(userSocialId));
}

@PatchMapping("/")
public CommonResponse<JoinDto.JoinResponseDto> joinUser(@RequestBody @Valid JoinDto.JoinRequestDto requestDto, @AuthMember @Parameter(hidden = true) User user) {
return CommonResponse.onSuccess(userService.join(requestDto, user));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package rootbox.rootboxApp.api.user.presentation.dto;

import jakarta.validation.constraints.NotNull;
import lombok.*;

public class ReAuthDto {

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ReGenerateAccessTokenDto {

@NotNull
String accessToken;

@NotNull
String refreshToken;
}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class ReGenerateRefreshTokenDto {

@NotNull
String accessToken;

@NotNull
String refreshToken;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package rootbox.rootboxApp.global.common.exception.base;

public class UserException extends GeneralException{

public UserException(BaseErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,28 @@ public OpenAPI SpringCodeBaseAPI() {
final String REFRESH_SCHEME_NAME = "Refresh Token";

Components components = new Components()
// Authorization 헤더용 Access Token (bearer auth)
.addSecuritySchemes(ACCESS_SCHEME_NAME,
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.name("Authorization")
.in(SecurityScheme.In.HEADER))
)
// Refresh 헤더용 Refresh Token (apikey 방식)
.addSecuritySchemes(REFRESH_SCHEME_NAME,
new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.name("Refresh") // 예: 헤더 키를 다르게 설정할 수 있음
.in(SecurityScheme.In.HEADER));
.type(SecurityScheme.Type.APIKEY)
.in(SecurityScheme.In.HEADER)
.name("Refresh")
);

return new OpenAPI()
.info(info)
.components(components)
.addServersItem(new Server().url("/"))
// 여기 두 개 모두 SecurityRequirement에 등록
// 원하는 경우 둘 다 글로벌로 적용 가능. 필요 시 각 API에 개별 지정 가능
.addSecurityItem(new SecurityRequirement().addList(ACCESS_SCHEME_NAME))
.addSecurityItem(new SecurityRequirement().addList(REFRESH_SCHEME_NAME));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class SecurityConfig {
new JwtAuthenticationExceptionHandler();

private static final String[] whiteList = {
"/users/auth/nickname", "/users/auth/kakao/test", "/users/auth/kakao/code", "/users/auth/kakao", "/users/auth/health"
"/users/auth/nickname", "/users/auth/kakao/test", "/users/auth/kakao/code", "/users/auth/kakao", "/users/auth/health", "/users/auth/refreshToken"
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
throws ServletException, IOException {

log.info("jwt 인증 시작, access token 인증 헤더 정보 : {}, refresh 토큰 인증 헤더 정보 : {}", request.getHeader("Authorization"), request.getHeader("Refresh"));
// Enumeration<String> headerNames = request.getHeaderNames();
// while (headerNames.hasMoreElements()) {
// String headerName = headerNames.nextElement();
// String headerValue = request.getHeader(headerName);
// log.info("인증 헤더 모두 null이기 때문에 헤더 정보 다 출력 => {} : {}", headerName, headerValue);
// }

if (request.getHeader("Authorization") == null && request.getHeader("Refresh") == null) {
Enumeration<String> headerNames = request.getHeaderNames();
// Enumeration<String> headerNames = request.getHeaderNames();

// while (headerNames.hasMoreElements()) {
// String headerName = headerNames.nextElement();
Expand All @@ -51,21 +57,31 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse

if (accessToken == null) {
accessToken = tokenProvider.resolveToken(request, "Refresh");
}

if (StringUtils.hasText(accessToken) && tokenProvider.validateRefreshToken(accessToken)){
Authentication authentication = tokenProvider.getRefreshAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}else{
SecurityContextHolder.getContext().setAuthentication(null);
}
// 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행
filterChain.doFilter(request,response);

// 토큰이 있다면 진행
if(StringUtils.hasText(accessToken) && tokenProvider.validateToken(accessToken)) {
}else {

Authentication authentication = tokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 SecurityContext에 설정

// 토큰이 있다면 진행
if (StringUtils.hasText(accessToken) && tokenProvider.validateToken(accessToken)) {

Authentication authentication = tokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증 정보를 SecurityContext에 설정

} else {
SecurityContextHolder.getContext().setAuthentication(null);
}
// 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행
filterChain.doFilter(request, response);
}
else{
SecurityContextHolder.getContext().setAuthentication(null);
}
// 다음 단계 실행 -> 다른 필터 및 컨트롤러 실행
filterChain.doFilter(request,response);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import rootbox.rootboxApp.global.entity.User;

import java.security.Key;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
Expand Down Expand Up @@ -69,6 +70,9 @@ public String resolveToken(HttpServletRequest request, String tokenType) {
}

String token = request.getHeader(headerName);

if (tokenType.equals("Refresh"))
return token;
if (StringUtils.hasText(token) && token.startsWith(BEARER_PREFIX)) {
return token.substring(7);
}
Expand Down Expand Up @@ -117,6 +121,11 @@ public boolean validateToken(String token) {
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
Claims expiredClaims = e.getClaims();
Date exp = expiredClaims.getExpiration();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("access 토큰 만료일자 : {}", sdf.format(exp));

log.info("Expired JWT token, 만료된 JWT token 입니다.");
throw new JwtAuthenticationException(GlobalErrorCode.TOKEN_EXPIRED);

Expand All @@ -131,13 +140,19 @@ public boolean validateToken(String token) {
return false;
}

public void validateRefreshToken(String refreshToken){
public boolean validateRefreshToken(String refreshToken){
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(refreshToken);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
throw new JwtAuthenticationException(GlobalErrorCode.INVALID_TOKEN);
} catch (ExpiredJwtException e) {
Claims expiredClaims = e.getClaims();
Date issuedAt = expiredClaims.getIssuedAt();
Date exp = expiredClaims.getExpiration();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("리프레시 토큰 생성일자 : {}, 리프레시 토큰 만료일자 : {}", sdf.format(issuedAt),sdf.format(exp));
log.info("Expired JWT token, 만료된 JWT 리프레시 token 입니다.");
throw new JwtAuthenticationException(GlobalErrorCode.REFRESH_TOKEN_EXPIRED);
} catch (UnsupportedJwtException e) {
Expand Down Expand Up @@ -175,4 +190,11 @@ public Authentication getAuthentication(String token){
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}

public Authentication getRefreshAuthentication(String token){
Claims claims =
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

return new UsernamePasswordAuthenticationToken(null, token, null);
}

}
4 changes: 2 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ jwt:
key: secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret
# secret : ${JWT_SECRET}
authorities-key: authoritiesKey
access-token-validity-in-seconds: 300000 # 15 days
refresh-token-validity-in-seconds: 5184000000 # 60 days
access-token-validity-in-seconds: 3000 # 15 days
refresh-token-validity-in-seconds: 1200000 # 60 days
oauth:
kakao:
baseUrl: ${KAKAO_BASE_URL}
Expand Down