diff --git a/build.gradle b/build.gradle index c578c7e..63479ed 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,7 @@ dependencies { runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' diff --git a/src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java b/src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java new file mode 100644 index 0000000..456b042 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/config/JwtFilter.java @@ -0,0 +1,48 @@ +package com.dnd.dndtravel.auth.config; + +import io.jsonwebtoken.io.IOException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +@RequiredArgsConstructor +public class JwtFilter extends GenericFilterBean { + + public static final String ACCESS_HEADER= "Authorization"; + private final JwtProvider jwtProvider; + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException, java.io.IOException { + HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + String token = resolveToken(httpServletRequest); + String requestURI = httpServletRequest.getRequestURI(); + + if (StringUtils.hasText(token) && jwtProvider.validateToken(token)) { + Authentication authentication = jwtProvider.getAuthentication(token); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + else { + System.out.println("유효한 jwt 토큰이 없습니다. uri: " + requestURI); + } + + filterChain.doFilter(servletRequest, servletResponse); + } + + // Request Header 토큰 정보 추출 + private String resolveToken(HttpServletRequest request) { + String bearerToken = request.getHeader(ACCESS_HEADER); + + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring(7); + } + + return null; + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java b/src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java new file mode 100644 index 0000000..f704b8d --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/config/JwtProvider.java @@ -0,0 +1,101 @@ +package com.dnd.dndtravel.auth.config; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SecurityException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; + +import java.security.Key; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtProvider implements InitializingBean { + + private static final String AUTHORITIES_KEY = "memberId"; + private final long tokenValidityInMilliseconds; + private final String secretKey; + private Key key; + + public JwtProvider( + @Value("${JWT_SECRET_KEY}") String secretKey, + @Value("86400") long tokenValidityInSeconds) { + this.secretKey = secretKey; + this.tokenValidityInMilliseconds = tokenValidityInSeconds * 1000; + } + + // secretKey 값을 Base64 Decode 해서 key 변수에 할당 + @Override + public void afterPropertiesSet() { + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + // JWT Access 토큰 생성 + public String createToken(Authentication authentication) { + Long memberId = Long.parseLong(authentication.getName()); + long now = (new Date()).getTime(); + Date validity = new Date(now + this.tokenValidityInMilliseconds); //1일 + + return Jwts.builder() + .setSubject(String.valueOf(memberId)) + .claim(AUTHORITIES_KEY, memberId) + .signWith(key, SignatureAlgorithm.HS256) + .setExpiration(validity) + .compact(); + } + + // Refresh 토큰 생성 + public String createRefreshToken(Long memberId) { + long now = (new Date()).getTime(); + Date validity = new Date(now + this.tokenValidityInMilliseconds * 14); // 14일 + + return Jwts.builder() + .setSubject(String.valueOf(memberId)) + .signWith(key, SignatureAlgorithm.HS256) + .setExpiration(validity) + .compact(); + } + + // authentication 객체 생성 + public Authentication getAuthentication(String token) { + Claims claims = Jwts.parser() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); + Collection authorities = + Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(",")) + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + return new UsernamePasswordAuthenticationToken(claims.get(AUTHORITIES_KEY), token, authorities); + } + + // Token 유효성 검증 + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (SecurityException | MalformedJwtException e) { + System.out.println("잘못된 JWT Signature"); + } catch (ExpiredJwtException e) { + System.out.println("만료된 JWT 토큰"); + } catch (UnsupportedJwtException e) { + System.out.println("지원 되지 않는 JWT 토큰"); + } catch (IllegalArgumentException e) { + System.out.println("잘못된 JWT 토큰"); + } + + return false; + } +} diff --git a/src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java b/src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java new file mode 100644 index 0000000..9f0ff62 --- /dev/null +++ b/src/main/java/com/dnd/dndtravel/auth/config/JwtSecurityConfig.java @@ -0,0 +1,35 @@ +package com.dnd.dndtravel.auth.config; + +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.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class JwtSecurityConfig { + + private final JwtProvider jwtProvider; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf() + .disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeHttpRequests() + .requestMatchers("/login/**").permitAll() + .anyRequest().authenticated() + .and() + .addFilterBefore(new JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file