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: JWT AccessToken & RefreshToken 구현 #149

Merged
merged 2 commits into from
Jan 24, 2024
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 @@ -23,7 +23,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);

// 토큰이 유효하다면
if (token != null && jwtTokenProvider.validateToken(token)) {
if (token != null && jwtTokenProvider.validateAccessToken(token)) {
// 토큰으로부터 유저 정보를 받아
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// SecurityContext 에 객체 저장
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.diareat.diareat.auth.component;

import com.diareat.diareat.util.api.ResponseCode;
import com.diareat.diareat.util.exception.BaseException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
Expand All @@ -16,6 +19,7 @@
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@RequiredArgsConstructor
@Component
Expand All @@ -26,24 +30,46 @@ public class JwtTokenProvider {

private final UserDetailsService userDetailsService;

private final RedisTemplate<String, String> redisTemplate;

// 객체 초기화, secretKey를 Base64로 인코딩
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}

// 토큰 생성
public String createToken(String userPk) {
public String createAccessToken(String userPk) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
Date now = new Date();
return Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + (720 * 60 * 1000L))) // 토큰 유효시각 설정 (12시간)
.setExpiration(new Date(now.getTime() + (60 * 60 * 1000L))) // 토큰 유효시각 설정 (1시간)
.signWith(SignatureAlgorithm.HS256, secretKey) // 암호화 알고리즘과, secret 값
.compact();
}

public String createRefreshToken(String userPk) {
Claims claims = Jwts.claims().setSubject(userPk); // JWT payload 에 저장되는 정보단위
Date now = new Date();
String refreshToken = Jwts.builder()
.setClaims(claims) // 정보 저장
.setIssuedAt(now) // 토큰 발행 시간 정보
.setExpiration(new Date(now.getTime() + (7 * 24 * 60 * 60 * 1000L))) // 토큰 유효시각 설정 (1주일)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();

redisTemplate.opsForValue().set(
userPk,
refreshToken,
7 * 24 * 60 * 60 * 1000L,
TimeUnit.MILLISECONDS
);

return refreshToken;
}

// 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(String.valueOf(this.getUserPk(token)));
Expand All @@ -56,7 +82,7 @@ public Long getUserPk(String token) {
}

// 토큰 유효성, 만료일자 확인
public boolean validateToken(String jwtToken) {
public boolean validateAccessToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date());
Expand All @@ -65,6 +91,13 @@ public boolean validateToken(String jwtToken) {
}
}

public void validateRefreshToken(Long userPK, String refreshToken) {
String redisRefreshToken = redisTemplate.opsForValue().get(String.valueOf(userPK));
if (redisRefreshToken == null || !redisRefreshToken.equals(refreshToken)) {
throw new BaseException(ResponseCode.REFRESH_TOKEN_VALIDATION_FAILURE);
}
}

// Request의 Header에서 token 값 가져오기
public String resolveToken(HttpServletRequest request) {
return request.getHeader("accessToken");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,47 @@ public class AuthController {
@PostMapping("/login")
public ApiResponse<ResponseJwtDto> authCheck(@RequestHeader String accessToken) {
Long userId = kakaoAuthService.isSignedUp(accessToken); // 유저 고유번호 추출
String jwt = (userId == null) ? null : jwtTokenProvider.createToken(userId.toString()); // 고유번호가 null이 아니라면 Jwt 토큰 발급
return ApiResponse.success(ResponseJwtDto.of(userId, jwt), ResponseCode.USER_LOGIN_SUCCESS.getMessage());

ResponseJwtDto responseJwtDto = (userId == null) ? null : ResponseJwtDto.builder()
.accessToken(jwtTokenProvider.createAccessToken(userId.toString()))
.refreshToken(jwtTokenProvider.createRefreshToken(userId.toString()))
.build();

return ApiResponse.success(responseJwtDto, ResponseCode.USER_LOGIN_SUCCESS.getMessage());
}

// 회원가입 (성공 시 Jwt 토큰 발급)
@Operation(summary = "[회원가입] 회원가입 및 토큰 발급", description = "신규 회원가입을 처리하고, 회원가입 성공 시 id와 Jwt 토큰을 발급합니다.")
@PostMapping("/join")
public ApiResponse<ResponseJwtDto> saveUser(@Valid @RequestBody JoinUserDto joinUserDto) {
Long userId = userService.saveUser(kakaoAuthService.createUserDto(joinUserDto));
String jwt = jwtTokenProvider.createToken(userId.toString());
return ApiResponse.success(ResponseJwtDto.of(userId, jwt), ResponseCode.USER_CREATE_SUCCESS.getMessage());

ResponseJwtDto responseJwtDto = (userId == null) ? null : ResponseJwtDto.builder()
.accessToken(jwtTokenProvider.createAccessToken(userId.toString()))
.refreshToken(jwtTokenProvider.createRefreshToken(userId.toString()))
.build();

return ApiResponse.success(responseJwtDto, ResponseCode.USER_CREATE_SUCCESS.getMessage());
}

// 토큰 검증 (Jwt 토큰을 서버에 전송하여, 서버가 유효한 토큰인지 확인하고 True 혹은 예외 반환)
@Operation(summary = "[토큰 검증] 토큰 검증", description = "클라이언트가 가지고 있던 Jwt 토큰을 서버에 전송하여, 서버가 유효한 토큰인지 확인하고 OK 혹은 예외를 반환합니다.")
@GetMapping("/token")
public ApiResponse<Boolean> tokenCheck(@RequestHeader String jwtToken) {
return ApiResponse.success(jwtTokenProvider.validateToken(jwtToken), ResponseCode.TOKEN_CHECK_SUCCESS.getMessage());
public ApiResponse<Boolean> tokenCheck(@RequestHeader String accessToken) {
return ApiResponse.success(jwtTokenProvider.validateAccessToken(accessToken), ResponseCode.TOKEN_CHECK_SUCCESS.getMessage());
}

@Operation(summary = "[토큰 재발급] 토큰 재발급", description = "클라이언트가 가지고 있던 Refresh 토큰을 서버에 전송하여, 서버가 유효한 토큰인지 확인하고 OK 혹은 예외를 반환합니다.")
@PostMapping("/reissue")
public ApiResponse<ResponseJwtDto> reissueToken(@RequestHeader String refreshToken) {
Long userId = jwtTokenProvider.getUserPk(refreshToken);
jwtTokenProvider.validateRefreshToken(userId, refreshToken);

ResponseJwtDto responseJwtDto = (userId == null) ? null : ResponseJwtDto.builder()
.accessToken(jwtTokenProvider.createAccessToken(userId.toString()))
.refreshToken(jwtTokenProvider.createRefreshToken(userId.toString()))
.build();

return ApiResponse.success(responseJwtDto, ResponseCode.TOKEN_REISSUE_SUCCESS.getMessage());
}
}
10 changes: 4 additions & 6 deletions src/main/java/com/diareat/diareat/auth/dto/ResponseJwtDto.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
package com.diareat.diareat.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class ResponseJwtDto {

private Long id;
private String jwt;

public static ResponseJwtDto of(Long id, String jwt) {
return new ResponseJwtDto(id, jwt);
}
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class CustomUserDetailService implements UserDetailsService {

@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {

return userRepository.findById(Long.parseLong(id))
.orElseThrow(() -> new UsernameNotFoundException(ResponseCode.USER_NOT_FOUND.getMessage()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public Long saveUser(CreateUserDto createUserDto) {
throw new UserException(ResponseCode.USER_NAME_ALREADY_EXIST);
}
if (userRepository.existsByKeyCode(createUserDto.getKeyCode())) {
log.info("이미 존재하는 키코드입니다 by {}", createUserDto.getKeyCode());
log.info("이미 존재하는 " +
"con키코드입니다 by {}", createUserDto.getKeyCode());
throw new UserException(ResponseCode.USER_ALREADY_EXIST);
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/diareat/diareat/util/api/ResponseCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum ResponseCode {

// 401 Unauthorized
TOKEN_VALIDATION_FAILURE(HttpStatus.UNAUTHORIZED, false, "토큰 검증 실패"),
REFRESH_TOKEN_VALIDATION_FAILURE(HttpStatus.UNAUTHORIZED, false, "Refresh 토큰 검증 실패"),

// 403 Forbidden
FORBIDDEN(HttpStatus.FORBIDDEN, false, "권한이 없습니다."),
Expand Down Expand Up @@ -56,6 +57,7 @@ public enum ResponseCode {
FOOD_RANK_READ_SUCCESS(HttpStatus.OK, true, "식습관 점수 기반 랭킹 조회 성공"),

TOKEN_CHECK_SUCCESS(HttpStatus.OK, true, "토큰 검증 완료"),
TOKEN_REISSUE_SUCCESS(HttpStatus.OK, true, "토큰 재발급 완료"),


// 201 Created
Expand Down
Loading