Skip to content

Commit

Permalink
Merge pull request #23 from tukcomCD2024/ARCH-114-feat/jwt
Browse files Browse the repository at this point in the history
feat : jwt 인증 기능 구현 #12
  • Loading branch information
seokho-1116 authored Jan 11, 2024
2 parents cc351d8 + 8fa1920 commit 8232244
Show file tree
Hide file tree
Showing 14 changed files with 551 additions and 6 deletions.
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
@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

0 comments on commit 8232244

Please sign in to comment.