Skip to content

Commit

Permalink
Merge pull request #93 from TRIP-Side-Project/feature/#89-logout
Browse files Browse the repository at this point in the history
refreshToken도입 + 로그아웃 기능 구현
  • Loading branch information
gkfktkrh153 authored Dec 19, 2023
2 parents 6672678 + 8563a77 commit bb04387
Show file tree
Hide file tree
Showing 8 changed files with 277 additions and 32 deletions.
14 changes: 14 additions & 0 deletions src/main/java/com/api/trip/common/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@
@Getter
public enum ErrorCode {

// 400
EMPTY_REFRESH_TOKEN("RefreshToken이 필요합니다.", HttpStatus.BAD_REQUEST),

// 401
LOGOUTED_TOKEN("이미 로그아웃 처리된 토큰입니다.", HttpStatus.UNAUTHORIZED),
SNATCH_TOKEN("Refresh Token 탈취를 감지하여 로그아웃 처리됩니다.", HttpStatus.UNAUTHORIZED),
INVALID_TYPE_TOKEN("Token의 타입은 Bearer입니다.", HttpStatus.UNAUTHORIZED),
EXPIRED_PERIOD_ACCESS_TOKEN("기한이 만료된 AccessToken입니다.", HttpStatus.UNAUTHORIZED),
EXPIRED_PERIOD_REFRESH_TOKEN("기한이 만료된 RefreshToken입니다.", HttpStatus.UNAUTHORIZED),
EMPTY_AUTHORITY("권한 정보가 필요합니다.", HttpStatus.UNAUTHORIZED),
INVALID_ACCESS_TOKEN("유효하지 않은 AccessToken입니다.", HttpStatus.UNAUTHORIZED),
INVALID_REFRESH_TOKEN("유효하지 않은 RefreshToken입니다.", HttpStatus.UNAUTHORIZED),

// 404
NOT_FOUND_MEMBER("회원이 존재하지 않습니다.", HttpStatus.NOT_FOUND);

private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.api.trip.common.exception.custom_exception;

import com.api.trip.common.exception.CustomException;
import com.api.trip.common.exception.ErrorCode;

public class BadRequestException extends CustomException {
public BadRequestException(ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.api.trip.common.exception.custom_exception;

import com.api.trip.common.exception.CustomException;
import com.api.trip.common.exception.ErrorCode;

public class InvalidException extends CustomException {
public InvalidException(ErrorCode errorCode) {
super(errorCode);
}
}
18 changes: 10 additions & 8 deletions src/main/java/com/api/trip/common/security/jwt/JwtTokenFilter.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.api.trip.common.security.jwt;

import com.api.trip.common.security.util.JwtTokenUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -21,19 +22,20 @@ public class JwtTokenFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("accessToken");

if (header == null || !header.startsWith("Bearer ")) {
log.error("Error occurs while getting header. header is null or invalid {}", request.getRequestURL());
filterChain.doFilter(request, response);
return;
}

String accessToken = header.split(" ")[1].trim();
String accessToken = JwtTokenUtils.extractBearerToken(request.getHeader("accessToken"));



if (jwtTokenProvider.validateAccessToken(accessToken)) {
if (!request.getRequestURI().equals("/api/members/rotate") && accessToken != null) { // 토큰 재발급의 요청이 아니면서 accessToken이 존재할 때

// 토큰이 유효한 경우 and 로그인 상태
Authentication authentication = jwtTokenProvider.getAuthenticationByAccessToken(accessToken);
jwtTokenProvider.checkLogin(authentication.getName());

SecurityContextHolder.getContext().setAuthentication(authentication);

}

filterChain.doFilter(request, response);
Expand Down
134 changes: 110 additions & 24 deletions src/main/java/com/api/trip/common/security/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.api.trip.common.security.jwt;


import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import com.api.trip.common.exception.ErrorCode;
import com.api.trip.common.exception.custom_exception.InvalidException;
import com.api.trip.common.security.util.JwtTokenUtils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
Expand All @@ -30,52 +30,100 @@
@Slf4j
public class JwtTokenProvider {

private final JwtTokenUtils jwtTokenUtils;

@Value("${custom.jwt.token.access-expiration-time}")
private long accessExpirationTime;

@Value("${custom.jwt.token.refresh-expiration-time}")
private long refreshExpirationTime;

private final Key key;

@Autowired
public JwtTokenProvider(@Value("${custom.jwt.token.secret}") String secretKey) {
public JwtTokenProvider(@Value("${custom.jwt.token.secret}") String secretKey, JwtTokenUtils jwtTokenUtils) {
this.jwtTokenUtils = jwtTokenUtils;
byte[] keyBytes = Decoders.BASE64.decode(secretKey);
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public JwtToken createJwtToken(String email, String authorities) {

Date now = new Date();
Claims claims = Jwts.claims().setSubject(email);
claims.put("roles", authorities);

String accessToken = Jwts.builder()

String accessToken = createAccessToken(claims, new Date(now.getTime() + accessExpirationTime));
String refreshToken = createRefreshToken(claims, new Date(now.getTime() + refreshExpirationTime));


return new JwtToken(accessToken, refreshToken);
}
public JwtToken refreshJwtToken(Authentication authentication){
String authorities = authentication.getAuthorities().stream()
.map(a -> a.getAuthority())
.collect(Collectors.joining(","));

JwtToken jwtToken = createJwtToken(authentication.getName(), authorities);

jwtTokenUtils.updateRefreshToken(authentication.getName(), jwtToken.getRefreshToken());
return jwtToken;
}


private String createAccessToken(Claims claims, Date expiredDate) {
return Jwts.builder()
.setClaims(claims) // 아이디, 권한정보
.setIssuedAt(new Date(System.currentTimeMillis())) // 생성일 설정
.setExpiration(expiredDate) // 만료일 설정
.signWith(SignatureAlgorithm.HS256, key)
.compact();
}

private String createRefreshToken(Claims claims, Date expiredDate) {
String refreshToken = Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + accessExpirationTime))
.setExpiration(expiredDate)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
jwtTokenUtils.setRefreshToken(claims.getSubject(), refreshToken);

return new JwtToken(accessToken, "refreshToken");
return refreshToken;
}

public boolean validateAccessToken(String accessToken) {
try {
parseToken(accessToken);
return true;
} catch (final JwtException | IllegalArgumentException exception) {
return false;
/**
* @Description
* AccessToken 검증 + Return 인증객체
*/
public Authentication getAuthenticationByAccessToken(String accessToken) {

Claims claims = validateAccessToken(accessToken);

if (claims.get("roles") == null){
throw new InvalidException(ErrorCode.EMPTY_AUTHORITY);
}
}

private Claims parseToken(final String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("roles").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());

public Authentication getAuthenticationByAccessToken(String accessToken) {
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}

Claims claims = parseToken(accessToken);
/**
* @Description
* RefreshToken 검증 + Return 인증객체
*/
public Authentication getAuthenticationByRefreshToken(String refreshToken){
Claims claims = validateRefreshToken(refreshToken);

if (claims.get("roles") == null){
throw new InvalidException(ErrorCode.EMPTY_AUTHORITY);
}
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("roles").toString().split(","))
.map(SimpleGrantedAuthority::new)
Expand All @@ -84,4 +132,42 @@ public Authentication getAuthenticationByAccessToken(String accessToken) {
UserDetails principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}


/**
* @Description
* 토큰의 만료여부와 유효성에 대해 검증합니다.
*/
private Claims validateAccessToken(String accessToken) {
try {
return parseToken(accessToken);

} catch (ExpiredJwtException e) {
throw new InvalidException(ErrorCode.EXPIRED_PERIOD_ACCESS_TOKEN);
} catch (final JwtException | IllegalArgumentException e) {
throw new InvalidException(ErrorCode.INVALID_ACCESS_TOKEN);
}
}
private Claims validateRefreshToken(String refreshToken) {
try {
return parseToken(refreshToken);
} catch (ExpiredJwtException e) {
throw new InvalidException(ErrorCode.EXPIRED_PERIOD_REFRESH_TOKEN);
} catch (final JwtException | IllegalArgumentException e) {
throw new InvalidException(ErrorCode.INVALID_REFRESH_TOKEN);
}
}

private Claims parseToken(final String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
}

public void checkLogin(String email) {
if (jwtTokenUtils.isLogin(email) == false)
throw new InvalidException(ErrorCode.LOGOUTED_TOKEN);
}
}
53 changes: 53 additions & 0 deletions src/main/java/com/api/trip/common/security/util/JwtTokenUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.api.trip.common.security.util;



import com.api.trip.common.exception.ErrorCode;
import com.api.trip.common.exception.custom_exception.BadRequestException;
import com.api.trip.common.redis.RedisService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
@RequiredArgsConstructor
public class JwtTokenUtils {

private final RedisService redisService;

@Value("${custom.jwt.token.refresh-expiration-time}")
private long refreshExpirationTime;
/**
* @Description
* Bearer토큰의 여부에 대해 검증한 뒤 토큰을 반환합니다.
*/
public static String extractBearerToken(String token) {
if(token != null){
if(!token.startsWith("Bearer"))
throw new BadRequestException(ErrorCode.INVALID_TYPE_TOKEN);
return token.split(" ")[1].trim();
}
return null;
}

public boolean isLogin(String email){
return redisService.getData("Login_" + email) != null;
}
public void setRefreshToken(String username, String refreshToken){
redisService.setData("Login_" + username, refreshToken, refreshExpirationTime, TimeUnit.SECONDS);
}
public void updateRefreshToken(String name, String refreshToken) {
setRefreshToken(name, refreshToken);
}

public void deleteRefreshToken(String name) {
redisService.deleteData("Login_" + name);
}

public String getRefreshToken(String name) {
return redisService.getData("Login_" + name).toString();
}

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.api.trip.domain.member.controller;

import com.api.trip.common.exception.ErrorCode;
import com.api.trip.common.exception.custom_exception.BadRequestException;
import com.api.trip.common.security.jwt.JwtToken;
import com.api.trip.common.security.util.JwtTokenUtils;
import com.api.trip.domain.email.service.EmailService;
import com.api.trip.domain.member.controller.dto.*;
import com.api.trip.domain.member.service.MemberService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

Expand Down Expand Up @@ -75,4 +82,22 @@ public ResponseEntity<Void> deleteMember(@RequestBody DeleteRequest deleteReques
return ResponseEntity.ok().build();
}

@PreAuthorize("isAuthenticated()")
@Operation(summary = "로그아웃")
@PostMapping("/logout")
public ResponseEntity<String> logoutMember() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return ResponseEntity.ok().body(memberService.logout(username));
}

@GetMapping("/rotate")
public JwtToken rotateToken(HttpServletRequest request){
String refreshToken = JwtTokenUtils.extractBearerToken(request.getHeader("refreshToken"));

if(refreshToken.isBlank())
throw new BadRequestException(ErrorCode.EMPTY_REFRESH_TOKEN);


return memberService.rotateToken(refreshToken);
}
}
Loading

0 comments on commit bb04387

Please sign in to comment.