Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.runimo.runimo.auth.filters;

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.runimo.runimo.auth.jwt.JwtResolver;
import org.runimo.runimo.common.response.ErrorResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.Collections;

import static org.runimo.runimo.common.GlobalConsts.WHITE_LIST_ENDPOINTS;

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String AUTH_HEADER = "Authorization";
private static final String AUTH_PREFIX = "Bearer ";
private static final int TOKEN_PREFIX_LENGTH = 7;
private final JwtResolver jwtResolver;
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws IOException {
try {
if (!hasValidAuthorizationHeader(request)) {
setErrorResponse(UserErrorCode.JWT_NOT_FOUND, response);
return;
}
String jwtToken = extractToken(request);
if (!processToken(jwtToken, response)) {
return;
}
filterChain.doFilter(request, response);
} catch (Exception e) {
log.warn("[ERROR]JWT broken : {}", e.getMessage());
setErrorResponse(UserErrorCode.JWT_BROKEN, response);
} finally {
SecurityContextHolder.clearContext();
}
}

private boolean hasValidAuthorizationHeader(HttpServletRequest request) {
String authHeader = request.getHeader(AUTH_HEADER);
return authHeader != null && authHeader.startsWith(AUTH_PREFIX);
}

private String extractToken(HttpServletRequest request) {
return request.getHeader(AUTH_HEADER).substring(TOKEN_PREFIX_LENGTH);
}

private boolean processToken(String jwtToken, HttpServletResponse response) throws IOException {
try {
String userId = jwtResolver.getUserIdFromAccessToken(jwtToken);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userId,
null,
Collections.emptyList()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
return true;
} catch (TokenExpiredException e) {
setErrorResponse(UserErrorCode.JWT_EXPIRED, response);
} catch (Exception e) { // JWT 손상 시 오류
log.warn("[ERROR]JWT broken : {}", e.getMessage());
setErrorResponse(UserErrorCode.JWT_BROKEN, response);
}
return false;
}

private void setErrorResponse(UserErrorCode errorCode, HttpServletResponse response) throws IOException {
response.setStatus(errorCode.getHttpStatusCode().value());
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
ErrorResponse errorResponse = ErrorResponse.of(errorCode);
response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
}

// JWT 필터는 로그인, 회원가입, 테스트용 API, 웹소켓 연결 요청에 대해서는 필터링을 하지 않는다.
@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
return WHITE_LIST_ENDPOINTS.stream().anyMatch(endpoint -> request.getRequestURI().startsWith(endpoint));
}
}
55 changes: 55 additions & 0 deletions src/main/java/org/runimo/runimo/auth/filters/UserErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package org.runimo.runimo.auth.filters;

import org.runimo.runimo.exceptions.code.CustomResponseCode;
import org.springframework.http.HttpStatus;

public enum UserErrorCode implements CustomResponseCode {

// 400
USER_ALREADY_EXISTS("UEH4001", HttpStatus.BAD_REQUEST, "이미 존재하는 사용자", "이미 존재하는 사용자"),
SIGNUP_FAILED("UEH4002", HttpStatus.BAD_REQUEST, "회원가입 실패", "회원가입 실패"),

// 401
LOGIN_FAILED_BY_PROVIDER("UEH4011", HttpStatus.FORBIDDEN, "소셜 로그인 실패", "소셜 로그인 실패"),
JWT_NOT_FOUND("UEH4012", HttpStatus.UNAUTHORIZED, "JWT 토큰이 존재하지 않습니다.", "JWT 토큰이 존재하지 않습니다."),
JWT_EXPIRED("UEH4013", HttpStatus.UNAUTHORIZED, "JWT 토큰이 만료되었습니다.", "JWT 토큰이 만료되었습니다."),
JWT_BROKEN("UEH4014", HttpStatus.UNAUTHORIZED, "JWT 토큰이 손상되었습니다", "JWT 토큰이 손상되었습니다"),
REFRESH_FAILED("UEH4015", HttpStatus.UNAUTHORIZED, "리프레시 토큰이 만료되었습니다.", "리프레시 토큰이 만료되었습니다."),

// 404
USER_NOT_FOUND("UEH4041", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없음", "사용자를 찾을 수 없음"),
;

private final String code;
private final HttpStatus httpStatusCode;
private final String clientMessage;
private final String logMessage;

UserErrorCode(String code, HttpStatus httpStatusCode, String clientMessage, String logMessage) {
this.code = code;
this.httpStatusCode = httpStatusCode;
this.clientMessage = clientMessage;
this.logMessage = logMessage;
}

@Override
public String getCode() {
return this.code;
}

@Override
public String getClientMessage() {
return this.clientMessage;
}

@Override
public String getLogMessage() {
return this.logMessage;
}

@Override
public HttpStatus getHttpStatusCode() {
return this.httpStatusCode;
}
}

29 changes: 29 additions & 0 deletions src/main/java/org/runimo/runimo/auth/jwt/JwtResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.runimo.runimo.auth.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class JwtResolver {

private static final String ISSUER = "RunUSAuthService";
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
@Value("${jwt.refresh.expiration}")
private long jwtRefreshExpiration;

public DecodedJWT verifyAccessToken(String token) throws JWTVerificationException {
return JWT.require(Algorithm.HMAC256(jwtSecret)).withIssuer(ISSUER).build().verify(token);
}

public String getUserIdFromAccessToken(String token) throws JWTVerificationException {
DecodedJWT jwt = verifyAccessToken(token);
return jwt.getSubject();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import lombok.RequiredArgsConstructor;
import org.runimo.runimo.auth.jwt.TokenStatus;
import org.runimo.runimo.common.CacheEntry;
import org.runimo.runimo.common.InMemoryCache;
import org.runimo.runimo.common.cache.CacheEntry;
import org.runimo.runimo.common.cache.InMemoryCache;
import org.runimo.runimo.user.domain.SocialProvider;
import org.springframework.stereotype.Component;

Expand Down
7 changes: 6 additions & 1 deletion src/main/java/org/runimo/runimo/common/BaseEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
@MappedSuperclass
public abstract class BaseEntity {
public abstract class BaseEntity implements Serializable {

@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/runimo/runimo/common/GlobalConsts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.runimo.runimo.common;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.time.ZoneId;
import java.util.Set;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class GlobalConsts {

public static final String TIME_ZONE_ID = "Asia/Seoul";
public static final ZoneId ZONE_ID = ZoneId.of(TIME_ZONE_ID);
public static final String DEFAULT_IMG_URL = "default_img_url";
public static final String SESSION_ATTRIBUTE_USER = "user-info";
public static final Set<String> WHITE_LIST_ENDPOINTS = Set.of(
"/test/auth",
"/auth",
"/swagger-ui",
"/v3/api-docs"
);

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.common;
package org.runimo.runimo.common.cache;

import org.springframework.lang.Nullable;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.common;
package org.runimo.runimo.common.cache;

import java.time.Duration;
import java.util.Optional;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.common;
package org.runimo.runimo.common.cache;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/runimo/runimo/common/response/ErrorResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.runimo.runimo.common.response;

import org.runimo.runimo.exceptions.code.CustomResponseCode;

public class ErrorResponse extends Response {

private ErrorResponse(final String errorMessage, final String errorCode) {
super(false, errorMessage, errorCode);
}

private ErrorResponse(final CustomResponseCode errorCode) {
super(false, errorCode);
}

public static ErrorResponse of(final String errorMessage, final String errorCode) {
return new ErrorResponse(errorMessage, errorCode);
}

public static ErrorResponse of(final CustomResponseCode errorCode) {
return new ErrorResponse(errorCode);
}
}

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.common;
package org.runimo.runimo.common.response;

import lombok.Getter;
import org.runimo.runimo.exceptions.code.CustomResponseCode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.runimo.runimo.common;
package org.runimo.runimo.common.response;

import lombok.Getter;
import org.runimo.runimo.exceptions.code.CustomResponseCode;
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/runimo/runimo/common/scale/Distance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.runimo.runimo.common.scale;

import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.io.Serial;
import java.io.Serializable;

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Distance implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long amount;

public Distance(Long amount) {
this.amount = amount;
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/runimo/runimo/common/scale/Pace.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.runimo.runimo.common.scale;

import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.io.Serial;
import java.io.Serializable;

@Embeddable
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Pace implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long paceInMilliSeconds;

public Pace(Long paceInMilliSeconds) {
this.paceInMilliSeconds = paceInMilliSeconds;
}
}
4 changes: 2 additions & 2 deletions src/main/java/org/runimo/runimo/config/CacheConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.sun.security.auth.UserPrincipal;
import org.runimo.runimo.auth.jwt.TokenStatus;
import org.runimo.runimo.common.InMemoryCache;
import org.runimo.runimo.common.SpringInMemoryCache;
import org.runimo.runimo.common.cache.InMemoryCache;
import org.runimo.runimo.common.cache.SpringInMemoryCache;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand Down
Loading