Skip to content

Commit

Permalink
[feat] 6주차 과제 제출
Browse files Browse the repository at this point in the history
  • Loading branch information
mikekks committed Dec 8, 2023
1 parent 55ddd99 commit 6fb9394
Show file tree
Hide file tree
Showing 52 changed files with 752 additions and 124 deletions.
7 changes: 7 additions & 0 deletions second-seminar/second-seminar/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ out/

### VS Code ###
.vscode/

*.yml
application-local.yml
*.sql
src/main/resources/application-local.yml
src/main/resources/application-dev.yml
src/main/resources/data.sql
12 changes: 12 additions & 0 deletions second-seminar/second-seminar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ dependencies {
// security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

dependencies {
// AWS sdk
implementation("software.amazon.awssdk:bom:2.21.0")
implementation("software.amazon.awssdk:s3:2.21.0")
}

dependencies {
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
implementation group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
}
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package thirdseminar.thirdseminar;
package secondSeminar.secondSeminar;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package secondSeminar.secondSeminar.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;

@Configuration
public class AwsConfig {

private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId";
private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey";

private final String accessKey;
private final String secretKey;
private final String regionString;

public AwsConfig(@Value("${aws-property.access-key}") final String accessKey,
@Value("${aws-property.secret-key}") final String secretKey,
@Value("${aws-property.aws-region}") final String regionString) {
this.accessKey = accessKey;
this.secretKey = secretKey;
this.regionString = regionString;
}


@Bean
public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() {
System.setProperty(AWS_ACCESS_KEY_ID, accessKey);
System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey);
return SystemPropertyCredentialsProvider.create();
}

@Bean
public Region getRegion() {
return Region.of(regionString);
}

@Bean
public S3Client getS3Client() {
return S3Client.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}

@Bean
S3Presigner getS3Presigner() {
return S3Presigner.builder()
.region(getRegion())
.credentialsProvider(systemPropertyCredentialsProvider())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package secondSeminar.secondSeminar.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class BCryptPasswordConfig {

private static final int STRENGTH = 10;

@Bean
public PasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder(STRENGTH);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package secondSeminar.secondSeminar.config;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
setResponse(response);
}

private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package secondSeminar.secondSeminar.config;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

@Component
public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) {
setResponse(response);
}

private void setResponse(HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package thirdseminar.thirdseminar.config;
package secondSeminar.secondSeminar.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package secondSeminar.secondSeminar.config;


import static secondSeminar.secondSeminar.config.JwtValidationType.VALID_JWT;

import io.micrometer.common.lang.NonNull;
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.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;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
try {
final String token = getJwtFromRequest(request);
if (jwtTokenProvider.validateToken(token) == VALID_JWT) {
Long memberId = jwtTokenProvider.getUserFromJwt(token);
// authentication 객체 생성 -> principal에 유저정보를 담는다.
UserAuthentication authentication = new UserAuthentication(memberId.toString(), null, null);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception exception) {
try {
throw new Exception();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 다음 필터로 요청 전달
filterChain.doFilter(request, response);
}

private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring("Bearer ".length());
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package secondSeminar.secondSeminar.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {

private static final String MEMBER_ID = "memberId";

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

@PostConstruct
protected void init() {
//base64 라이브러리에서 encodeToString을 이용해서 byte[] 형식을 String 형식으로 변환
JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8));
}

public String generateToken(Authentication authentication, Long tokenExpirationTime) {
final Date now = new Date();
final Claims claims = Jwts.claims()
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenExpirationTime)); // 만료 시간

claims.put(MEMBER_ID, authentication.getPrincipal());

return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // Header
.setClaims(claims) // Claim
.signWith(getSigningKey()) // Signature
.compact();
}

private SecretKey getSigningKey() {
String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성
return Keys.hmacShaKeyFor(
encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용
}

public JwtValidationType validateToken(String token) {
try {
final Claims claims = getBody(token);
return JwtValidationType.VALID_JWT;
} catch (MalformedJwtException ex) {
return JwtValidationType.INVALID_JWT_TOKEN;
} catch (ExpiredJwtException ex) {
return JwtValidationType.EXPIRED_JWT_TOKEN;
} catch (UnsupportedJwtException ex) {
return JwtValidationType.UNSUPPORTED_JWT_TOKEN;
} catch (IllegalArgumentException ex) {
return JwtValidationType.EMPTY_JWT;
}
}

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

public Long getUserFromJwt(String token) {
Claims claims = getBody(token);
return Long.valueOf(claims.get(MEMBER_ID).toString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package secondSeminar.secondSeminar.config;

public enum JwtValidationType {
VALID_JWT, // 유효한 JWT
INVALID_JWT_SIGNATURE, // 유효하지 않은 서명
INVALID_JWT_TOKEN, // 유효하지 않은 토큰
EXPIRED_JWT_TOKEN, // 만료된 토큰
UNSUPPORTED_JWT_TOKEN, // 지원하지 않는 형식의 토큰
EMPTY_JWT // 빈 JWT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package secondSeminar.secondSeminar.config;

import static org.springframework.security.config.http.SessionCreationPolicy.STATELESS;

import jakarta.servlet.Filter;
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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;


private static final String[] AUTH_WHITELIST = {
"/sign-up",
"/sign-in"
};
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.sessionManagement()
.sessionCreationPolicy(STATELESS)
.and()
.exceptionHandling()
.authenticationEntryPoint(customJwtAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
.and()
.authorizeHttpRequests()
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}

@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedOriginPatterns("*")
.allowedMethods("*");
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package secondSeminar.secondSeminar.config;

import java.util.Collection;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

public class UserAuthentication extends UsernamePasswordAuthenticationToken {

// 사용자 인증 객체 생성
public UserAuthentication(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(principal, credentials, authorities);
}
}
Loading

0 comments on commit 6fb9394

Please sign in to comment.