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
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ dependencies {
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'

implementation 'org.springframework.boot:spring-boot-starter-security'

implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
package com.retrip.trip.infra.adapter.in.presentation.filter;

import com.retrip.trip.application.in.request.context.UserContext;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Component
public class AuthenticationFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String path = request.getRequestURI();
String pathLowercase = path.toLowerCase();

// 제외할 URL 체크
if (path.equals("/") ||
pathLowercase.contains("swagger") ||
pathLowercase.contains("api-docs") ||
pathLowercase.contains("actuator") ||
pathLowercase.contains("robots.txt") ||
pathLowercase.contains("status-check")) {
filterChain.doFilter(request, response);
return; // 필터 종료
}

String token = request.getHeader("Authorization");
if (token == null || token.isEmpty()) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
request.setAttribute("userContext", UserContext.mockOf());
filterChain.doFilter(request, response);
}
}
//package com.retrip.trip.infra.adapter.in.presentation.filter;
//
//import com.retrip.trip.application.in.request.context.UserContext;
//import jakarta.servlet.FilterChain;
//import jakarta.servlet.ServletException;
//import jakarta.servlet.http.HttpServletRequest;
//import jakarta.servlet.http.HttpServletResponse;
//import org.springframework.stereotype.Component;
//import org.springframework.web.filter.OncePerRequestFilter;
//
//import java.io.IOException;
//
//@Component
//public class AuthenticationFilter extends OncePerRequestFilter {
//
// @Override
// protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
// FilterChain filterChain) throws ServletException, IOException {
// String path = request.getRequestURI();
// String pathLowercase = path.toLowerCase();
//
// // 제외할 URL 체크
// if (path.equals("/") ||
// pathLowercase.contains("swagger") ||
// pathLowercase.contains("api-docs") ||
// pathLowercase.contains("actuator") ||
// pathLowercase.contains("robots.txt") ||
// pathLowercase.contains("status-check")) {
// filterChain.doFilter(request, response);
// return; // 필터 종료
// }
//
// String token = request.getHeader("Authorization");
// if (token == null || token.isEmpty()) {
// response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
// return;
// }
// request.setAttribute("userContext", UserContext.mockOf());
// filterChain.doFilter(request, response);
// }
//}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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<Claims> jws = Jwts.parser()
.verifyWith(publicKey)
.build()
.parseSignedClaims(token);

return jws.getPayload();
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/retrip/trip/infra/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,46 @@
package com.retrip.trip.infra.config;

import com.retrip.trip.infra.adapter.in.presentation.filter.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
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.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 filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth

.requestMatchers(
"/", "/swagger-ui/**", "/v3/api-docs/**", "/actuator/**", "/status-check"
).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
16 changes: 15 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ spring:

datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/trip;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
url: jdbc:h2:mem:trip;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=false;MODE=MySQL
username: sa
password:

Expand All @@ -30,3 +30,17 @@ springdoc:
enabled: true
api-docs:
enabled: true

jwt:
public-key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlfO/CoX9EzhskpAKX9ADH0wfEQZP4rAq
ptqq80W2YaOHAnXu+oU1UrP0b9ccKKSzMCVDwdmXrecZB0dFLPnoazMEbVOf6MSwrhGfxupPRmmJ
sIYgmQwo8/vnjaq/GYFfnHyCy6yKL41G+GZVqgeKdhr+w1jUw4L9Fs0l2J/AYqwTxZOnzxrU5erP
GSE5Dd3AwWt/brxuwA7sRfVS3mbbsYyYExjUrEbst8VtF3Pis35T8YfSDKMOUgiDnp30EAdGU1Up
u59J3+ToLRrIqIszRZqmasrWTL2/ihPO76PSTIMAsJMScjAwjUXA47YOOy6Vkzy8r3bPTYHp5C1N
KucWwwIDAQAB
-----END PUBLIC KEY-----

server:
port: 8081