Skip to content

Commit

Permalink
release: 0.0.8 (#85)
Browse files Browse the repository at this point in the history
* feat: 회원 로그아웃을 구현한다. (#80)

* feat: Test Container 의존성 추가 (#73)

* feat: RedisTestContainerConfig 구현 (#73)

* feat: IntegrationTest 적용 (#73)

* test: 회원 탈퇴 API 통합 테스트 진행 (with redis) (#73)

* fix: SonarCloud 에러 수정 (#73)

* fix: SonarCloud 에러 수정 (#73)

* feat: 통합 테스트 RedisService 의존성 추가 및 Repository Redis 관련 메소드 추가 (#73)

* refactor: SecurityService clearSecurityContext 메소드 추가 (#73)

* feat: 회원 로그아웃 API 구현 (#52)

* test: 회원 로그아웃 통합 테스트 구현 (#52)

* refactor: UserService 코드 리팩토링 (코드 리뷰 반영) (#52)

* feat: Security 관련 클래스 추가 구현 및 기타 코드 수정 (#84)

* feat: static 경로 설정 (#83)

* chore: jwt 관련 binding 의존성 추가 (#83)

* chore: 라이브러리 버전 추가 (#83)

* refactor: 인증 관련 코드 리팩토링 (#83)

* feat: ErrorResponse static 메서드 추가 (#83)

* feat: SecurityConfig 추가 구현 (#83)

* refactor: 기존 API & 코드 리팩토링 및 누락된 테스트 코드 구현 (#82)

* refactor: 유저 관련 API url 리팩토링 (#81)

* refactor: 기존 테스트 관련 클래스 리팩토링 (#81)

* refactor: 기존 코드 리팩토링 (#81)

* feat: Redis Test Container 구성 및 회원 탈퇴 테스트 진행 (#79)

* feat: Test Container 의존성 추가 (#73)

* feat: RedisTestContainerConfig 구현 (#73)

* feat: IntegrationTest 적용 (#73)

* test: 회원 탈퇴 API 통합 테스트 진행 (with redis) (#73)

* fix: SonarCloud 에러 수정 (#73)

* fix: SonarCloud 에러 수정 (#73)

* refactor: 버전 관리 분리 (코드 리뷰 반영) (#52)

* fix: CI Integration Tester 에러 수정 (#73)
  • Loading branch information
choidongkuen authored Jan 15, 2024
1 parent 8c97dce commit 3d1bd23
Show file tree
Hide file tree
Showing 19 changed files with 308 additions and 88 deletions.
6 changes: 6 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ projectVersion=1.0
### TEST ###
junitVersion=5.10.1
assertJVersion=3.24.2
testContainer=1.17.6
junitTestContainer=5.8.1
### LOMBOK ###
lombokVersion=1.18.30
### SPRING ###
Expand All @@ -19,3 +21,7 @@ mockWebServerVersion=4.12.0
sentryVersion=4.1.1
### AWS-CLOUD ###
springCloudAwsVersion=3.1.0
### JWT ###
jwtVersion=0.9.1
jwtBindingVersion=4.0.1
jwtJaxbApiVersion=2.3.1
9 changes: 8 additions & 1 deletion gradle/devtool.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ allprojects {
dependencies {
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok"
implementation 'io.jsonwebtoken:jjwt:0.9.1'

implementation "io.jsonwebtoken:jjwt:${jwtVersion}"

// com.sun.xml.bind
implementation "com.sun.xml.bind:jaxb-impl:${jwtBindingVersion}"
implementation "com.sun.xml.bind:jaxb-core:${jwtBindingVersion}"
// javax.xml.bind
implementation "javax.xml.bind:jaxb-api:${jwtJaxbApiVersion}"

testCompileOnly "org.projectlombok:lombok:${lombokVersion}"
testAnnotationProcessor "org.projectlombok:lombok"
Expand Down
5 changes: 5 additions & 0 deletions gradle/test.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ allprojects {
}

dependencies {
// test container for redis
testImplementation "org.junit.jupiter:junit-jupiter:${junitTestContainer}"
testImplementation "org.testcontainers:testcontainers:${testContainer}"
testImplementation "org.testcontainers:junit-jupiter:${testContainer}"

testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.auth.domain.response.TokenResponse;
import net.teumteum.auth.service.OAuthService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;

@Slf4j
@RequestMapping
@RequiredArgsConstructor
public class OAuthLoginController {

private final net.teumteum.auth.service.OAuthService oAuthService;
private final OAuthService oAuthService;

@GetMapping("/logins/callbacks/{provider}")
@ResponseStatus(HttpStatus.OK)
Expand Down
9 changes: 4 additions & 5 deletions src/main/java/net/teumteum/auth/service/OAuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,10 @@ private OAuthUserInfo getOAuthUserInfo(ClientRegistration clientRegistration, Au

private TokenResponse checkUserAndMakeResponse(OAuthUserInfo oAuthUserInfo, Authenticated authenticated) {
String oauthId = oAuthUserInfo.getOAuthId();
Optional<User> user = getUser(oauthId, authenticated);
if (user.isEmpty()) {
return new TokenResponse(oAuthUserInfo.getOAuthId());
}
return jwtService.createServiceToken(user.get());

return getUser(oauthId, authenticated)
.map(jwtService::createServiceToken)
.orElseGet(() -> new TokenResponse(oauthId));
}

private Map<String, Object> getOAuthAttribute(ClientRegistration clientRegistration, OAuthToken oAuthToken) {
Expand Down
5 changes: 4 additions & 1 deletion src/main/java/net/teumteum/core/error/ErrorResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;

@Getter
@NoArgsConstructor
Expand All @@ -16,4 +15,8 @@ public class ErrorResponse {
public static ErrorResponse of(Throwable exception) {
return new ErrorResponse(exception.getMessage());
}

public static ErrorResponse of(String message) {
return new ErrorResponse(message);
}
}
52 changes: 29 additions & 23 deletions src/main/java/net/teumteum/core/security/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@

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


import java.util.Collections;
import java.util.List;
import lombok.RequiredArgsConstructor;
import net.teumteum.core.security.filter.JwtAccessDeniedHandler;
import net.teumteum.core.security.filter.JwtAuthenticationEntryPoint;
import net.teumteum.core.security.filter.JwtAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
Expand All @@ -27,43 +28,48 @@
@RequiredArgsConstructor
public class SecurityConfig {

private static final String[] PATTERNS = {"/", "/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**",
"/docs/index.html", "/common/*.html", "/jwt-test", "/auth/**"};
private static final String[] PATTERNS = {"/css/**", "/images/**", "/js/**", "/favicon.ico", "/h2-console/**",
"/logins/**"};

private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler accessDeniedHandler;
private final JwtAuthenticationFilter jwtAuthenticationFilter;

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedHeaders(Collections.singletonList("*"));
config.setAllowedMethods(Collections.singletonList("*"));
config.setAllowedOriginPatterns(Collections.singletonList("/**")); // 허용할 origin
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring().requestMatchers("/h2-console/**");
return web -> web.ignoring()
.requestMatchers("/css/**", "/js/**", "/img/**", "/favicon.ico", "/error");
}

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
request -> request.requestMatchers("/**").permitAll()
.requestMatchers(PATTERNS).permitAll().anyRequest()
.authenticated()).httpBasic(AbstractHttpConfigurer::disable).formLogin(AbstractHttpConfigurer::disable)
http.csrf(AbstractHttpConfigurer::disable).cors(cors -> cors.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(request -> request.requestMatchers("/auth/**", "/logins/**").permitAll()
.requestMatchers(HttpMethod.POST, "/users/registers").permitAll().requestMatchers(PATTERNS).permitAll()
.anyRequest().authenticated()).httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.exceptionHandling(
exceptionHandling -> exceptionHandling.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedOrigin("https://api.teum.org");
config.addAllowedHeader("*");
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
config.addExposedHeader("Authorization");
config.addExposedHeader("Authorization-refresh");
config.setAllowCredentials(true);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
package net.teumteum.core.security.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import lombok.extern.slf4j.Slf4j;
import net.teumteum.core.error.ErrorResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

import static jakarta.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;

@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authenticationException
HttpServletResponse response,
AuthenticationException authenticationException
) throws IOException {
this.sendUnAuthenticatedError(response, authenticationException);
}

private void sendUnAuthenticatedError(HttpServletResponse response,
Exception exception) throws IOException {
Exception exception) throws IOException {
OutputStream os = response.getOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
log.error("Responding with unauthenticated error. Message - {}", exception.getMessage());
response.sendError(SC_UNAUTHORIZED, exception.getMessage());
objectMapper.writeValue(os, ErrorResponse.of("인증 과정에서 오류가 발생했습니다."));
os.flush();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@ public static void clearSecurityContext() {
SecurityContextHolder.clearContext();
}

private UserAuthentication getUserAuthentication() {
return (UserAuthentication) SecurityContextHolder.getContext().getAuthentication();
}


public Long getCurrentUserId() {
return getUserAuthentication() == null ? userConnector.findAllUser().get(0).getId()
: getUserAuthentication().getId();
Expand All @@ -36,4 +31,8 @@ public void setUserId(Long userId) {
UserAuthentication userAuthentication = getUserAuthentication();
userAuthentication.setUserId(userId);
}

private UserAuthentication getUserAuthentication() {
return (UserAuthentication) SecurityContextHolder.getContext().getAuthentication();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,23 @@ public InterestQuestionResponse getInterestQuestion(@RequestParam("user-id") Lis
return userService.getInterestQuestionByUserIds(userIds, balance);
}

@DeleteMapping("/withdraws")
@DeleteMapping
@ResponseStatus(HttpStatus.OK)
public void withdraw() {
userService.withdraw(getCurrentUserId());
}

@PostMapping("/registers")
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserRegisterResponse register(@RequestBody UserRegisterRequest request) {
return userService.register(request);
}

@PostMapping("/logouts")
@ResponseStatus(HttpStatus.OK)
public void logout() {
userService.logout(getCurrentUserId());
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/net/teumteum/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.RequiredArgsConstructor;
import net.teumteum.core.security.Authenticated;
import net.teumteum.core.security.service.RedisService;
import net.teumteum.core.security.service.SecurityService;
import net.teumteum.user.domain.BalanceGameType;
import net.teumteum.user.domain.InterestQuestion;
import net.teumteum.user.domain.User;
Expand Down Expand Up @@ -77,6 +78,13 @@ public UserRegisterResponse register(UserRegisterRequest request) {
return new UserRegisterResponse(userRepository.save(request.toUser()).getId());
}

@Transactional
public void logout(Long userId) {
getUser(userId);
redisService.deleteData(String.valueOf(userId));
SecurityService.clearSecurityContext();
}


public FriendsResponse findFriendsByUserId(Long userId) {
var user = getUser(userId);
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
static-path-pattern: /static/**

servlet:
multipart:
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ spring:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
static-path-pattern: /static/**


servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB

### JPA ###
### JPA ###
jpa:
hibernate:
ddl-auto: validate
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.teumteum.core.config;

import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

@Testcontainers
public class RedisTestContainerConfig implements BeforeAllCallback {

private static final String REDIS_IMAGE = "redis:7.0.8-alpine";
private static final int REDIS_PORT = 6379;

private GenericContainer redis;

@Override
public void beforeAll(ExtensionContext context) throws Exception {
redis = new GenericContainer(DockerImageName.parse(REDIS_IMAGE)).withExposedPorts(REDIS_PORT);

redis.start();
System.setProperty("spring.data.redis.host", redis.getHost());
System.setProperty("spring.data.redis.port", String.valueOf(redis.getMappedPort(REDIS_PORT)));
}
}
Loading

0 comments on commit 3d1bd23

Please sign in to comment.