-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
24 changed files
with
748 additions
and
639 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
src/main/java/com/Alchive/backend/config/jwt/JwtAuthenticationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package com.Alchive.backend.config.jwt; | ||
|
||
import com.Alchive.backend.config.error.ErrorCode; | ||
import com.Alchive.backend.config.error.ErrorResponse; | ||
import com.Alchive.backend.config.error.exception.BusinessException; | ||
import com.Alchive.backend.config.error.exception.token.TokenNotExistsException; | ||
import com.Alchive.backend.domain.user.User; | ||
import com.Alchive.backend.service.UserService; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.util.AntPathMatcher; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
|
||
import java.io.IOException; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtAuthenticationFilter extends OncePerRequestFilter { | ||
private final JwtTokenProvider jwtTokenProvider; | ||
private final UserService userService; | ||
private final AntPathMatcher pathMatcher = new AntPathMatcher(); | ||
|
||
|
||
// 인증에서 제외할 URL과 메서드 | ||
private static final Map<String, List<String>> EXCLUDE_URL = Map.ofEntries( | ||
Map.entry("/api/v2", List.of("GET")), | ||
Map.entry("/api/v2/api-docs/**", List.of("GET")), | ||
Map.entry("/api/swagger-ui/**", List.of("GET")), | ||
Map.entry("/api/v2/boards", List.of("GET")), | ||
Map.entry("/api/v2/boards/{boardId}", List.of("GET")), | ||
Map.entry("/api/v2/users", List.of("GET", "POST")), | ||
Map.entry("/api/v2/users/{userId}", List.of("GET")), | ||
Map.entry("/api/v2/users/username/{name}", List.of("GET")), | ||
Map.entry("/api/v2/sns/{snsId}", List.of("GET")), | ||
Map.entry("/api/v2/slack/reminder", List.of("GET")), | ||
Map.entry("/api/v2/slack/added", List.of("GET")) | ||
); | ||
|
||
// EXCLUDE_URL과 메서드에 일치할 경우 현재 필터를 진행하지 않고 다음 필터 진행 | ||
@Override | ||
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { | ||
String path = request.getServletPath(); | ||
String method = request.getMethod(); | ||
return EXCLUDE_URL.entrySet().stream() | ||
.anyMatch(entry -> pathMatches(entry.getKey(), path) && entry.getValue().contains(method)); | ||
} | ||
|
||
// 경로 패턴을 비교하는 유틸리티 메서드 (단순히 String 비교를 넘어 패턴 매칭이 필요할 경우 활용) | ||
private boolean pathMatches(String pattern, String path) { | ||
return pathMatcher.match(pattern, path); | ||
} | ||
|
||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { | ||
try { | ||
// 액세스 토큰 추출 및 검증 | ||
String accessToken = jwtTokenProvider.resolveAccessToken(request); | ||
if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) { | ||
authenticateWithToken(accessToken); | ||
} | ||
// 액세스 토큰이 없거나 만료된 경우 리프레시 토큰 확인 | ||
else { | ||
// 리프레시 토큰 추출 및 검증 | ||
String refreshToken = jwtTokenProvider.resolveRefreshToken(request); | ||
if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) { | ||
String email = jwtTokenProvider.getEmailFromToken(refreshToken); | ||
// 새로운 액세스, 리프레시 토큰 발급 | ||
String newAccessToken = jwtTokenProvider.createAccessToken(email); | ||
String newRefreshToken = jwtTokenProvider.createRefreshToken(email); | ||
response.setHeader("Authorization", "Bearer " + newAccessToken); | ||
response.setHeader("Refresh-Token", newRefreshToken); | ||
// 새로 발급된 액세스 토큰으로 인증 처리 | ||
authenticateWithToken(newAccessToken); | ||
} else { | ||
// 토큰이 없는 경우 | ||
throw new TokenNotExistsException(); | ||
} | ||
} | ||
|
||
filterChain.doFilter(request, response); | ||
} catch (BusinessException e) { | ||
handleException(response, e); | ||
} | ||
} | ||
|
||
private void authenticateWithToken(String token) { | ||
// 토큰에서 이메일 추출 후 이메일로 사용자 조회 | ||
String email = jwtTokenProvider.getEmailFromToken(token); | ||
User user = userService.findByEmail(email); | ||
// 인증 객체 생성 | ||
UsernamePasswordAuthenticationToken authentication = | ||
new UsernamePasswordAuthenticationToken(user, null); | ||
// SecurityContext에 인증 정보 설정 | ||
SecurityContextHolder.getContext().setAuthentication(authentication); | ||
} | ||
|
||
// 토큰 검증 중 토큰이 없거나 유효하지 않은 경우 예외 처리 | ||
private void handleException(HttpServletResponse response, BusinessException exception) throws IOException { | ||
ErrorCode errorCode = exception.getErrorCode(); | ||
ErrorResponse errorResponse = ErrorResponse.builder() | ||
.code(String.valueOf(errorCode.getCode())) | ||
.message(errorCode.getMessage()) | ||
.build(); | ||
response.setStatus(errorCode.getHttpStatus()); | ||
response.setContentType("application/json; charset=UTF-8"); // 한글을 위해 UTF-8 인코딩 설정 | ||
|
||
// ErrorResponse를 JSON 형식으로 변환하여 응답 | ||
ObjectMapper objectMapper = new ObjectMapper(); | ||
String jsonResponse = objectMapper.writeValueAsString(errorResponse); | ||
|
||
response.getWriter().write(jsonResponse); | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/com/Alchive/backend/config/jwt/JwtController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.Alchive.backend.config.jwt; | ||
|
||
import com.Alchive.backend.config.result.ResultResponse; | ||
import io.swagger.v3.oas.annotations.Operation; | ||
import io.swagger.v3.oas.annotations.tags.Tag; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import static com.Alchive.backend.config.result.ResultCode.TOKEN_ACCESS_SUCCESS; | ||
|
||
@Tag(name = "JWT", description = "[Test] JWT 관련 api입니다.") | ||
@RequiredArgsConstructor | ||
@RestController | ||
@RequestMapping("/api/v2/jwt") // 공통 url | ||
public class JwtController { | ||
private final JwtTokenProvider jwtTokenProvider; | ||
|
||
@Operation(summary = "토큰 재발급 메서드", description = "액세스 토큰을 재발급하는 메서드입니다.") | ||
@GetMapping("") | ||
public ResponseEntity<ResultResponse> createToken(String email) { | ||
String accessToken = jwtTokenProvider.createAccessToken(email); | ||
return ResponseEntity.ok(ResultResponse.of(TOKEN_ACCESS_SUCCESS, accessToken)); | ||
} | ||
} |
95 changes: 95 additions & 0 deletions
95
src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package com.Alchive.backend.config.jwt; | ||
|
||
import com.Alchive.backend.config.error.exception.token.TokenExpiredException; | ||
import io.jsonwebtoken.Claims; | ||
import io.jsonwebtoken.Jwts; | ||
import io.jsonwebtoken.SignatureAlgorithm; | ||
import io.jsonwebtoken.io.Decoders; | ||
import io.jsonwebtoken.security.Keys; | ||
import jakarta.annotation.PostConstruct; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.security.Key; | ||
import java.util.Date; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class JwtTokenProvider { | ||
// JWT 토큰 생성 | ||
private Key secretKey; | ||
@Value("${jwt.token.secret-key}") | ||
private String SECRET_KEY; | ||
@Value("${jwt.token.access-expire-length}") | ||
private Long ACCESS_EXPIRE_LENGTH; | ||
@Value("${jwt.token.refresh-expire-length}") | ||
private Long REFRESH_EXPIRE_LENGTH; | ||
|
||
@PostConstruct | ||
protected void init() { | ||
secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY)); | ||
} | ||
|
||
// 액세스 및 리프레시 토큰 생성 | ||
public String createAccessToken(String email) { | ||
return createToken(email, ACCESS_EXPIRE_LENGTH); | ||
} | ||
|
||
public String createRefreshToken(String email) { | ||
return createToken(email, REFRESH_EXPIRE_LENGTH); | ||
} | ||
|
||
private String createToken(String email, Long expireLength) { | ||
Claims claims = Jwts.claims().setSubject(email); | ||
return Jwts.builder().setClaims(claims) | ||
.setIssuedAt(new Date(System.currentTimeMillis())) | ||
.setExpiration(new Date(System.currentTimeMillis() + expireLength)) | ||
.signWith(secretKey, SignatureAlgorithm.HS256) | ||
.compact(); | ||
} | ||
|
||
// 액세스 토큰 추출 | ||
public String resolveAccessToken(HttpServletRequest request) { | ||
return resolveToken(request, "Authorization", "Bearer "); | ||
} | ||
|
||
// 리프레시 토큰 추출 | ||
public String resolveRefreshToken(HttpServletRequest request) { | ||
return resolveToken(request, "Refresh-Token", ""); | ||
} | ||
|
||
// HTTP 요청 헤더에서 토큰 추출 | ||
private String resolveToken(HttpServletRequest request, String headerName, String prefix) { | ||
try { | ||
String header = request.getHeader(headerName); | ||
return prefix.isEmpty() ? header : header.substring(prefix.length()); | ||
} catch (NullPointerException | IllegalArgumentException e) { | ||
return null; | ||
} | ||
} | ||
|
||
// 토큰 검증 | ||
public boolean validateToken(String token) { | ||
try { | ||
Jwts.parserBuilder() | ||
.setSigningKey(secretKey) | ||
.build() | ||
.parseClaimsJws(token); | ||
return true; | ||
} catch (Exception e) { | ||
throw new TokenExpiredException(); | ||
} | ||
} | ||
|
||
// 이메일 추출 | ||
public String getEmailFromToken(String token) { | ||
Claims claims = Jwts.parserBuilder() | ||
.setSigningKey(secretKey) | ||
.build() | ||
.parseClaimsJws(token) | ||
.getBody(); | ||
return claims.getSubject(); | ||
} | ||
} |
Oops, something went wrong.