Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : jwt 인증 기능 구현 #12 #23

Merged
merged 11 commits into from
Jan 11, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@ConfigurationPropertiesScan
seokho-1116 marked this conversation as resolved.
Show resolved Hide resolved
@SpringBootApplication
public class CoreApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<Error> 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
) {

}
}
Original file line number Diff line number Diff line change
@@ -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<JwtDsl, HttpSecurity> {

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
);
}
}
Original file line number Diff line number Diff line change
@@ -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
) {

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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.http.HttpHeaders;
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 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(HttpHeaders.AUTHORIZATION);

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
);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading