From 836166dc989a083d624c4c49d5c7399457139ef1 Mon Sep 17 00:00:00 2001 From: ChoiDongKuen Date: Sun, 7 Jan 2024 06:46:46 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20application.yml=20profile=20=EB=B3=84?= =?UTF-8?q?=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B8=B0=EB=B3=B8=20=EA=B5=AC=ED=98=84,?= =?UTF-8?q?=20JWT=20=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#30)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: spring security 관련 의존성 추가 * feat: spring oauth 2.0 관련 의존성 추가 * refactor: User 엔티티 관련 리팩토링 및 관련 클래스 리팩토링 * feat: UserAuthentication (인증 객체) 구현 * feat: SecurityUtil 구현 * feat: redis 의존성 추가 * feat: profile 분리 ( 개발, 운영, 테스트, 인증, 데이터소스, 레디스 ) * feat: io-netty-dns-native-macos 관련 의존성 추가 * feat: JWT 관련 설정값 & Redis 관련 설정값 주입 구현 * feat: application-redis.yml 구현 * feat: jwt 의존성 추가 (#17) * feat: jwtAuthenticationFilter 구현 및 관련 Service 구현 (#17) * feat: 응답을 위한 TokenResponse 구현 및 기타 구현 (#17) * fix: IntegrationTest 수정 (#17) * refactor: JwtAuthenticationFilter 불필요한 주석 제거 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * fix: PropertyTest 제거 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) * feat: securityConfig JwtAuthenticationFilter 적용 (#17) * fix: ci 에러 수정 (#17) * fix: ci 에러 수정 (#17) --- gradle/spring.gradle | 12 ++ src/main/java/net/teumteum/Application.java | 4 +- .../net/teumteum/core/config/AppConfig.java | 11 ++ .../teumteum/core/context/LoginContext.java | 4 +- .../core/context/LoginContextImpl.java | 11 +- .../teumteum/core/entity/TimeBaseEntity.java | 38 +++--- .../teumteum/core/property/JwtProperty.java | 32 +++++ .../teumteum/core/property/RedisProperty.java | 13 +++ .../teumteum/core/security/Authenticated.java | 6 + .../core/security/SecurityConfig.java | 60 ++++++++++ .../core/security/UserAuthentication.java | 48 ++++++++ .../core/security/dto/TokenResponse.java | 18 +++ .../filter/JwtAuthenticationFilter.java | 64 ++++++++++ .../core/security/service/AuthService.java | 19 +++ .../core/security/service/JwtService.java | 110 ++++++++++++++++++ .../core/security/service/RedisService.java | 43 +++++++ .../security/service/SecurityService.java | 38 ++++++ .../user/controller/UserController.java | 32 +++-- .../java/net/teumteum/user/domain/OAuth.java | 24 ++++ .../java/net/teumteum/user/domain/Oauth.java | 21 ---- .../net/teumteum/user/domain/RoleType.java | 8 ++ .../java/net/teumteum/user/domain/User.java | 27 ++--- .../teumteum/user/domain/UserConnector.java | 3 + .../domain/request/UserUpdateRequest.java | 15 ++- .../user/domain/response/UserGetResponse.java | 4 +- .../domain/response/UsersGetByIdResponse.java | 4 +- .../user/service/UserConnectorImpl.java | 8 +- src/main/resources/application-auth.yml | 16 +++ src/main/resources/application-datasource.yml | 14 +++ src/main/resources/application-dev.yml | 25 ++++ src/main/resources/application-prod.yml | 38 ++++++ src/main/resources/application-redis.yml | 10 ++ src/main/resources/application.properties | 27 ----- src/main/resources/application.yml | 12 ++ .../db/migration/V1__create_users.sql | 42 +++---- .../db/migration/V4__update_users.sql | 3 + .../java/net/teumteum/integration/Api.java | 71 +++++------ .../teumteum/integration/IntegrationTest.java | 14 ++- .../net/teumteum/integration/Repository.java | 52 +++++---- .../integration/SecurityContextSetting.java | 18 +++ .../integration/TestLoginContext.java | 8 +- .../integration/UserIntegrationTest.java | 28 ++--- .../net/teumteum/user/domain/UserFixture.java | 54 +++++---- .../user/domain/UserRepositoryTest.java | 14 ++- .../user/service/UserConnectorTest.java | 11 +- src/test/resources/application.properties | 26 ++--- src/test/resources/schema.sql | 5 +- 47 files changed, 896 insertions(+), 269 deletions(-) create mode 100644 src/main/java/net/teumteum/core/config/AppConfig.java create mode 100644 src/main/java/net/teumteum/core/property/JwtProperty.java create mode 100644 src/main/java/net/teumteum/core/property/RedisProperty.java create mode 100644 src/main/java/net/teumteum/core/security/Authenticated.java create mode 100644 src/main/java/net/teumteum/core/security/SecurityConfig.java create mode 100644 src/main/java/net/teumteum/core/security/UserAuthentication.java create mode 100644 src/main/java/net/teumteum/core/security/dto/TokenResponse.java create mode 100644 src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/net/teumteum/core/security/service/AuthService.java create mode 100644 src/main/java/net/teumteum/core/security/service/JwtService.java create mode 100644 src/main/java/net/teumteum/core/security/service/RedisService.java create mode 100644 src/main/java/net/teumteum/core/security/service/SecurityService.java create mode 100644 src/main/java/net/teumteum/user/domain/OAuth.java delete mode 100644 src/main/java/net/teumteum/user/domain/Oauth.java create mode 100644 src/main/java/net/teumteum/user/domain/RoleType.java create mode 100644 src/main/resources/application-auth.yml create mode 100644 src/main/resources/application-datasource.yml create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-prod.yml create mode 100644 src/main/resources/application-redis.yml delete mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/db/migration/V4__update_users.sql create mode 100644 src/test/java/net/teumteum/integration/SecurityContextSetting.java diff --git a/gradle/spring.gradle b/gradle/spring.gradle index 0702e629..81823276 100644 --- a/gradle/spring.gradle +++ b/gradle/spring.gradle @@ -13,12 +13,24 @@ allprojects { dependencies { implementation "org.springframework.boot:spring-boot-starter" implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-configuration-processor' + + runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.104.Final:osx-aarch_64' implementation "org.springframework.boot:spring-boot-starter-webflux" + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation "org.springframework.boot:spring-boot-starter-actuator" + implementation "org.springframework.boot:spring-boot-starter-security" + implementation "org.springframework.boot:spring-boot-starter-oauth2-client" + implementation 'io.jsonwebtoken:jjwt:0.9.1' + + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' testImplementation "org.springframework.boot:spring-boot-starter-test" + testImplementation 'org.springframework.security:spring-security-test' } } diff --git a/src/main/java/net/teumteum/Application.java b/src/main/java/net/teumteum/Application.java index 7f388f2d..55025096 100644 --- a/src/main/java/net/teumteum/Application.java +++ b/src/main/java/net/teumteum/Application.java @@ -2,12 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; -@EnableJpaAuditing + @SpringBootApplication public class Application { - public static void main(String[] args) { SpringApplication.run(Application.class, args); } diff --git a/src/main/java/net/teumteum/core/config/AppConfig.java b/src/main/java/net/teumteum/core/config/AppConfig.java new file mode 100644 index 00000000..475f8d89 --- /dev/null +++ b/src/main/java/net/teumteum/core/config/AppConfig.java @@ -0,0 +1,11 @@ +package net.teumteum.core.config; + +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +@ConfigurationPropertiesScan("net.teumteum.core.property") +public class AppConfig { +} diff --git a/src/main/java/net/teumteum/core/context/LoginContext.java b/src/main/java/net/teumteum/core/context/LoginContext.java index b8af79fd..5a5a9ee7 100644 --- a/src/main/java/net/teumteum/core/context/LoginContext.java +++ b/src/main/java/net/teumteum/core/context/LoginContext.java @@ -2,8 +2,8 @@ public interface LoginContext { - void setUserId(Long userId); - Long getUserId(); + void setUserId(Long userId); + } diff --git a/src/main/java/net/teumteum/core/context/LoginContextImpl.java b/src/main/java/net/teumteum/core/context/LoginContextImpl.java index 204d4c38..3867e1a4 100644 --- a/src/main/java/net/teumteum/core/context/LoginContextImpl.java +++ b/src/main/java/net/teumteum/core/context/LoginContextImpl.java @@ -12,14 +12,13 @@ public class LoginContextImpl implements LoginContext { private Long userId; - @Override - public void setUserId(Long userId) { - this.userId = userId; - } - @Override public Long getUserId() { return userId; } -} + @Override + public void setUserId(Long userId) { + this.userId = userId; + } +} \ No newline at end of file diff --git a/src/main/java/net/teumteum/core/entity/TimeBaseEntity.java b/src/main/java/net/teumteum/core/entity/TimeBaseEntity.java index 6df9785a..c074310f 100644 --- a/src/main/java/net/teumteum/core/entity/TimeBaseEntity.java +++ b/src/main/java/net/teumteum/core/entity/TimeBaseEntity.java @@ -1,35 +1,29 @@ package net.teumteum.core.entity; import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; -import jakarta.persistence.PrePersist; -import jakarta.persistence.PreUpdate; -import java.time.Instant; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.Instant; @Getter +@SuperBuilder @NoArgsConstructor @MappedSuperclass +@EntityListeners(AuditingEntityListener.class) public abstract class TimeBaseEntity { + @CreatedDate + @Column(name = "created_at", nullable = false, updatable = false) + private Instant createdAt; - @Column(name = "created_at", columnDefinition = "TIMESTAMP(6)", nullable = false, updatable = false) - protected Instant createdAt; - - @Column(name = "updated_at", columnDefinition = "TIMESTAMP(6)", nullable = false) - protected Instant updatedAt; - - @PrePersist - void prePersist() { - var now = Instant.now(); - - createdAt = createdAt != null ? createdAt : now; - updatedAt = updatedAt != null ? updatedAt : now; - } - - @PreUpdate - void preUpdate() { - updatedAt = updatedAt != null ? updatedAt : Instant.now(); - } - + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; } + diff --git a/src/main/java/net/teumteum/core/property/JwtProperty.java b/src/main/java/net/teumteum/core/property/JwtProperty.java new file mode 100644 index 00000000..71a5869c --- /dev/null +++ b/src/main/java/net/teumteum/core/property/JwtProperty.java @@ -0,0 +1,32 @@ +package net.teumteum.core.property; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "jwt") +public class JwtProperty { + + private String bearer; + private String secret; + private Access access; + private Refresh refresh; + + + @Getter + @Setter + public static class Access{ + private long expiration; + private String header; + + } + + @Getter + @Setter + public static class Refresh { + private long expiration; + private String header; + } +} diff --git a/src/main/java/net/teumteum/core/property/RedisProperty.java b/src/main/java/net/teumteum/core/property/RedisProperty.java new file mode 100644 index 00000000..fbc645fb --- /dev/null +++ b/src/main/java/net/teumteum/core/property/RedisProperty.java @@ -0,0 +1,13 @@ +package net.teumteum.core.property; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@Setter +@ConfigurationProperties(prefix = "data.redis") +public class RedisProperty { + private String host; + private int port; +} diff --git a/src/main/java/net/teumteum/core/security/Authenticated.java b/src/main/java/net/teumteum/core/security/Authenticated.java new file mode 100644 index 00000000..26ba3f84 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/Authenticated.java @@ -0,0 +1,6 @@ +package net.teumteum.core.security; + +/* 소셜 OAuth 로그인 타입 */ +public enum Authenticated { + 카카오,네이버; +} diff --git a/src/main/java/net/teumteum/core/security/SecurityConfig.java b/src/main/java/net/teumteum/core/security/SecurityConfig.java new file mode 100644 index 00000000..2c1369c7 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/SecurityConfig.java @@ -0,0 +1,60 @@ +package net.teumteum.core.security; + + +import lombok.RequiredArgsConstructor; +import net.teumteum.core.security.filter.JwtAuthenticationFilter; +import org.springframework.boot.autoconfigure.security.servlet.PathRequest; +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.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +public class SecurityConfig { + + private final JwtAuthenticationFilter jwtAuthenticationFilter; + + @Bean + SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { + MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector); + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(request + -> request.requestMatchers("/users").permitAll() + .requestMatchers(PathRequest.toH2Console()).permitAll() + .anyRequest().authenticated()) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .sessionManagement(sessionManagement + -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .cors(cors -> cors.configurationSource(this.corsConfigurationSource())); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + configuration.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/net/teumteum/core/security/UserAuthentication.java b/src/main/java/net/teumteum/core/security/UserAuthentication.java new file mode 100644 index 00000000..6568f3f2 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/UserAuthentication.java @@ -0,0 +1,48 @@ +package net.teumteum.core.security; + +import lombok.Getter; +import net.teumteum.user.domain.User; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class UserAuthentication extends AbstractAuthenticationToken { + + private final String oauthId; + private Long id; + + public UserAuthentication(User user) { + super(authorities(user)); + this.id = user.getId(); + this.oauthId = user.getOauth().getOauthId(); + } + + private static List authorities(User User) { + List authorities = new ArrayList<>(); + authorities.add(new SimpleGrantedAuthority(User.getRoleType().name())); + return authorities; + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return id; + } + + @Override + public boolean isAuthenticated() { + return true; + } + + public void setUserId(Long userId) { + id = userId; + } +} diff --git a/src/main/java/net/teumteum/core/security/dto/TokenResponse.java b/src/main/java/net/teumteum/core/security/dto/TokenResponse.java new file mode 100644 index 00000000..df88e24c --- /dev/null +++ b/src/main/java/net/teumteum/core/security/dto/TokenResponse.java @@ -0,0 +1,18 @@ +package net.teumteum.core.security.dto; + +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class TokenResponse { + private String accessToken; + private String refreshToken; + + @Builder + public TokenResponse(String accessToken, String refreshToken) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } +} diff --git a/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..068303e4 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/filter/JwtAuthenticationFilter.java @@ -0,0 +1,64 @@ +package net.teumteum.core.security.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.teumteum.core.property.JwtProperty; +import net.teumteum.core.security.UserAuthentication; +import net.teumteum.core.security.service.AuthService; +import net.teumteum.core.security.service.JwtService; +import net.teumteum.user.domain.User; +import org.springframework.security.authentication.InsufficientAuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + private final JwtService jwtService; + private final AuthService authService; + private final JwtProperty jwtProperty; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + /* Cors Preflight Request */ + if (request.getMethod().equals("OPTIONS")) { + return; + } + + try { + String token = this.resolveTokenFromRequest(request); + if (checkTokenExistenceAndValidation(token)) { + User user = this.authService.findUserByToken(token).get(); + UserAuthentication authentication = new UserAuthentication(user); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (InsufficientAuthenticationException e) { + log.info("JwtAuthentication UnauthorizedUserException!"); + } + filterChain.doFilter(request, response); + } + + private boolean checkTokenExistenceAndValidation(String token) { + return StringUtils.hasText(token) && this.jwtService.validateToken(token); + } + + private String resolveTokenFromRequest(HttpServletRequest request) { + String token = request.getHeader(jwtProperty.getAccess().getHeader()); + if (!ObjectUtils.isEmpty(token) && token.toLowerCase().startsWith(jwtProperty.getBearer().toLowerCase())) { + return token.substring(jwtProperty.getBearer().length()).trim(); + } + return null; + } +} diff --git a/src/main/java/net/teumteum/core/security/service/AuthService.java b/src/main/java/net/teumteum/core/security/service/AuthService.java new file mode 100644 index 00000000..df2b9a4a --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/AuthService.java @@ -0,0 +1,19 @@ +package net.teumteum.core.security.service; + +import lombok.RequiredArgsConstructor; +import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserConnector; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class AuthService { + private final JwtService jwtService; + private final UserConnector userConnector; + public Optional findUserByToken(String accessToken) { + Long id = Long.parseLong(jwtService.getUserIdFromToken(accessToken)); + return userConnector.findUserById(id); + } +} diff --git a/src/main/java/net/teumteum/core/security/service/JwtService.java b/src/main/java/net/teumteum/core/security/service/JwtService.java new file mode 100644 index 00000000..2ef8bd18 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/JwtService.java @@ -0,0 +1,110 @@ +package net.teumteum.core.security.service; + +import io.jsonwebtoken.*; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.teumteum.core.property.JwtProperty; +import net.teumteum.core.security.dto.TokenResponse; +import net.teumteum.user.domain.User; +import org.springframework.security.oauth2.jwt.JwtException; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +/* JWT 관련 모든 작업을 위한 Service */ +@Service +@RequiredArgsConstructor +@Slf4j +public class JwtService { + private final JwtProperty jwtProperty; + private final RedisService redisService; + + // HttpServletRequest 부터 Access Token 추출 + public Optional extractAccessToken(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(this.jwtProperty.getAccess().getHeader())) + .filter(StringUtils::hasText) + .filter(accessToken -> accessToken.startsWith(jwtProperty.getBearer())) + .map(accessToken -> accessToken.replace(jwtProperty.getBearer(), "")); + } + + // HttpServletRequest 부터 Refresh Token 추출 + public String extractRefreshToken(HttpServletRequest request) { + return request.getHeader(this.jwtProperty.getRefresh().getHeader()); + } + + // access token 생성 + public String createAccessToken(String payload) { + return this.createToken(payload, this.jwtProperty.getAccess().getExpiration()); + } + + + // refresh token 생성 + public String createRefreshToken() { + return this.createToken(UUID.randomUUID().toString(), this.jwtProperty.getRefresh().getExpiration()); + + } + + + // access token 으로부터 회원 아이디 추출 + public String getUserIdFromToken(String token) { + try { + return Jwts.parser() + .setSigningKey(this.jwtProperty.getSecret()) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } catch (Exception exception) { + throw new JwtException("Access Token is not valid"); + } + } + + // kakao oauth 로그인 & 일반 로그인 시 jwt 응답 생성 + redis refresh 저장 + public TokenResponse createServiceToken(User users) { + String accessToken = this.createAccessToken(String.valueOf(users.getId())); + String refreshToken = this.createRefreshToken(); + + /* 서비스 토큰 생성 */ + TokenResponse userServiceTokenResponseDto = TokenResponse.builder() + .accessToken(this.jwtProperty.getBearer() + " " + accessToken) + .refreshToken(refreshToken) + .build(); + + /* redis refresh token 저장 */ + this.redisService.setDataExpire(String.valueOf(users.getId()), + userServiceTokenResponseDto.getRefreshToken(), this.jwtProperty.getRefresh().getExpiration()); + + return userServiceTokenResponseDto; + } + + // token 유효성 검증 + public boolean validateToken(String token) { + try { + Jws claimsJws = Jwts.parser().setSigningKey(this.jwtProperty.getSecret()).parseClaimsJws(token); + return !claimsJws.getBody().getExpiration().before(new Date()); + } catch (ExpiredJwtException exception) { + log.warn("만료된 jwt 입니다."); + } catch (UnsupportedJwtException exception) { + log.warn("지원되지 않는 jwt 입니다."); + } catch (IllegalArgumentException exception) { + log.warn("jwt 에 오류가 존재합니다."); + } + return false; + } + + // 실제 token 생성 로직 + private String createToken(String payload, Long tokenExpiration) { + Claims claims = Jwts.claims().setSubject(payload); + Date tokenExpiresIn = new Date(new Date().getTime() + tokenExpiration); + + return Jwts.builder() + .setClaims(claims) + .setIssuedAt(new Date()) + .setExpiration(tokenExpiresIn) + .signWith(SignatureAlgorithm.HS512, this.jwtProperty.getSecret()) + .compact(); + } +} diff --git a/src/main/java/net/teumteum/core/security/service/RedisService.java b/src/main/java/net/teumteum/core/security/service/RedisService.java new file mode 100644 index 00000000..3b2e5183 --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/RedisService.java @@ -0,0 +1,43 @@ +package net.teumteum.core.security.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.stereotype.Service; + +import java.time.Duration; + +/* Redis 관련 작업을 위한 서비스 */ +@Service +@RequiredArgsConstructor +public class RedisService { + private final StringRedisTemplate stringRedisTemplate; + + /* key 에 해당하는 데이터 얻어오는 메소드 */ + public String getData(String key) { + ValueOperations valueOperations = getStringStringValueOperations(); + return valueOperations.get(key); + } + + /* key - value 데이터 설정하는 메소드 */ + public void setData(String key, String value) { + ValueOperations valueOperations = getStringStringValueOperations(); + valueOperations.set(key, value); + } + + /* key 에 해당하는 데이터 삭제하는 메소드 */ + public void deleteData(String key) { + this.stringRedisTemplate.delete(key); + } + + /* key 에 해당하는 데이터 만료기간 설정 메소드 */ + public void setDataExpire(String key, String value, Long duration) { + ValueOperations valueOperations = getStringStringValueOperations(); + Duration expireDuration = Duration.ofSeconds(duration); + valueOperations.set(key, value, expireDuration); + } + + private ValueOperations getStringStringValueOperations() { + return this.stringRedisTemplate.opsForValue(); + } +} diff --git a/src/main/java/net/teumteum/core/security/service/SecurityService.java b/src/main/java/net/teumteum/core/security/service/SecurityService.java new file mode 100644 index 00000000..3cf572bd --- /dev/null +++ b/src/main/java/net/teumteum/core/security/service/SecurityService.java @@ -0,0 +1,38 @@ +package net.teumteum.core.security.service; + +import lombok.RequiredArgsConstructor; +import net.teumteum.core.security.UserAuthentication; +import net.teumteum.user.domain.UserConnector; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SecurityService { + + private final UserConnector userConnector; + + 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(); + } + + + public String getCurrentUserOAuthId() { + UserAuthentication userAuthentication = getUserAuthentication(); + return userAuthentication.getOauthId(); + } + + public void setUserId(Long userId) { + UserAuthentication userAuthentication = getUserAuthentication(); + userAuthentication.setUserId(userId); + } +} diff --git a/src/main/java/net/teumteum/user/controller/UserController.java b/src/main/java/net/teumteum/user/controller/UserController.java index 89f28b84..0c21dc52 100644 --- a/src/main/java/net/teumteum/user/controller/UserController.java +++ b/src/main/java/net/teumteum/user/controller/UserController.java @@ -1,33 +1,27 @@ package net.teumteum.user.controller; -import java.util.Arrays; import lombok.RequiredArgsConstructor; -import net.teumteum.core.context.LoginContext; import net.teumteum.core.error.ErrorResponse; +import net.teumteum.core.security.service.SecurityService; import net.teumteum.user.domain.request.UserUpdateRequest; import net.teumteum.user.domain.response.FriendsResponse; import net.teumteum.user.domain.response.UserGetResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; import net.teumteum.user.service.UserService; +import org.springframework.context.ApplicationContext; import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import java.util.Arrays; @RestController @RequiredArgsConstructor @RequestMapping("/users") public class UserController { + private final ApplicationContext applicationContext; private final UserService userService; - private final LoginContext loginContext; + private final SecurityService securityService; @GetMapping("/{userId}") @ResponseStatus(HttpStatus.OK) @@ -39,8 +33,8 @@ public UserGetResponse getUserById(@PathVariable("userId") Long userId) { @ResponseStatus(HttpStatus.OK) public UsersGetByIdResponse getUsersById(@RequestParam("id") String userIds) { var parsedUserIds = Arrays.stream(userIds.split(",")) - .map(Long::valueOf) - .toList(); + .map(Long::valueOf) + .toList(); return userService.getUsersById(parsedUserIds); } @@ -48,13 +42,13 @@ public UsersGetByIdResponse getUsersById(@RequestParam("id") String userIds) { @PutMapping @ResponseStatus(HttpStatus.OK) public void updateUser(@RequestBody UserUpdateRequest request) { - userService.updateUser(loginContext.getUserId(), request); + userService.updateUser(getCurrentUserId(), request); } @PostMapping("/{friendId}/friends") @ResponseStatus(HttpStatus.OK) public void addFriend(@PathVariable("friendId") Long friendId) { - userService.addFriends(loginContext.getUserId(), friendId); + userService.addFriends(getCurrentUserId(), friendId); } @GetMapping("/{userId}/friends") @@ -68,4 +62,8 @@ public FriendsResponse findFriends(@PathVariable("userId") Long userId) { public ErrorResponse handleIllegalArgumentException(IllegalArgumentException illegalArgumentException) { return ErrorResponse.of(illegalArgumentException); } + + private Long getCurrentUserId() { + return securityService.getCurrentUserId(); + } } diff --git a/src/main/java/net/teumteum/user/domain/OAuth.java b/src/main/java/net/teumteum/user/domain/OAuth.java new file mode 100644 index 00000000..0ccbbd38 --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/OAuth.java @@ -0,0 +1,24 @@ +package net.teumteum.user.domain; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import net.teumteum.core.security.Authenticated; + +@Getter +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class OAuth { + + @Column(name = "oauth_id", unique = true, nullable = false) + private String oauthId; + + @Enumerated(EnumType.STRING) + @Column(name = "authenticated", nullable = false) + private Authenticated authenticated; +} diff --git a/src/main/java/net/teumteum/user/domain/Oauth.java b/src/main/java/net/teumteum/user/domain/Oauth.java deleted file mode 100644 index 6305689d..00000000 --- a/src/main/java/net/teumteum/user/domain/Oauth.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.teumteum.user.domain; - -import jakarta.persistence.Column; -import jakarta.persistence.Embeddable; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@Embeddable -@NoArgsConstructor -@AllArgsConstructor -public class Oauth { - - @Column(name = "oauth_authenticate_info", unique = true) - private String oAuthAuthenticateInfo; - - @Column(name = "authenticated") - private String authenticated; - -} diff --git a/src/main/java/net/teumteum/user/domain/RoleType.java b/src/main/java/net/teumteum/user/domain/RoleType.java new file mode 100644 index 00000000..3b1d700e --- /dev/null +++ b/src/main/java/net/teumteum/user/domain/RoleType.java @@ -0,0 +1,8 @@ +package net.teumteum.user.domain; + +import lombok.Getter; + +@Getter +public enum RoleType { + ROLE_USER, ROLE_ADMIN; +} diff --git a/src/main/java/net/teumteum/user/domain/User.java b/src/main/java/net/teumteum/user/domain/User.java index 83a11e17..ed220211 100644 --- a/src/main/java/net/teumteum/user/domain/User.java +++ b/src/main/java/net/teumteum/user/domain/User.java @@ -1,20 +1,6 @@ package net.teumteum.user.domain; -import jakarta.persistence.Column; -import jakarta.persistence.ElementCollection; -import jakarta.persistence.Embedded; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.FetchType; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.PrePersist; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @@ -22,6 +8,11 @@ import org.springframework.format.annotation.DateTimeFormat; import org.springframework.util.Assert; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + @Getter @Entity(name = "users") @NoArgsConstructor @@ -47,7 +38,11 @@ public class User extends TimeBaseEntity { private int mannerTemperature; @Embedded - private Oauth oauth; + private OAuth oauth; + + @Enumerated(EnumType.STRING) + @Column(name = "role_type") + private RoleType roleType; @Embedded private ActivityArea activityArea; diff --git a/src/main/java/net/teumteum/user/domain/UserConnector.java b/src/main/java/net/teumteum/user/domain/UserConnector.java index 295ee545..f3ad2620 100644 --- a/src/main/java/net/teumteum/user/domain/UserConnector.java +++ b/src/main/java/net/teumteum/user/domain/UserConnector.java @@ -1,8 +1,11 @@ package net.teumteum.user.domain; +import java.util.List; import java.util.Optional; public interface UserConnector { Optional findUserById(Long id); + + List findAllUser(); } diff --git a/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java b/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java index b4d120cd..e029d3ab 100644 --- a/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java +++ b/src/main/java/net/teumteum/user/domain/request/UserUpdateRequest.java @@ -3,12 +3,10 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; import java.util.Set; -import net.teumteum.user.domain.ActivityArea; -import net.teumteum.user.domain.Job; -import net.teumteum.user.domain.JobStatus; -import net.teumteum.user.domain.Oauth; -import net.teumteum.user.domain.Terms; -import net.teumteum.user.domain.User; + +import net.teumteum.user.domain.*; + +import static net.teumteum.user.domain.RoleType.ROLE_USER; public record UserUpdateRequest( Long id, @@ -25,7 +23,7 @@ public record UserUpdateRequest( private static final Long IGNORE_ID = null; private static final int IGNORE_MANNER_TEMPERATURE = -1; - private static final Oauth IGNORE_OAUTH = null; + private static final OAuth IGNORE_O_AUTH = null; private static final boolean NOT_CERTIFICATED = false; private static final Terms IGNORE_TERMS = null; private static final Set IGNORE_FRIENDS = Set.of(); @@ -37,7 +35,8 @@ public User toUser() { newBirth, newCharacterId, IGNORE_MANNER_TEMPERATURE, - IGNORE_OAUTH, + IGNORE_O_AUTH, + ROLE_USER, new ActivityArea( newActivityArea.city, newActivityArea.streets diff --git a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java index 06369c23..ccf69252 100644 --- a/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UserGetResponse.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; + +import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.User; public record UserGetResponse( @@ -10,7 +12,7 @@ public record UserGetResponse( String birth, Long characterId, int mannerTemperature, - String authenticated, + Authenticated authenticated, ActivityArea activityArea, String mbti, String status, diff --git a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java index cf7a2b5d..a21841e7 100644 --- a/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java +++ b/src/main/java/net/teumteum/user/domain/response/UsersGetByIdResponse.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; + +import net.teumteum.core.security.Authenticated; import net.teumteum.user.domain.User; public record UsersGetByIdResponse( @@ -21,7 +23,7 @@ public record UserGetResponse( String birth, Long characterId, int mannerTemperature, - String authenticated, + Authenticated authenticated, ActivityArea activityArea, String mbti, String status, diff --git a/src/main/java/net/teumteum/user/service/UserConnectorImpl.java b/src/main/java/net/teumteum/user/service/UserConnectorImpl.java index 93fca5b5..e0704157 100644 --- a/src/main/java/net/teumteum/user/service/UserConnectorImpl.java +++ b/src/main/java/net/teumteum/user/service/UserConnectorImpl.java @@ -1,6 +1,5 @@ package net.teumteum.user.service; -import java.util.Optional; import lombok.RequiredArgsConstructor; import net.teumteum.user.domain.User; import net.teumteum.user.domain.UserConnector; @@ -8,6 +7,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; + @Service @RequiredArgsConstructor @Transactional(readOnly = true) @@ -20,4 +22,8 @@ public Optional findUserById(Long id) { return userRepository.findById(id); } + @Override + public List findAllUser() { + return userRepository.findAll(); + } } diff --git a/src/main/resources/application-auth.yml b/src/main/resources/application-auth.yml new file mode 100644 index 00000000..755a4f90 --- /dev/null +++ b/src/main/resources/application-auth.yml @@ -0,0 +1,16 @@ +## AUTHENTICATION & AUTHORIZATION +spring: + config: + activate: + on-profile: "auth" + +## JWT +jwt: + bearer: Bearer + secret: ${JWT_SECRET_KEY} + access: + expiration: ${JWT_ACCESS_EXPIRATION:3600000} + header: Authorization + refresh: + expiration: ${JWT_REFRESH_EXPIRATION:1209600000} + header: Authorization-refresh \ No newline at end of file diff --git a/src/main/resources/application-datasource.yml b/src/main/resources/application-datasource.yml new file mode 100644 index 00000000..8b90b347 --- /dev/null +++ b/src/main/resources/application-datasource.yml @@ -0,0 +1,14 @@ +## DATASOURCE +spring: + config: + activate: + on-profile: "datasource" + + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DATABASE_HOST:localhost}:${DATABASE_PORT}/${DATABASE_NAME}?&characterEncoding=UTF-8&serverTimezone=Asia/Seoul&autoReconnect=true + username: ${DATABASE_USERNAME} + password: ${DATABASE_PASSWORD} + hikari: + connection-timeout: 3000 + maximum-pool-size: 80 diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 00000000..64f158e6 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,25 @@ +## DEVELOPMENT +spring: + config: + activate: + on-profile: "dev" + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + +### JPA ### + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 + + +## LOGGING +logging: + level: + org.hibernate.SQL: debug + org.hibernate.type: trace \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 00000000..aa5b7d62 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,38 @@ +## PRODUCTION +spring: + config: + activate: + on-profile: "prod" + + mvc: + pathmatch: + matching-strategy: ant_path_matcher + +### JPA ### + jpa: + hibernate: + ddl-auto: validate + properties: + hibernate: + format_sql: true + default_batch_fetch_size: 100 + +### FLYWAY ### + flyway: + url: + user: + password: + baseline-on-migrate: true + +### ACTUATOR ### +management: + endpoints: + web: + exposure: + include: prometheus + +## LOGGING +logging: + level: + org.hibernate.SQL: info + org.hibernate.type: info diff --git a/src/main/resources/application-redis.yml b/src/main/resources/application-redis.yml new file mode 100644 index 00000000..489a229c --- /dev/null +++ b/src/main/resources/application-redis.yml @@ -0,0 +1,10 @@ +### REDIS ### +spring: + config: + activate: + on-profile: "redis" + data: + redis: + host: ${REDIS_HOST:localhost} + port: ${REDIS_PORT:6379} + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index fa12529b..00000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1,27 +0,0 @@ -spring.profiles.active=prod - -### SERVER CONFIG ### -server.port=8080 -server.name=teum-teum-server -spring.application.name=teum-teum-server - -### JPA ### -spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.datasource.url= -spring.datasource.username= -spring.datasource.password= -spring.datasource.hikari.connection-timeout=3000 -spring.datasource.hikari.maximum-pool-size=80 -spring.jpa.hibernate.ddl-auto=validate -spring.jpa.hibernate.show-sql=false -spring.jpa.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect - -### FLYWAY ### -spring.flyway.url= -spring.flyway.user= -spring.flyway.password= -spring.flyway.baseline-on-migrate=true - -### ACTUATOR ### -management.endpoints.web.exposure.include=prometheus -management.metrics.tags.application=${spring.application.name} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 00000000..64a6d1ad --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,12 @@ +server: + port: ${APPLICATION_PORT:8080} + +spring: + application: + name: teum-teum-server + + profiles: + group: + "dev": "dev, auth, datasource, redis" + "prod": "prod, auth, datasource, redis" +# "test": diff --git a/src/main/resources/db/migration/V1__create_users.sql b/src/main/resources/db/migration/V1__create_users.sql index 08626b0e..e7ad413c 100644 --- a/src/main/resources/db/migration/V1__create_users.sql +++ b/src/main/resources/db/migration/V1__create_users.sql @@ -1,25 +1,25 @@ create table if not exists users( - id bigint not null auto_increment, - certificated boolean, - manner_temperature integer, - mbti varchar(4), - character_id bigint, - birth varchar(10), - name varchar(10), - goal varchar(50), - authenticated varchar(255), - oauth_authenticate_info varchar(255) unique, - city varchar(255), - detail_job_class varchar(255), - job_class varchar(255), - job_name varchar(255), - status enum('직장인','학생','취업준비생'), - terms_of_service boolean not null, - privacy_policy boolean not null, - created_at timestamp(6) not null, - updated_at timestamp(6) not null, - primary key (id) -); + id bigint not null auto_increment, + certificated boolean, + manner_temperature integer, + mbti varchar(4), + character_id bigint, + birth varchar(10), + name varchar(10), + goal varchar(50), + authenticated varchar(255) not null, + oauth_authenticate_info varchar(255) unique, + city varchar(255), + detail_job_class varchar(255), + job_class varchar(255), + job_name varchar(255), + status enum('직장인','학생','취업준비생'), + terms_of_service boolean not null, + privacy_policy boolean not null, + created_at timestamp(6) not null, + updated_at timestamp(6) not null, + primary key (id) + ); create table if not exists users_interests( users_id bigint not null, diff --git a/src/main/resources/db/migration/V4__update_users.sql b/src/main/resources/db/migration/V4__update_users.sql new file mode 100644 index 00000000..f9314f2a --- /dev/null +++ b/src/main/resources/db/migration/V4__update_users.sql @@ -0,0 +1,3 @@ +alter table users drop column oauth_authenticate_info; +alter table users add column oauth_id varchar(255) not null unique; +alter table users add column role_type varchar(255); diff --git a/src/test/java/net/teumteum/integration/Api.java b/src/test/java/net/teumteum/integration/Api.java index 8c0a22b9..798c6f31 100644 --- a/src/test/java/net/teumteum/integration/Api.java +++ b/src/test/java/net/teumteum/integration/Api.java @@ -7,49 +7,55 @@ import org.springframework.context.ApplicationContext; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; +import org.springframework.security.test.context.support.WithMockUser; import org.springframework.stereotype.Controller; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec; +@WithMockUser(username = "user", roles = {"USER"}) @TestComponent class Api { private final WebTestClient webTestClient; + public Api(ApplicationContext applicationContext) { var controllers = applicationContext.getBeansWithAnnotation(Controller.class).values(); webTestClient = WebTestClient.bindToController(controllers.toArray()) - .argumentResolvers(resolvers -> resolvers.addCustomResolver(new PageableHandlerMethodArgumentResolver())) - .build(); + .argumentResolvers(resolvers -> resolvers.addCustomResolver(new PageableHandlerMethodArgumentResolver())) + .build(); } + ResponseSpec getUser(String token, Long userId) { - return webTestClient.get() - .uri("/users/" + userId) - .header(HttpHeaders.AUTHORIZATION, token) - .exchange(); + return webTestClient + .get() + .uri("/users/" + userId) + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); } ResponseSpec getUsersById(String token, String userIds) { return webTestClient.get() - .uri("/users?id=" + userIds) - .header(HttpHeaders.AUTHORIZATION, token) - .exchange(); + .uri("/users?id=" + userIds) + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); } ResponseSpec updateUser(String token, UserUpdateRequest userUpdateRequest) { - return webTestClient.put() - .uri("/users") - .header(HttpHeaders.AUTHORIZATION, token) - .bodyValue(userUpdateRequest) - .exchange(); + return webTestClient + .put() + .uri("/users") + .header(HttpHeaders.AUTHORIZATION, token) + .bodyValue(userUpdateRequest) + .exchange(); } ResponseSpec addFriends(String token, Long friendId) { return webTestClient.post() - .uri("/users/" + friendId + "/friends") - .header(HttpHeaders.AUTHORIZATION, token) - .exchange(); + .uri("/users/" + friendId + "/friends") + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); } ResponseSpec getFriendsByUserId(String token, Long userId) { @@ -61,32 +67,31 @@ ResponseSpec getFriendsByUserId(String token, Long userId) { ResponseSpec getOpenMeetings(String token, Long cursorId, int size) { return webTestClient.get() - .uri("/meetings" + - "?cursorId=" + cursorId + - "&size=" + size) - .header(HttpHeaders.AUTHORIZATION, token) - .exchange(); + .uri("/meetings" + + "?cursorId=" + cursorId + + "&size=" + size) + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); } ResponseSpec getMeetingById(String token, Long meetingId) { return webTestClient.get() - .uri("/meetings/" + meetingId) - .header(HttpHeaders.AUTHORIZATION, token) - .exchange(); + .uri("/meetings/" + meetingId) + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); } ResponseSpec getMeetingsByTopic(String token, Pageable pageable, boolean isOpen, Topic topic) { String sort = pageable.getSort().toString().replace(": ", ","); String uri = "/meetings?sort=" + sort + - "&page=" + pageable.getOffset() + - "&size=" + pageable.getPageSize() + - "&isOpen=" + isOpen + - "&topic=" + topic; + "&page=" + pageable.getOffset() + + "&size=" + pageable.getPageSize() + + "&isOpen=" + isOpen + + "&topic=" + topic; return webTestClient.get() - .uri(uri) - .header(HttpHeaders.AUTHORIZATION, token) - .exchange(); + .uri(uri) + .header(HttpHeaders.AUTHORIZATION, token) + .exchange(); } - } diff --git a/src/test/java/net/teumteum/integration/IntegrationTest.java b/src/test/java/net/teumteum/integration/IntegrationTest.java index b4d56f5c..cfa7d63a 100644 --- a/src/test/java/net/teumteum/integration/IntegrationTest.java +++ b/src/test/java/net/teumteum/integration/IntegrationTest.java @@ -1,16 +1,17 @@ package net.teumteum.integration; import net.teumteum.Application; -import net.teumteum.core.context.LoginContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.test.context.ContextConfiguration; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -@ContextConfiguration(classes = {Application.class, Api.class, Repository.class, TestLoginContext.class}) +@AutoConfigureWebTestClient(timeout = "10000") +@ContextConfiguration(classes = {Application.class, Api.class, Repository.class, SecurityContextSetting.class, TestLoginContext.class}) abstract public class IntegrationTest { @Autowired @@ -20,7 +21,10 @@ abstract public class IntegrationTest { protected Repository repository; @Autowired - protected LoginContext loginContext; + protected SecurityContextSetting securityContextSetting; + + @Autowired + protected TestLoginContext loginContext; @AfterEach @BeforeEach @@ -28,4 +32,8 @@ void clearAll() { repository.clear(); } + @BeforeEach + void setSecurityContextSetting() { + securityContextSetting.set(); + } } diff --git a/src/test/java/net/teumteum/integration/Repository.java b/src/test/java/net/teumteum/integration/Repository.java index b0d06494..b53418ca 100644 --- a/src/test/java/net/teumteum/integration/Repository.java +++ b/src/test/java/net/teumteum/integration/Repository.java @@ -1,8 +1,8 @@ package net.teumteum.integration; -import java.util.List; -import java.util.stream.Stream; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; +import net.teumteum.core.config.AppConfig; import net.teumteum.meeting.domain.Meeting; import net.teumteum.meeting.domain.MeetingFixture; import net.teumteum.meeting.domain.MeetingRepository; @@ -11,19 +11,30 @@ import net.teumteum.user.domain.UserFixture; import net.teumteum.user.domain.UserRepository; import org.springframework.boot.test.context.TestComponent; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.stream.Stream; @TestComponent +@Import(AppConfig.class) @RequiredArgsConstructor class Repository { private final UserRepository userRepository; private final MeetingRepository meetingRepository; + private final EntityManager entityManager; User saveAndGetUser() { var user = UserFixture.getNullIdUser(); return userRepository.saveAndFlush(user); } + List getAllUser() { + return userRepository.findAll(); + } + + Meeting saveAndGetOpenMeeting() { var meeting = MeetingFixture.getOpenMeeting(); return meetingRepository.saveAndFlush(meeting); @@ -31,65 +42,65 @@ Meeting saveAndGetOpenMeeting() { List saveAndGetOpenMeetingsByTopic(int size, Topic topic) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTopic(topic)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsTopic(int size, Topic topic) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithTopic(topic)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetingsByTitle(int size, String title) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithTitle(title)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsByTitle(int size, String title) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithTitle(title)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetingsByIntroduction(int size, String introduction) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithIntroduction(introduction)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsByIntroduction(int size, String introduction) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithIntroduction(introduction)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetingsByParticipantUserId(int size, Long participantUserId) { var meetings = Stream.generate(() -> MeetingFixture.getOpenMeetingWithParticipantUserId(participantUserId)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetCloseMeetingsByParticipantUserId(int size, Long participantUserId) { var meetings = Stream.generate(() -> MeetingFixture.getCloseMeetingWithParticipantUserId(participantUserId)) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } List saveAndGetOpenMeetings(int size) { var meetings = Stream.generate(MeetingFixture::getOpenMeeting) - .limit(size) - .toList(); + .limit(size) + .toList(); return meetingRepository.saveAllAndFlush(meetings); } @@ -97,5 +108,4 @@ void clear() { userRepository.deleteAll(); meetingRepository.deleteAll(); } - } diff --git a/src/test/java/net/teumteum/integration/SecurityContextSetting.java b/src/test/java/net/teumteum/integration/SecurityContextSetting.java new file mode 100644 index 00000000..a90a09f9 --- /dev/null +++ b/src/test/java/net/teumteum/integration/SecurityContextSetting.java @@ -0,0 +1,18 @@ +package net.teumteum.integration; + +import net.teumteum.core.security.UserAuthentication; +import net.teumteum.user.domain.User; +import net.teumteum.user.domain.UserFixture; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +@TestComponent +public class SecurityContextSetting { + public void set() { + User user = UserFixture.getIdUser(); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(new UserAuthentication(user)); + SecurityContextHolder.setContext(context); + } +} diff --git a/src/test/java/net/teumteum/integration/TestLoginContext.java b/src/test/java/net/teumteum/integration/TestLoginContext.java index 62d8c866..d184c563 100644 --- a/src/test/java/net/teumteum/integration/TestLoginContext.java +++ b/src/test/java/net/teumteum/integration/TestLoginContext.java @@ -9,12 +9,12 @@ public class TestLoginContext implements LoginContext { private Long userId; @Override - public void setUserId(Long userId) { - this.userId = userId; + public Long getUserId() { + return this.userId; } @Override - public Long getUserId() { - return this.userId; + public void setUserId(Long userId) { + this.userId = userId; } } diff --git a/src/test/java/net/teumteum/integration/UserIntegrationTest.java b/src/test/java/net/teumteum/integration/UserIntegrationTest.java index b136783d..f19f93fb 100644 --- a/src/test/java/net/teumteum/integration/UserIntegrationTest.java +++ b/src/test/java/net/teumteum/integration/UserIntegrationTest.java @@ -1,7 +1,7 @@ package net.teumteum.integration; -import java.util.List; import net.teumteum.core.error.ErrorResponse; +import net.teumteum.user.domain.User; import net.teumteum.user.domain.response.FriendsResponse; import net.teumteum.user.domain.response.UserGetResponse; import net.teumteum.user.domain.response.UsersGetByIdResponse; @@ -10,12 +10,15 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.List; + @DisplayName("유저 통합테스트의") class UserIntegrationTest extends IntegrationTest { private static final String VALID_TOKEN = "VALID_TOKEN"; private static final String INVALID_TOKEN = "IN_VALID_TOKEN"; + @Nested @DisplayName("유저 조회 API는") class Find_user_api { @@ -32,11 +35,11 @@ void Return_user_info_if_exist_user_id_received() { // then Assertions.assertThat( - result.expectStatus().isOk() - .expectBody(UserGetResponse.class) - .returnResult().getResponseBody()) - .usingRecursiveComparison() - .isEqualTo(expected); + result.expectStatus().isOk() + .expectBody(UserGetResponse.class) + .returnResult().getResponseBody()) + .usingRecursiveComparison() + .isEqualTo(expected); } @Test @@ -50,7 +53,7 @@ void Return_400_bad_request_if_not_exists_user_id_received() { // then result.expectStatus().isBadRequest() - .expectBody(ErrorResponse.class); + .expectBody(ErrorResponse.class); } } @@ -72,9 +75,9 @@ void Return_user_info_if_exist_user_ids_received() { // then Assertions.assertThat(result.expectStatus().isOk() - .expectBody(UsersGetByIdResponse.class) - .returnResult() - .getResponseBody() + .expectBody(UsersGetByIdResponse.class) + .returnResult() + .getResponseBody() ).usingRecursiveComparison().isEqualTo(expected); } @@ -112,10 +115,9 @@ class Update_user_api { void Update_user_info() { // given var existUser = repository.saveAndGetUser(); + List allUser = repository.getAllUser(); var updateUser = RequestFixture.userUpdateRequest(existUser); - loginContext.setUserId(existUser.getId()); - // when var result = api.updateUser(VALID_TOKEN, updateUser); @@ -136,8 +138,6 @@ void Return_200_ok_with_success_make_friends() { var myToken = "JWT MY_TOKEN"; var friend = repository.saveAndGetUser(); - loginContext.setUserId(me.getId()); - // when var result = api.addFriends(myToken, friend.getId()); diff --git a/src/test/java/net/teumteum/user/domain/UserFixture.java b/src/test/java/net/teumteum/user/domain/UserFixture.java index b463beaa..5365cb14 100644 --- a/src/test/java/net/teumteum/user/domain/UserFixture.java +++ b/src/test/java/net/teumteum/user/domain/UserFixture.java @@ -1,22 +1,31 @@ package net.teumteum.user.domain; +import lombok.Builder; + import java.util.List; import java.util.Set; import java.util.UUID; -import lombok.Builder; + +import static net.teumteum.core.security.Authenticated.네이버; public class UserFixture { public static User getNullIdUser() { return newUserByBuilder(UserBuilder.builder() - .id(null) - .build()); + .id(null) + .build()); + } + + public static User getIdUser() { + return newUserByBuilder(UserBuilder.builder() + .id(1L) + .build()); } public static User getUserWithId(Long id) { return newUserByBuilder(UserBuilder.builder() - .id(id) - .build()); + .id(id) + .build()); } public static User getDefaultUser() { @@ -25,20 +34,21 @@ public static User getDefaultUser() { public static User newUserByBuilder(UserBuilder userBuilder) { return new User( - userBuilder.id, - userBuilder.name, - userBuilder.birth, - userBuilder.characterId, - userBuilder.mannerTemperature, - userBuilder.oauth, - userBuilder.activityArea, - userBuilder.mbti, - userBuilder.status, - userBuilder.goal, - userBuilder.job, - userBuilder.interests, - userBuilder.terms, - Set.of() + userBuilder.id, + userBuilder.name, + userBuilder.birth, + userBuilder.characterId, + userBuilder.mannerTemperature, + userBuilder.oauth, + userBuilder.roleType, + userBuilder.activityArea, + userBuilder.mbti, + userBuilder.status, + userBuilder.goal, + userBuilder.job, + userBuilder.interests, + userBuilder.terms, + Set.of() ); } @@ -56,7 +66,9 @@ public static class UserBuilder { @Builder.Default private int mannerTemperature = 36; @Builder.Default - private Oauth oauth = new Oauth(UUID.randomUUID().toString(), "naver"); + private OAuth oauth = new OAuth(UUID.randomUUID().toString(), 네이버); + @Builder.Default + private RoleType roleType = RoleType.ROLE_USER; @Builder.Default private ActivityArea activityArea = new ActivityArea("서울", List.of("강남", "홍대")); @Builder.Default @@ -69,7 +81,7 @@ public static class UserBuilder { private Job job = new Job("netflix", true, "developer", "backend"); @Builder.Default private List interests = List.of( - "game", "sleep", "Eating delicious food" + "game", "sleep", "Eating delicious food" ); @Builder.Default private Terms terms = new Terms(true, true); diff --git a/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java b/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java index 19ad6119..f786240b 100644 --- a/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java +++ b/src/test/java/net/teumteum/user/domain/UserRepositoryTest.java @@ -1,15 +1,19 @@ package net.teumteum.user.domain; import jakarta.persistence.EntityManager; -import java.util.Optional; +import net.teumteum.core.config.AppConfig; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; @DataJpaTest +@Import(AppConfig.class) @DisplayName("UserRepository 클래스의") class UserRepositoryTest { @@ -56,10 +60,10 @@ void Find_success_if_exists_user_id_input() { // then Assertions.assertThat(result) - .isPresent() - .usingRecursiveComparison() - .ignoringFields("value.createdAt", "value.updatedAt") - .isEqualTo(Optional.of(existsUser)); + .isPresent() + .usingRecursiveComparison() + .ignoringFields("value.createdAt", "value.updatedAt") + .isEqualTo(Optional.of(existsUser)); } } diff --git a/src/test/java/net/teumteum/user/service/UserConnectorTest.java b/src/test/java/net/teumteum/user/service/UserConnectorTest.java index 09730adc..f3676f34 100644 --- a/src/test/java/net/teumteum/user/service/UserConnectorTest.java +++ b/src/test/java/net/teumteum/user/service/UserConnectorTest.java @@ -1,6 +1,5 @@ package net.teumteum.user.service; -import java.util.Optional; import net.teumteum.user.domain.UserConnector; import net.teumteum.user.domain.UserFixture; import net.teumteum.user.domain.UserRepository; @@ -16,6 +15,8 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import java.util.Optional; + @ExtendWith(SpringExtension.class) @DisplayName("UserConnector 클래스의") @ContextConfiguration(classes = UserConnectorImpl.class) @@ -50,10 +51,10 @@ void Return_optional_user_if_exists_user_id() { // then Assertions.assertThat(result) - .isPresent() - .usingRecursiveComparison() - .ignoringFields("value.oauth.oAuthAuthenticateInfo") - .isEqualTo(expect); + .isPresent() + .usingRecursiveComparison() + .ignoringFields("value.oauth.oauthId") + .isEqualTo(expect); } } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index bab49cd6..7702ec21 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,18 +1,14 @@ spring.profiles.active=test - -spring.datasource.driver-class-name = org.h2.Driver -spring.datasource.url = jdbc:h2:mem:test;MODE=MySQL;DATABASE_TO_LOWER=TRUE - -spring.jpa.hibernated.ddl-auto = validate -spring.jpa.database-platform = org.hibernate.dialect.MySQLDialect - -spring.datasource.hikari.maximum-pool-size = 4 -spring.datasource.hikari.pool-name = H2_TEST_POOL - +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL;DATABASE_TO_LOWER=TRUE +spring.jpa.hibernated.ddl-auto=validate +spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect +spring.datasource.hikari.maximum-pool-size=4 +spring.datasource.hikari.pool-name=H2_TEST_POOL ### FOR DEBUGGING ### -logging.level.org.hibernate.SQL = debug -logging.level.org.hibernate.type.descriptor.sql = trace +logging.level.org.hibernate.SQL=debug +logging.level.org.hibernate.type.descriptor.sql=trace +spring.jpa.properties.hibernate.format_sql=true +spring.jpa.properties.hibernate.highlight_sql=true +spring.jpa.properties.hibernate.use_sql_comments=true -spring.jpa.properties.hibernate.format_sql = true -spring.jpa.properties.hibernate.highlight_sql = true -spring.jpa.properties.hibernate.use_sql_comments = true diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql index 9ef35d51..bb71abc2 100644 --- a/src/test/resources/schema.sql +++ b/src/test/resources/schema.sql @@ -8,8 +8,9 @@ create table if not exists users birth varchar(10), name varchar(10), goal varchar(50), - authenticated varchar(255), - oauth_authenticate_info varchar(255) unique, + oauth_id varchar(255) not null unique, + authenticated varchar(255) not null, + role_type varchar(255), city varchar(255), detail_job_class varchar(255), job_class varchar(255),