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
1 change: 1 addition & 0 deletions application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {

// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'org.springframework.boot:spring-boot-starter-validation'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package com.yapp.ndgl.application.common.logging;

import java.io.IOException;
import java.util.UUID;
import java.util.List;
import java.util.UUID;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@Order(20)
Expand All @@ -38,12 +42,12 @@ public class MdcFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain
) throws ServletException, IOException {
String requestId = resolveRequestId(request);
String userId = resolveUserId(request);
String userId = resolveUserId();
String clientIp = resolveClientIp(request);

MDC.put(REQUEST_ID_KEY, requestId);
Expand Down Expand Up @@ -71,12 +75,13 @@ private String resolveRequestId(HttpServletRequest request) {
return UUID.randomUUID().toString();
}

private String resolveUserId(HttpServletRequest request) {
Object attribute = request.getAttribute(USER_ID_ATTRIBUTE);
if (attribute instanceof String userId && StringUtils.hasText(userId)) {
return userId;
private String resolveUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
return "anonymous";
}
return "anonymous";

return authentication.getPrincipal().toString();
}

private String resolveClientIp(HttpServletRequest request) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.yapp.ndgl.application.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yapp.ndgl.common.exception.CommonErrorCode;
import com.yapp.ndgl.common.response.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

private final ObjectMapper objectMapper;

@Override
public void commence(
final HttpServletRequest request,
final HttpServletResponse response,
final AuthenticationException authException) throws IOException {

CommonErrorCode errorCode = CommonErrorCode.UNAUTHORIZED;
response.setStatus(errorCode.getStatusCode().getCode());
response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
objectMapper.writeValue(response.getWriter(), ErrorResponse.error(errorCode));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.yapp.ndgl.application.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yapp.ndgl.application.domains.auth.component.JwtTokenProvider;
import com.yapp.ndgl.application.domains.auth.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private static final String[] PUBLIC_PATHS = {
"/api/v1/auth/**", // 기존 v1 인증 (하위 호환)
"/api/v2/auth/social/**", // v2 소셜 로그인
"/api/v2/auth/reissue", // v2 토큰 재발급
"/swagger",
"/swagger-ui/**",
"/api-docs/**",
"/v3/api-docs/**",
"/admin/**", // AdminAuthInterceptor가 별도 토큰 검증
"/actuator/**"
};

private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtTokenProvider, objectMapper);
}

@Bean
public FilterRegistrationBean<JwtAuthenticationFilter> jwtFilterRegistration(
final JwtAuthenticationFilter jwtAuthenticationFilter) {
FilterRegistrationBean<JwtAuthenticationFilter> registration =
new FilterRegistrationBean<>(jwtAuthenticationFilter);
registration.setEnabled(false);
return registration;
}

@Bean
public SecurityFilterChain securityFilterChain(
final HttpSecurity http,
final JwtAuthenticationFilter jwtAuthenticationFilter) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_PATHS).permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(ex ->
ex.authenticationEntryPoint(customAuthenticationEntryPoint)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public JwtTokenProvider(
this.expiration = expiration;
}

public String generateToken(String uuid) {
public String generateToken(final String uuid) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration);

Expand All @@ -35,25 +35,11 @@ public String generateToken(String uuid) {
.compact();
}

public String getUuidFromToken(String token) {
Claims claims = Jwts.parser()
public Claims parseClaims(final String token) {
return Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token)
.getPayload();

return claims.getSubject();
}

public boolean validateToken(String token) {
try {
Jwts.parser()
.verifyWith(secretKey)
.build()
.parseSignedClaims(token);
return true;
} catch (Exception e) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
import com.yapp.ndgl.application.domains.auth.annotation.CurrentUuid;
import com.yapp.ndgl.common.exception.CommonErrorCode;
import com.yapp.ndgl.common.exception.GlobalException;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
Expand All @@ -14,28 +16,26 @@
@Component
public class CurrentUuidArgumentResolver implements HandlerMethodArgumentResolver {

private static final String UUID_ATTRIBUTE = "uuid";
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUuid.class) &&
parameter.getParameterType().equals(String.class);
}

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUuid.class) &&
parameter.getParameterType().equals(String.class);
}
@Override
public Object resolveArgument(
final MethodParameter parameter,
final ModelAndViewContainer mavContainer,
final NativeWebRequest webRequest,
final WebDataBinderFactory binderFactory) {

@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
String uuid = (String) request.getAttribute(UUID_ATTRIBUTE);
if (uuid == null) {
throw new GlobalException(CommonErrorCode.UNAUTHORIZED);
}
return uuid;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
throw new GlobalException(CommonErrorCode.UNAUTHORIZED);
}
if (!(authentication.getPrincipal() instanceof String uuid)) {
throw new GlobalException(CommonErrorCode.UNAUTHORIZED);
}
return uuid;
}
throw new GlobalException(CommonErrorCode.UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -1,50 +1,85 @@
package com.yapp.ndgl.application.domains.auth.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yapp.ndgl.application.domains.auth.component.JwtTokenProvider;
import com.yapp.ndgl.common.exception.CommonErrorCode;
import com.yapp.ndgl.common.response.ErrorResponse;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

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

@Component
@Order(10)
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
private static final String UUID_ATTRIBUTE = "uuid";
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";

private final JwtTokenProvider jwtTokenProvider;
private final JwtTokenProvider jwtTokenProvider;
private final ObjectMapper objectMapper;

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
@Override
protected void doFilterInternal(
final HttpServletRequest request,
final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {

String token = resolveToken(request);
String token = resolveToken(request);

if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
String uuid = jwtTokenProvider.getUuidFromToken(token);
request.setAttribute(UUID_ATTRIBUTE, uuid);
if (StringUtils.hasText(token)) {
try {
Claims claims = jwtTokenProvider.parseClaims(token);
String uuid = claims.getSubject();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(uuid, null, Collections.emptyList());
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (ExpiredJwtException e) {
sendErrorResponse(response, CommonErrorCode.EXPIRED_TOKEN);
return;
} catch (SignatureException e) {
sendErrorResponse(response, CommonErrorCode.INVALID_TOKEN_SIGNATURE);
return;
} catch (MalformedJwtException e) {
sendErrorResponse(response, CommonErrorCode.MALFORMED_TOKEN);
return;
} catch (UnsupportedJwtException e) {
sendErrorResponse(response, CommonErrorCode.UNSUPPORTED_TOKEN);
return;
} catch (IllegalArgumentException e) {
sendErrorResponse(response, CommonErrorCode.INVALID_TOKEN);
return;
}
}

filterChain.doFilter(request, response);
}

filterChain.doFilter(request, response);
}
private void sendErrorResponse(final HttpServletResponse response, final CommonErrorCode errorCode) throws IOException {
SecurityContextHolder.clearContext();
response.setStatus(errorCode.getStatusCode().getCode());
response.setContentType(MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8");
objectMapper.writeValue(response.getWriter(), ErrorResponse.error(errorCode));
}

private String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
private String resolveToken(final HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(BEARER_PREFIX.length());
}
return null;
}
return null;
}
}
Loading