diff --git a/security-admin/build.gradle b/security-admin/build.gradle index 2a00307c..9c9de646 100644 --- a/security-admin/build.gradle +++ b/security-admin/build.gradle @@ -20,6 +20,7 @@ dependencies { implementation project(':domain-user') implementation project(':domain-base') implementation project(':domain-token') + implementation project(':application-config') // SPRING SECURITY api 'org.springframework.boot:spring-boot-starter-security' api 'org.springframework.boot:spring-boot-starter-test' diff --git a/security-front/build.gradle b/security-front/build.gradle index 5bb9ee1b..6201e438 100644 --- a/security-front/build.gradle +++ b/security-front/build.gradle @@ -21,6 +21,7 @@ dependencies { implementation project(':domain-base') implementation project(':domain-token') implementation project(':external-oauth') + implementation project(':application-config') // SPRING SECURITY api 'org.springframework.boot:spring-boot-starter-security' api 'org.springframework.boot:spring-boot-starter-test' diff --git a/security-front/src/main/java/com/nowait/frontsecurity/auth/jwt/JwtAuthorizationFilter.java b/security-front/src/main/java/com/nowait/frontsecurity/auth/jwt/JwtAuthorizationFilter.java new file mode 100644 index 00000000..3969108e --- /dev/null +++ b/security-front/src/main/java/com/nowait/frontsecurity/auth/jwt/JwtAuthorizationFilter.java @@ -0,0 +1,67 @@ +package com.nowait.frontsecurity.auth.jwt; + +import java.io.IOException; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.OncePerRequestFilter; +import com.nowait.externaloauth.service.CustomUserDetailService; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@RequiredArgsConstructor +@Slf4j +public class JwtAuthorizationFilter extends OncePerRequestFilter { + private final JwtUtil jwtUtil; + private final CustomUserDetailService userDetailsService; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + try { + String token = extractTokenFromRequest(request); + if (token != null) { + // 만료 체크 + if (jwtUtil.isExpired(token)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("access token expired"); + return; + } + // 토큰 category 체크(불필요하면 생략) + String tokenCategory = jwtUtil.getTokenCategory(token); + if (!"accessToken".equals(tokenCategory)) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.getWriter().print("invalid access token"); + return; + } + // userId 추출 → UserDetails 조회 + Long userId = jwtUtil.getUserId(token); + var userDetails = userDetailsService.loadUserById(userId); + // 인증 객체 생성 및 컨텍스트에 설정 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + } + } catch (Exception ex) { + log.error("JWT filter error: {}", ex.getMessage()); + } finally { + filterChain.doFilter(request, response); + } + } + + private String extractTokenFromRequest(HttpServletRequest request) { + String header = request.getHeader("Authorization"); + if (header != null && header.startsWith("Bearer ")) { + return header.substring(7); + } + return null; + } +} diff --git a/security-front/src/main/java/com/nowait/frontsecurity/auth/jwt/JwtUtil.java b/security-front/src/main/java/com/nowait/frontsecurity/auth/jwt/JwtUtil.java new file mode 100644 index 00000000..9c41194e --- /dev/null +++ b/security-front/src/main/java/com/nowait/frontsecurity/auth/jwt/JwtUtil.java @@ -0,0 +1,76 @@ +package com.nowait.frontsecurity.auth.jwt; + +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Jwts; + +@Component +public class JwtUtil { + private final SecretKey secretKey; + + // 시크릿 키를 암호화하여, 키 생성 + public JwtUtil(@Value("${jwt.secret}") String secret) { + this.secretKey = new SecretKeySpec( + secret.getBytes(StandardCharsets.UTF_8), + Jwts.SIG.HS256.key().build().getAlgorithm() + ); + } + + public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) { + return Jwts.builder() + .claim("tokenCategory", tokenCategory) // accessToken + .claim("userId", userId) + .claim("role", role) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } + + public String createRefreshToken(String tokenCategory, Long userId, Long expiredMs) { + return Jwts.builder() + .claim("tokenCategory", tokenCategory) // refreshToken + .claim("userId", userId) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiredMs)) + .signWith(secretKey) + .compact(); + } + + public String getTokenCategory(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseClaimsJws(token) + .getBody() + .get("tokenCategory", String.class); + } + + public String getRole(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseClaimsJws(token) + .getBody() + .get("role", String.class); + } + + public Long getUserId(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseClaimsJws(token) + .getBody() + .get("userId", Long.class); + } + + public Boolean isExpired(String token) { + return Jwts.parser().verifyWith(secretKey).build() + .parseSignedClaims(token) + .getPayload() + .getExpiration() + .before(new Date()); + } + +}