From 74842323b3a19131c13d483ce774091314905573 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:45:22 +0900 Subject: [PATCH 01/10] feat : security config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스프링 시큐리티 관련 config Open #12 --- .../config/{ => security}/SecurityConfig.java | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) rename backend/core/src/main/java/site/timecapsulearchive/core/global/config/{ => security}/SecurityConfig.java (53%) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/SecurityConfig.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/SecurityConfig.java similarity index 53% rename from backend/core/src/main/java/site/timecapsulearchive/core/global/config/SecurityConfig.java rename to backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/SecurityConfig.java index 89c113a7c..9d2df9aac 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/SecurityConfig.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/SecurityConfig.java @@ -1,8 +1,11 @@ -package site.timecapsulearchive.core.global.config; +package site.timecapsulearchive.core.global.config.security; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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; @@ -11,30 +14,46 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final AuthenticationFailureHandler authenticationFailureHandler; + private final AuthenticationProvider jwtAuthenticationProvider; + @Bean public PasswordEncoder getPasswordEncoder() { return new BCryptPasswordEncoder(); } - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain filterChainWithJwt( + HttpSecurity http, + AuthenticationConfiguration authenticationConfiguration + ) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .headers(header -> header.frameOptions(FrameOptionsConfig::disable)) - .securityMatcher("/**") + .securityMatcher("/api/**") .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) - .authorizeHttpRequests(authz -> authz - .anyRequest().permitAll() + .authorizeHttpRequests(auth -> auth + .requestMatchers("/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui/**") + .permitAll() + .anyRequest().authenticated() + ) + .apply( + JwtDsl.jwtDsl( + authenticationConfiguration, + jwtAuthenticationProvider, + authenticationFailureHandler + ) ); return http.build(); From 31397b014c40452c76a9f80ffe361e4e70a4fe46 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:48:39 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat=20:=20jwt=20=EA=B4=80=EB=A0=A8=20con?= =?UTF-8?q?fig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConfigurationPropertie로 jwt config Open #12 --- .../timecapsulearchive/core/CoreApplication.java | 2 ++ .../core/global/config/security/JwtProperties.java | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtProperties.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/CoreApplication.java b/backend/core/src/main/java/site/timecapsulearchive/core/CoreApplication.java index eb605d2aa..eb0a4d045 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/CoreApplication.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/CoreApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +@ConfigurationPropertiesScan @SpringBootApplication public class CoreApplication { diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtProperties.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtProperties.java new file mode 100644 index 000000000..672bb31f0 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtProperties.java @@ -0,0 +1,13 @@ +package site.timecapsulearchive.core.global.config.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties( + String secretKey, + long accessTokenValidityMs, + long refreshTokenValidityMs, + long temporaryTokenValidityMs +) { + +} \ No newline at end of file From 6758f53f4113eb76ee8e99db8311f1fa6509246d Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:50:44 +0900 Subject: [PATCH 03/10] =?UTF-8?q?feat=20:=20jwt=20factory=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 토큰을 만들어주는 factory 추가 Open #12 --- .../global/security/jwt/JwtConstants.java | 15 ++ .../core/global/security/jwt/JwtFactory.java | 135 ++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtFactory.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java new file mode 100644 index 000000000..8f5be576a --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java @@ -0,0 +1,15 @@ +package site.timecapsulearchive.core.global.security.jwt; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum JwtConstants { + MEMBER_ID("memberId"), + AUTHORIZATION_HEADER("Authorization"), + TOKEN_TYPE("Bearer "), + MEMBER_INFO_KEY("memberInfoKey"); + + private final String value; +} \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtFactory.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtFactory.java new file mode 100644 index 000000000..a659caa28 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtFactory.java @@ -0,0 +1,135 @@ +package site.timecapsulearchive.core.global.security.jwt; + +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.stereotype.Component; +import site.timecapsulearchive.core.global.config.security.JwtProperties; +import site.timecapsulearchive.core.global.error.exception.InvalidTokenException; + +@Component +public class JwtFactory { + + private static final String MEMBER_ID_CLAIM = JwtConstants.MEMBER_ID.getValue(); + private static final String MEMBER_INFO_CLAIM = JwtConstants.MEMBER_INFO_KEY.getValue(); + + private final SecretKey key; + private final long accessTokenValidityMs; + private final long refreshTokenValidityMs; + private final long temporaryValidityMs; + + public JwtFactory(JwtProperties jwtProperties) { + this.key = Keys.hmacShaKeyFor(jwtProperties.secretKey().getBytes(StandardCharsets.UTF_8)); + this.accessTokenValidityMs = jwtProperties.accessTokenValidityMs(); + this.refreshTokenValidityMs = jwtProperties.refreshTokenValidityMs(); + this.temporaryValidityMs = jwtProperties.temporaryTokenValidityMs(); + } + + /** + * 사용자 아이디를 받아서 엑세스 토큰 반환 + * + * @param memberId 사용자 아이디 + * @return 액세스 토큰 + */ + public String createAccessToken(Long memberId) { + Date now = new Date(); + Date validity = new Date(now.getTime() + accessTokenValidityMs); + + return Jwts.builder() + .claim(MEMBER_ID_CLAIM, memberId.toString()) + .setExpiration(validity) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 사용자 식별자를 받아서 리프레시 토큰 반환 + * + * @param memberProfileKey 사용자 식별자 + * @return 리프레시 토큰 + */ + public String createRefreshToken(String memberProfileKey) { + Date now = new Date(); + Date validity = new Date(now.getTime() + refreshTokenValidityMs); + + return Jwts.builder() + .claim(MEMBER_INFO_CLAIM, memberProfileKey) + .setExpiration(validity) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 사용자 아이디를 받아서 임시 토큰 (1시간) 토큰 반환 + * + * @param memberId 사용자 아이디 + * @return 리프레시 토큰 + */ + public String createTemporaryAccessToken(Long memberId) { + Date now = new Date(); + Date validity = new Date(now.getTime() + temporaryValidityMs); + + return Jwts.builder() + .claim(MEMBER_ID_CLAIM, memberId.toString()) + .setExpiration(validity) + .signWith(key, SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 토큰과 클레임 키로 클레임 값 파싱 + * + * @param token 토큰 + * @param claimKey 파싱할 클레임 키 + * @return 클레임 키에 따른 값 + */ + public String getClaimValue(final String token, final String claimKey) { + try { + return jwtParser() + .parseClaimsJws(token) + .getBody() + .get(claimKey, String.class); + } catch (final JwtException | IllegalArgumentException e) { + throw new InvalidTokenException(e); + } + } + + private JwtParser jwtParser() { + return Jwts.parserBuilder() + .setSigningKey(key) + .build(); + } + + /** + * 토큰을 파싱해서 올바른 토큰인지 확인 + * + * @param token 검증할 토큰 + * @return 유효한 토큰이면 {@code true} + */ + public boolean isValid(final String token) { + try { + jwtParser().parseClaimsJws(token); + + return true; + } catch (final JwtException | IllegalArgumentException e) { + return false; + } + } + + public String getExpiresIn() { + return String.valueOf(accessTokenValidityMs); + } + + public String getRefreshTokenExpiresIn() { + return String.valueOf(refreshTokenValidityMs); + } + + public String getTemporaryTokenExpiresIn() { + return String.valueOf(temporaryValidityMs); + } +} From 48e90bbdf759f550b408e1c73b8007e3b8f6d7d8 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:52:15 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat=20:=20error=20code=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 에러 처리를 위한 ErrorCode 도입 Open #12 --- .../core/global/common/response/ErrorCode.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorCode.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorCode.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorCode.java new file mode 100644 index 000000000..822e30e66 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorCode.java @@ -0,0 +1,14 @@ +package site.timecapsulearchive.core.global.common.response; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + INVALID_TOKEN_EXCEPTION("A001", "jwt 토큰이 유효하지 않습니다."), + ALREADY_RE_ISSUED_TOKEN_EXCEPTION("A002", "이미 액세스 토큰 재발급에 사용된 리프레시 토큰입니다."); + + private final String code; + private final String message; +} From c21583549fb81c913345038f3a04fb7b2a0446e8 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:53:28 +0900 Subject: [PATCH 05/10] =?UTF-8?q?feat=20:=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 토큰으로 인증하는 필터 추가 Open #12 --- .../exception/InvalidTokenException.java | 18 ++++ .../security/jwt/JwtAuthenticationFilter.java | 89 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InvalidTokenException.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InvalidTokenException.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InvalidTokenException.java new file mode 100644 index 000000000..326cf35ed --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/InvalidTokenException.java @@ -0,0 +1,18 @@ +package site.timecapsulearchive.core.global.error.exception; + +import org.springframework.security.core.AuthenticationException; +import site.timecapsulearchive.core.global.common.response.ErrorCode; + +/** + * 유효하지 않은 jwt 토큰일 때 발생하는 예외 + */ +public class InvalidTokenException extends AuthenticationException { + + public InvalidTokenException() { + super(ErrorCode.INVALID_TOKEN_EXCEPTION.getMessage()); + } + + public InvalidTokenException(Throwable throwable) { + super(ErrorCode.INVALID_TOKEN_EXCEPTION.getMessage(), throwable); + } +} \ No newline at end of file diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java new file mode 100644 index 000000000..6876ae4ba --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java @@ -0,0 +1,89 @@ +package site.timecapsulearchive.core.global.security.jwt; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import site.timecapsulearchive.core.global.error.exception.InvalidTokenException; + +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private static final int PREFIX_LENGTH = 7; + private static final String AUTHORIZATION_HEADER = JwtConstants.TOKEN_TYPE.getValue(); + private static final String TOKEN_TYPE = JwtConstants.TOKEN_TYPE.getValue(); + + private final AuthenticationManager authenticationManager; + private final AuthenticationFailureHandler jwtAuthenticationFailureHandler; + + @Override + protected void doFilterInternal( + HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain + ) throws ServletException, IOException { + String accessToken = extractAccessToken(request); + + try { + if (accessToken.isBlank()) { + throw new InvalidTokenException(); + } + + Authentication authenticationResult = attemptAuthentication(accessToken); + + successfulAuthentication(authenticationResult); + + filterChain.doFilter(request, response); + } catch (AuthenticationException authenticationException) { + unsuccessfulAuthentication( + request, response, + authenticationException + ); + } + } + + private String extractAccessToken(HttpServletRequest request) { + String token = request.getHeader(AUTHORIZATION_HEADER); + + if (isNotValidFormat(token)) { + return ""; + } + + return token.substring(PREFIX_LENGTH); + } + + private boolean isNotValidFormat(String token) { + return !StringUtils.hasText(token) || !token.startsWith(TOKEN_TYPE); + } + + private Authentication attemptAuthentication(String accessToken) { + Authentication authentication = JwtAuthenticationToken.unauthenticated(accessToken); + + return authenticationManager.authenticate(authentication); + } + + private void successfulAuthentication(Authentication authResult) { + SecurityContextHolder.getContext() + .setAuthentication(authResult); + } + + private void unsuccessfulAuthentication( + HttpServletRequest request, HttpServletResponse response, + AuthenticationException authenticationException + ) throws ServletException, IOException { + jwtAuthenticationFailureHandler.onAuthenticationFailure( + request, + response, + authenticationException + ); + } +} From 1dd6cdde36edabf673b06ef07dc94cb92f19b06f Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:56:35 +0900 Subject: [PATCH 06/10] =?UTF-8?q?feat=20:=20jwt=20=EC=9D=B8=EC=A6=9D=20pro?= =?UTF-8?q?vider=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 인증 provider 추가 Open #12 --- .../jwt/JwtAuthenticationProvider.java | 43 +++++++++++ .../security/jwt/JwtAuthenticationToken.java | 71 +++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationProvider.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationToken.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationProvider.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationProvider.java new file mode 100644 index 000000000..58379626c --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationProvider.java @@ -0,0 +1,43 @@ +package site.timecapsulearchive.core.global.security.jwt; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; +import site.timecapsulearchive.core.global.error.exception.InvalidTokenException; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationProvider implements AuthenticationProvider { + + private static final String MEMBER_ID_CLAIM_KEY = JwtConstants.MEMBER_ID.getValue(); + + private final JwtFactory jwtFactory; + + @Override + public Authentication authenticate(Authentication authentication) + throws AuthenticationException { + String accessToken = (String) authentication.getCredentials(); + + if (isNotValid(accessToken)) { + throw new InvalidTokenException(); + } + + String memberId = jwtFactory.getClaimValue( + accessToken, + MEMBER_ID_CLAIM_KEY + ); + + return JwtAuthenticationToken.authenticated(Long.valueOf(memberId)); + } + + private boolean isNotValid(String accessToken) { + return !jwtFactory.isValid(accessToken); + } + + @Override + public boolean supports(Class authentication) { + return JwtAuthenticationToken.class.isAssignableFrom(authentication); + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationToken.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationToken.java new file mode 100644 index 000000000..64e7fe367 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationToken.java @@ -0,0 +1,71 @@ +package site.timecapsulearchive.core.global.security.jwt; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.util.Assert; + +public class JwtAuthenticationToken implements Authentication { + + private final List authorities; + private final String accessToken; + private final Long memberId; + private boolean authenticated; + + private JwtAuthenticationToken( + String accessToken, + Long memberId, + boolean authenticated + ) { + this.memberId = memberId; + this.accessToken = accessToken; + this.authorities = Collections.emptyList(); + this.authenticated = authenticated; + } + + public static JwtAuthenticationToken authenticated(Long memberId) { + return new JwtAuthenticationToken(null, memberId, true); + } + + public static JwtAuthenticationToken unauthenticated(String accessToken) { + return new JwtAuthenticationToken(accessToken, null, false); + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public Object getCredentials() { + return accessToken; + } + + @Override + public Object getDetails() { + return ""; + } + + @Override + public Object getPrincipal() { + return memberId; + } + + @Override + public boolean isAuthenticated() { + return authenticated; + } + + @Override + public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { + Assert.isTrue(!isAuthenticated, "false로만 설정이 가능합니다. 생성자를 이용하세요"); + this.authenticated = false; + } + + @Override + public String getName() { + return ""; + } +} From ab4a48625e731cac758ec72bfdd5f2c69b665c32 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:57:11 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat=20:=20jwt=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwt 인증 실패 핸들러 추가 Open #12 --- .../jwt/JwtAuthenticationFailureHandler.java | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFailureHandler.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFailureHandler.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFailureHandler.java new file mode 100644 index 000000000..45bc4678b --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFailureHandler.java @@ -0,0 +1,41 @@ +package site.timecapsulearchive.core.global.security.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.stereotype.Component; +import site.timecapsulearchive.core.global.common.response.ErrorCode; +import site.timecapsulearchive.core.global.common.response.ErrorResponse; + +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFailureHandler implements AuthenticationFailureHandler { + + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationFailure( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) throws IOException { + SecurityContextHolder.clearContext(); + + ErrorResponse errorResponse = ErrorResponse.create( + ErrorCode.INVALID_TOKEN_EXCEPTION.getCode(), + exception.getMessage() + ); + + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + + response.getWriter() + .write(objectMapper.writeValueAsString(errorResponse)); + } +} From 4429b99a5fb253c7ad83b87957d25a90e7449f21 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 20:58:00 +0900 Subject: [PATCH 08/10] =?UTF-8?q?feat=20:=20security=20jwt=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - security에 jwt 관련 설정 추가 Open #12 --- .../core/global/config/security/JwtDsl.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtDsl.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtDsl.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtDsl.java new file mode 100644 index 000000000..36712d869 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/config/security/JwtDsl.java @@ -0,0 +1,47 @@ +package site.timecapsulearchive.core.global.config.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import site.timecapsulearchive.core.global.security.jwt.JwtAuthenticationFilter; + +@RequiredArgsConstructor +public class JwtDsl extends AbstractHttpConfigurer { + + private final AuthenticationConfiguration authenticationConfiguration; + private final AuthenticationProvider jwtAuthenticationProvider; + private final AuthenticationFailureHandler authenticationFailureHandler; + + public static JwtDsl jwtDsl( + AuthenticationConfiguration authenticationConfiguration, + AuthenticationProvider authenticationProvider, + AuthenticationFailureHandler authenticationEntryPoint + ) { + return new JwtDsl( + authenticationConfiguration, + authenticationProvider, + authenticationEntryPoint + ); + } + + @Override + public void init(HttpSecurity http) throws Exception { + http + .authenticationProvider(jwtAuthenticationProvider) + .addFilterBefore( + jwtAuthenticationFilter(), + UsernamePasswordAuthenticationFilter.class + ); + } + + private JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception { + return new JwtAuthenticationFilter( + authenticationConfiguration.getAuthenticationManager(), + authenticationFailureHandler + ); + } +} From 3d7021c3979c488b93c4804ddcdf8e61983e3e2d Mon Sep 17 00:00:00 2001 From: hong seokho Date: Mon, 8 Jan 2024 21:00:01 +0900 Subject: [PATCH 09/10] =?UTF-8?q?feat=20:=20business=20exception,=20error?= =?UTF-8?q?=20response=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비즈니스 익셉션, 에러 응답 클래스 추가 --- .../global/common/response/ErrorResponse.java | 23 +++++++++++++++++++ .../error/exception/BusinessException.java | 16 +++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorResponse.java create mode 100644 backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/BusinessException.java diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorResponse.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorResponse.java new file mode 100644 index 000000000..d455a1ac9 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/common/response/ErrorResponse.java @@ -0,0 +1,23 @@ +package site.timecapsulearchive.core.global.common.response; + +import java.util.Collections; +import java.util.List; + +public record ErrorResponse( + String code, + String message, + List errors +) { + + public static ErrorResponse create(String code, String message) { + return new ErrorResponse(code, message, Collections.emptyList()); + } + + private record Error( + String field, + String value, + String reason + ) { + + } +} diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/BusinessException.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/BusinessException.java new file mode 100644 index 000000000..67b7529b1 --- /dev/null +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/error/exception/BusinessException.java @@ -0,0 +1,16 @@ +package site.timecapsulearchive.core.global.error.exception; + +import lombok.Getter; +import site.timecapsulearchive.core.global.common.response.ErrorCode; + +@Getter +public class BusinessException extends RuntimeException { + + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + + this.errorCode = errorCode; + } +} From 8fa1920518a790fc500207f8584a714698f500c4 Mon Sep 17 00:00:00 2001 From: hong seokho Date: Thu, 11 Jan 2024 16:12:28 +0900 Subject: [PATCH 10/10] =?UTF-8?q?fix=20:=20Authorization=20header=20?= =?UTF-8?q?=EC=83=81=EC=88=98=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Spring 제공 Header로 변경 Open #12 --- .../core/global/security/jwt/JwtAuthenticationFilter.java | 4 ++-- .../core/global/security/jwt/JwtConstants.java | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java index 6876ae4ba..b7ec37b05 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtAuthenticationFilter.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -19,7 +20,6 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private static final int PREFIX_LENGTH = 7; - private static final String AUTHORIZATION_HEADER = JwtConstants.TOKEN_TYPE.getValue(); private static final String TOKEN_TYPE = JwtConstants.TOKEN_TYPE.getValue(); private final AuthenticationManager authenticationManager; @@ -52,7 +52,7 @@ protected void doFilterInternal( } private String extractAccessToken(HttpServletRequest request) { - String token = request.getHeader(AUTHORIZATION_HEADER); + String token = request.getHeader(HttpHeaders.AUTHORIZATION); if (isNotValidFormat(token)) { return ""; diff --git a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java index 8f5be576a..e4ec15ad5 100644 --- a/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java +++ b/backend/core/src/main/java/site/timecapsulearchive/core/global/security/jwt/JwtConstants.java @@ -7,7 +7,6 @@ @RequiredArgsConstructor public enum JwtConstants { MEMBER_ID("memberId"), - AUTHORIZATION_HEADER("Authorization"), TOKEN_TYPE("Bearer "), MEMBER_INFO_KEY("memberInfoKey");