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

[INIT] Security Config및 JWT 필터 설정 #24

Merged
merged 17 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ repositories {
}

dependencies {

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
Expand All @@ -34,14 +35,23 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

//jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// slack logback
implementation 'com.github.maricn:logback-slack-appender:1.4.0'

// s3
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")

// s3
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")

}

tasks.named('test') {
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/com/tiki/server/auth/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.tiki.server.auth.config;

import com.tiki.server.auth.exception.handler.CustomAccessDeniedHandler;
import com.tiki.server.auth.exception.handler.CustomAuthenticationEntryPointHandler;
import com.tiki.server.auth.filter.ExceptionHandlerFilter;
import com.tiki.server.auth.filter.JwtAuthenticationFilter;
import com.tiki.server.auth.jwt.JwtProvider;
import com.tiki.server.auth.jwt.JwtValidator;
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.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomAuthenticationEntryPointHandler customAuthenticationEntryPointHandler;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagementConfigurer ->
sessionManagementConfigurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptionHandlingConfigurer ->
exceptionHandlingConfigurer
.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPointHandler))
.authorizeHttpRequests(request ->
request
.requestMatchers("/api/v1/auth/login").permitAll()
Chan531 marked this conversation as resolved.
Show resolved Hide resolved
.requestMatchers("/api/v1/auth/password").permitAll()
.requestMatchers("/api/v1/member/password").permitAll()
.requestMatchers("/api/v1/member").permitAll()
.anyRequest()
.authenticated())
.addFilterBefore(
jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class
)
.addFilterBefore(
new ExceptionHandlerFilter(), JwtAuthenticationFilter.class
)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.tiki.server.auth.exception.handler;

import com.tiki.server.auth.info.AuthenticationResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static com.tiki.server.auth.message.ErrorCode.UNAUTHORIZED_USER;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

private AuthenticationResponse authenticationResponse;

@Override
Chan531 marked this conversation as resolved.
Show resolved Hide resolved
public void handle(
HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException
) throws IOException {
authenticationResponse.makeFailureResponse(response, UNAUTHORIZED_USER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.tiki.server.auth.exception.handler;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tiki.server.auth.info.AuthenticationResponse;
import com.tiki.server.auth.message.ErrorCode;
import com.tiki.server.common.dto.ErrorResponse;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static com.tiki.server.auth.message.ErrorCode.UNAUTHENTICATED_USER;

@Component
public class CustomAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
Chan531 marked this conversation as resolved.
Show resolved Hide resolved

AuthenticationResponse authenticationResponse;

@Override
public void commence(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException
) throws IOException {
authenticationResponse.makeFailureResponse(response, UNAUTHENTICATED_USER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.tiki.server.auth.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@Component
@RequiredArgsConstructor
public class ExceptionHandlerFilter extends OncePerRequestFilter {

@Override
Chan531 marked this conversation as resolved.
Show resolved Hide resolved
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.tiki.server.auth.filter;


import com.tiki.server.auth.exception.AuthException;
import com.tiki.server.auth.jwt.JwtProvider;
import com.tiki.server.auth.jwt.JwtValidator;
import com.tiki.server.auth.jwt.UserAuthentication;
import com.tiki.server.common.Constants;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

import static com.tiki.server.auth.jwt.JwtValidationType.VALID_JWT;
import static com.tiki.server.auth.message.ErrorCode.INVALID_KEY;
import static io.jsonwebtoken.lang.Strings.hasText;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtProvider jwtProvider;
private final JwtValidator jwtValidator;

@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws IOException {
try {
val token = jwtProvider.getAccessTokenFromRequest(request);
if (hasText(token) && jwtValidator.validateToken(token) == VALID_JWT) {
val authentication = new UserAuthentication(jwtProvider.getUserFromJwt(token), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.tiki.server.auth.info;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tiki.server.auth.message.ErrorCode;
import com.tiki.server.common.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;

import java.io.IOException;

import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@RequiredArgsConstructor
public class AuthenticationResponse {

private final ObjectMapper objectMapper;

public void makeFailureResponse(HttpServletResponse response, ErrorCode errorCode) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType(APPLICATION_JSON_VALUE);
response.setStatus(errorCode.getHttpStatus().value());
response.getWriter().println(objectMapper.writeValueAsString(ErrorResponse.of(errorCode.getMessage())));
}
}
17 changes: 17 additions & 0 deletions src/main/java/com/tiki/server/auth/jwt/JwtGenerator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.tiki.server.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import lombok.val;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;

import static io.jsonwebtoken.security.Keys.hmacShaKeyFor;
import static java.util.Base64.getEncoder;

@Component
public class JwtGenerator {

}
52 changes: 52 additions & 0 deletions src/main/java/com/tiki/server/auth/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.tiki.server.auth.jwt;

Chan531 marked this conversation as resolved.
Show resolved Hide resolved
import com.tiki.server.auth.exception.AuthException;
import com.tiki.server.common.Constants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.val;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.crypto.SecretKey;

import static com.tiki.server.auth.message.ErrorCode.INVALID_KEY;
import static io.jsonwebtoken.security.Keys.hmacShaKeyFor;
import static java.util.Base64.getEncoder;

@RequiredArgsConstructor
@Component
public class JwtProvider {
Chan531 marked this conversation as resolved.
Show resolved Hide resolved

@Value("${jwt.secret}")
private String secretKey;

public String getAccessTokenFromRequest(HttpServletRequest request) {
val accessToken = request.getHeader(Constants.AUTHORIZATION);
if (!StringUtils.hasText(accessToken) || !accessToken.startsWith(Constants.BEARER)) {
paragon0107 marked this conversation as resolved.
Show resolved Hide resolved
throw new AuthException(INVALID_KEY);
}
return accessToken.substring(Constants.BEARER.length());
}

public long getUserFromJwt(String token) {
val claims = getBodyFromJwt(token);
return Long.parseLong(claims.get("memberId").toString());
}

public Claims getBodyFromJwt(final String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}

private SecretKey getSigningKey() {
val encodedKey = getEncoder().encodeToString(secretKey.getBytes());
return hmacShaKeyFor(encodedKey.getBytes());
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/tiki/server/auth/jwt/JwtValidationType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tiki.server.auth.jwt;

public enum JwtValidationType {
VALID_JWT,
INVALID_JWT_TOKEN,
EXPIRED_JWT_TOKEN,
UNSUPPORTED_JWT_TOKEN,
EMPTY_JWT
}
38 changes: 38 additions & 0 deletions src/main/java/com/tiki/server/auth/jwt/JwtValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.tiki.server.auth.jwt;

import com.tiki.server.auth.exception.AuthException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import static com.tiki.server.auth.jwt.JwtValidationType.*;

@Slf4j
@RequiredArgsConstructor
@Component
public class JwtValidator {

private final JwtProvider jwtProvider;
Chan531 marked this conversation as resolved.
Show resolved Hide resolved

public JwtValidationType validateToken(String token) {
try {
jwtProvider.getBodyFromJwt(token);
return VALID_JWT;
} catch (MalformedJwtException exception) {
log.error(exception.getMessage());
return INVALID_JWT_TOKEN;
} catch (ExpiredJwtException exception) {
log.error(exception.getMessage());
return EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException exception) {
log.error(exception.getMessage());
return UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException exception) {
log.error(exception.getMessage());
return EMPTY_JWT;
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/tiki/server/auth/jwt/UserAuthentication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.tiki.server.auth.jwt;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

public class UserAuthentication extends UsernamePasswordAuthenticationToken {
public UserAuthentication(
Object principal,
Object credentials,
Collection<? extends GrantedAuthority> authorities
) {
super(principal, credentials, authorities);
}
}
Loading
Loading