diff --git a/build.gradle b/build.gradle index fbccb3b..d7d8440 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation "org.springdoc:springdoc-openapi-starter-webmvc-api:2.8.5" implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.5" + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.security:spring-security-crypto' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/AuthenticationFilter.java b/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/AuthenticationFilter.java index 97e0cb3..3e92d14 100644 --- a/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/AuthenticationFilter.java +++ b/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/AuthenticationFilter.java @@ -1,105 +1,105 @@ -package com.retrip.trip.infra.adapter.in.presentation.filter; - -import com.retrip.trip.application.in.request.context.UserContext; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwts; -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.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.security.KeyFactory; -import java.security.PublicKey; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.UUID; - -@Slf4j -@Component -public class AuthenticationFilter extends OncePerRequestFilter { - - @Value("${jwt.public-key}") - private String publicKeyString; - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - String path = request.getRequestURI(); - String pathLowercase = path.toLowerCase(); - - if (path.equals("/") || - pathLowercase.contains("swagger") || - pathLowercase.contains("api-docs") || - pathLowercase.contains("actuator") || - pathLowercase.contains("robots.txt") || - pathLowercase.contains("status-check") || - pathLowercase.contains("/h2-console")) { - filterChain.doFilter(request, response); - return; - } - - String token = resolveToken(request); - if (token == null || token.isEmpty()) { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - - try { - Claims claims = getClaims(token); - - String subject = claims.getSubject(); - UUID memberId = UUID.fromString(subject); - - UserContext userContext = new UserContext( - memberId, - claims.get("nickname", String.class), - claims.get("email", String.class), - claims.get("name", String.class), - null, - 0 - ); - - request.setAttribute("userContext", userContext); - - filterChain.doFilter(request, response); - - } catch (Exception e) { - log.error("Token validation failed: {}", e.getMessage()); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - - } - } - - private String resolveToken(HttpServletRequest request) { - String bearerToken = request.getHeader("Authorization"); - if (bearerToken != null && bearerToken.startsWith("Bearer ")) { - return bearerToken.substring(7); - } - return null; - } - - private Claims getClaims(String token) throws Exception { - String sanitizedKey = publicKeyString - .replace("-----BEGIN PUBLIC KEY-----", "") - .replace("-----END PUBLIC KEY-----", "") - .replaceAll("\\s", ""); - - byte[] publicBytes = Base64.getDecoder().decode(sanitizedKey); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - PublicKey publicKey = keyFactory.generatePublic(keySpec); - - Jws jws = Jwts.parser() - .verifyWith(publicKey) - .build() - .parseSignedClaims(token); - - return jws.getPayload(); - } -} \ No newline at end of file +//package com.retrip.trip.infra.adapter.in.presentation.filter; +// +//import com.retrip.trip.application.in.request.context.UserContext; +//import io.jsonwebtoken.Claims; +//import io.jsonwebtoken.Jws; +//import io.jsonwebtoken.Jwts; +//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.springframework.beans.factory.annotation.Value; +//import org.springframework.stereotype.Component; +//import org.springframework.web.filter.OncePerRequestFilter; +// +//import java.io.IOException; +//import java.security.KeyFactory; +//import java.security.PublicKey; +//import java.security.spec.X509EncodedKeySpec; +//import java.util.Base64; +//import java.util.UUID; +// +//@Slf4j +//@Component +//public class AuthenticationFilter extends OncePerRequestFilter { +// +// @Value("${jwt.public-key}") +// private String publicKeyString; +// +// @Override +// protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, +// FilterChain filterChain) throws ServletException, IOException { +// String path = request.getRequestURI(); +// String pathLowercase = path.toLowerCase(); +// +// if (path.equals("/") || +// pathLowercase.contains("swagger") || +// pathLowercase.contains("api-docs") || +// pathLowercase.contains("actuator") || +// pathLowercase.contains("robots.txt") || +// pathLowercase.contains("status-check") || +// pathLowercase.contains("/h2-console")) { +// filterChain.doFilter(request, response); +// return; +// } +// +// String token = resolveToken(request); +// if (token == null || token.isEmpty()) { +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// return; +// } +// +// try { +// Claims claims = getClaims(token); +// +// String subject = claims.getSubject(); +// UUID memberId = UUID.fromString(subject); +// +// UserContext userContext = new UserContext( +// memberId, +// claims.get("nickname", String.class), +// claims.get("email", String.class), +// claims.get("name", String.class), +// null, +// 0 +// ); +// +// request.setAttribute("userContext", userContext); +// +// filterChain.doFilter(request, response); +// +// } catch (Exception e) { +// log.error("Token validation failed: {}", e.getMessage()); +// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); +// +// } +// } +// +// private String resolveToken(HttpServletRequest request) { +// String bearerToken = request.getHeader("Authorization"); +// if (bearerToken != null && bearerToken.startsWith("Bearer ")) { +// return bearerToken.substring(7); +// } +// return null; +// } +// +// private Claims getClaims(String token) throws Exception { +// String sanitizedKey = publicKeyString +// .replace("-----BEGIN PUBLIC KEY-----", "") +// .replace("-----END PUBLIC KEY-----", "") +// .replaceAll("\\s", ""); +// +// byte[] publicBytes = Base64.getDecoder().decode(sanitizedKey); +// X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); +// KeyFactory keyFactory = KeyFactory.getInstance("RSA"); +// PublicKey publicKey = keyFactory.generatePublic(keySpec); +// +// Jws jws = Jwts.parser() +// .verifyWith(publicKey) +// .build() +// .parseSignedClaims(token); +// +// return jws.getPayload(); +// } +//} \ No newline at end of file diff --git a/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/JwtAuthenticationFilter.java b/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/JwtAuthenticationFilter.java index 5451098..a3b3007 100644 --- a/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/retrip/trip/infra/adapter/in/presentation/filter/JwtAuthenticationFilter.java @@ -1,136 +1,109 @@ -//package com.retrip.trip.infra.adapter.in.presentation.filter; -// -//import com.retrip.trip.application.in.request.context.UserContext; -//import io.jsonwebtoken.Claims; -//import io.jsonwebtoken.Jws; -//import io.jsonwebtoken.Jwts; -//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.springframework.beans.factory.annotation.Value; -//import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -//import org.springframework.security.core.authority.SimpleGrantedAuthority; -//import org.springframework.security.core.context.SecurityContextHolder; -//import org.springframework.stereotype.Component; -//import org.springframework.web.filter.OncePerRequestFilter; -// -//import java.io.IOException; -//import java.security.KeyFactory; -//import java.security.PublicKey; -//import java.security.spec.X509EncodedKeySpec; -//import java.util.Base64; -//import java.util.Collections; -//import java.util.List; -//import java.util.UUID; -// -//@Slf4j -//@Component -//public class JwtAuthenticationFilter extends OncePerRequestFilter { -// -// @Value("${jwt.public-key}") -// private String publicKeyString; -// -// @Override -// protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) -// throws ServletException, IOException { -// -// String path = request.getRequestURI(); -// -// if (isExcludedPath(path)) { -// filterChain.doFilter(request, response); -// return; -// } -// -// String token = resolveToken(request); -// -// if (token != null && validateToken(token)) { -// try { -// -// Claims claims = getClaims(token); -// -// -// String subject = claims.getSubject(); -// UUID memberId = UUID.fromString(subject); -// -// -// String role = claims.get("auth", String.class); -// -// UserContext userContext = new UserContext( -// memberId, -// claims.get("nickname", String.class), -// claims.get("email", String.class), -// claims.get("name", String.class), -// null, -// 0 -// ); -// -// request.setAttribute("userContext", userContext); -// -// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( -// memberId, -// null, -// role != null ? List.of(new SimpleGrantedAuthority(role)) : Collections.emptyList() -// ); -// SecurityContextHolder.getContext().setAuthentication(authentication); -// -// } catch (Exception e) { -// log.error("Token processing error: {}", e.getMessage()); -// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// return; -// } -// } else { -// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); -// return; -// } -// -// filterChain.doFilter(request, response); -// } -// -// private boolean isExcludedPath(String path) { -// String pathLowercase = path.toLowerCase(); -// return path.equals("/") || -// pathLowercase.contains("swagger") || -// pathLowercase.contains("api-docs") || -// pathLowercase.contains("actuator") || -// pathLowercase.contains("robots.txt") || -// pathLowercase.contains("status-check"); -// } -// -// private String resolveToken(HttpServletRequest request) { -// String bearerToken = request.getHeader("Authorization"); -// if (bearerToken != null && bearerToken.startsWith("Bearer ")) { -// return bearerToken.substring(7); -// } -// return null; -// } -// -// private boolean validateToken(String token) { -// try { -// getClaims(token); -// return true; -// } catch (Exception e) { -// return false; -// } -// } -// -// private Claims getClaims(String token) throws Exception { -// String sanitizedKey = publicKeyString -// .replace("-----BEGIN PUBLIC KEY-----", "") -// .replace("-----END PUBLIC KEY-----", "") -// .replaceAll("\\s", ""); -// -// byte[] publicBytes = Base64.getDecoder().decode(sanitizedKey); -// X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); -// KeyFactory keyFactory = KeyFactory.getInstance("RSA"); -// PublicKey publicKey = keyFactory.generatePublic(keySpec); -// -// Jws jws = Jwts.parser() -// .verifyWith(publicKey) -// .build() -// .parseSignedClaims(token); -// -// return jws.getPayload(); -// } -//} \ No newline at end of file +package com.retrip.trip.infra.adapter.in.presentation.filter; + +import com.retrip.trip.application.in.request.context.UserContext; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.Jwts; +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.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@Slf4j +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Value("${jwt.public-key}") + private String publicKeyString; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + String token = resolveToken(request); + + if (token != null && validateToken(token)) { + try { + Claims claims = getClaims(token); + + String subject = claims.getSubject(); + UUID memberId = UUID.fromString(subject); + String role = claims.get("auth", String.class); + + UserContext userContext = new UserContext( + memberId, + claims.get("nickname", String.class), + claims.get("email", String.class), + claims.get("name", String.class), + null, + 0 + ); + request.setAttribute("userContext", userContext); + + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + memberId, + null, + role != null ? List.of(new SimpleGrantedAuthority(role)) : Collections.emptyList() + ); + SecurityContextHolder.getContext().setAuthentication(authentication); + + } catch (Exception e) { + log.error("Token processing error: {}", e.getMessage()); + } + } + + filterChain.doFilter(request, response); + } + + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + return null; + } + + private boolean validateToken(String token) { + try { + getClaims(token); + return true; + } catch (Exception e) { + return false; + } + } + + private Claims getClaims(String token) throws Exception { + String sanitizedKey = publicKeyString + .replace("-----BEGIN PUBLIC KEY-----", "") + .replace("-----END PUBLIC KEY-----", "") + .replaceAll("\\s", ""); + + byte[] publicBytes = Base64.getDecoder().decode(sanitizedKey); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(keySpec); + + Jws jws = Jwts.parser() + .verifyWith(publicKey) + .build() + .parseSignedClaims(token); + + return jws.getPayload(); + } +} \ No newline at end of file diff --git a/src/main/java/com/retrip/trip/infra/config/SecurityConfig.java b/src/main/java/com/retrip/trip/infra/config/SecurityConfig.java index 5fddc7b..e2c1ab4 100644 --- a/src/main/java/com/retrip/trip/infra/config/SecurityConfig.java +++ b/src/main/java/com/retrip/trip/infra/config/SecurityConfig.java @@ -1,13 +1,45 @@ package com.retrip.trip.infra.config; +import com.retrip.trip.infra.adapter.in.presentation.filter.JwtAuthenticationFilter; // 이제 import 가능! +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration +@EnableWebSecurity +@RequiredArgsConstructor public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(auth -> auth + // GET /trips 와 하위 경로는 모두 허용 여행 목록 페이지는 로그인없이도 접근 가능한 페이지 + .requestMatchers(HttpMethod.GET, "/trips", "/trips/**").permitAll() + .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**").permitAll() + .anyRequest().authenticated() + ) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } } \ No newline at end of file