Merge branch 'be/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into be/feat/workspace

This commit is contained in:
kimtaesoo7 2024-08-28 09:17:49 +09:00
commit 519a17c830
23 changed files with 688 additions and 7 deletions

View File

@ -25,10 +25,14 @@ repositories {
dependencies {
// Spring Boot
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
// Lombok
compileOnly 'org.projectlombok:lombok'
@ -44,6 +48,14 @@ dependencies {
// GJson
implementation 'com.google.code.gson:gson:2.7'
// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
//Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
}

View File

@ -0,0 +1,22 @@
package com.worlabel.domain.auth.attribute;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
import java.util.Map;
@Getter
@ToString
@RequiredArgsConstructor
public abstract class OAuth2Attribute {
private final Map<String,Object> attributes;
public abstract String getId();
public abstract String getName();
public abstract String getEmail();
public abstract String getProfileImage();
}

View File

@ -0,0 +1,20 @@
package com.worlabel.domain.auth.attribute;
import com.worlabel.domain.auth.attribute.impl.GoogleAttribute;
import com.worlabel.domain.auth.entity.ProviderType;
import java.util.Map;
public class OAuth2AttributeFactory {
public static OAuth2Attribute parseAttribute(ProviderType provider, Map<String, Object> attributes){
OAuth2Attribute oAuth2Attribute = null;
switch (provider) {
case GOOGLE :
oAuth2Attribute = new GoogleAttribute(attributes);
break;
default:
throw new RuntimeException("지원하지 않는 소셜 로그인입니다.");
};
return oAuth2Attribute;
}
}

View File

@ -0,0 +1,34 @@
package com.worlabel.domain.auth.attribute.impl;
import ch.qos.logback.core.util.StringUtil;
import com.worlabel.domain.auth.attribute.OAuth2Attribute;
import org.springframework.util.StringUtils;
import java.util.Map;
public class GoogleAttribute extends OAuth2Attribute {
public GoogleAttribute(Map<String,Object> attributes) {
super(attributes);
}
@Override
public String getId() {
return super.getAttributes().get("sub").toString();
}
@Override
public String getName() {
return super.getAttributes().get("name").toString();
}
@Override
public String getEmail() {
return super.getAttributes().get("email").toString();
}
@Override
public String getProfileImage() {
return super.getAttributes().get("picture").toString();
}
}

View File

@ -0,0 +1,24 @@
package com.worlabel.domain.auth.controller;
import com.worlabel.domain.auth.dto.AuthMemberDto;
import com.worlabel.global.annotation.CurrentUser;
import com.worlabel.global.response.SuccessResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class AuthController {
// TODO: 리이슈 처리
@GetMapping("/user-info")
public SuccessResponse<AuthMemberDto> getMemberInfo(@CurrentUser AuthMemberDto currentMember){
return SuccessResponse.of(currentMember);
}
}

View File

@ -0,0 +1,22 @@
package com.worlabel.domain.auth.dto;
import com.worlabel.domain.member.entity.Member;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public class AuthMemberDto {
private int id;
private String email;
private String role;
public static AuthMemberDto of(Member member) {
AuthMemberDto authMemberDto = new AuthMemberDto();
authMemberDto.id = member.getId();
authMemberDto.email = member.getEmail();
authMemberDto.role = member.getRole().toString();
return authMemberDto;
}
}

View File

@ -0,0 +1,44 @@
package com.worlabel.domain.auth.entity;
import com.worlabel.domain.auth.dto.AuthMemberDto;
import com.worlabel.domain.member.entity.Member;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class CustomOAuth2Member implements OAuth2User {
@Getter
private final AuthMemberDto authMemberDto;
public CustomOAuth2Member(Member member) {
authMemberDto = AuthMemberDto.of(member);
}
@Override
public Map<String, Object> getAttributes() {
// OAuth2 제공자로부터 받은 사용자 속성 데이터를 반환합니다.
return Map.of(
"id", authMemberDto.getId(),
"email", authMemberDto.getEmail(),
"role", authMemberDto.getRole()
);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 사용자의 역할(RoleType) 권한으로 변환하여 반환합니다.
return List.of(new SimpleGrantedAuthority(authMemberDto.getRole()));
}
@Override
public String getName() {
// 사용자의 고유 식별자를 반환합니다. 여기서는 이메일을 사용합니다.
return authMemberDto.getEmail();
}
}

View File

@ -0,0 +1,47 @@
package com.worlabel.domain.auth.service;
import com.worlabel.domain.auth.attribute.OAuth2Attribute;
import com.worlabel.domain.auth.attribute.OAuth2AttributeFactory;
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
import com.worlabel.domain.auth.entity.ProviderType;
import com.worlabel.domain.member.entity.Member;
import com.worlabel.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
/**
* OAuth2 사용자 서비스 클래스
* OAuth2 인증을 통해 사용자 정보를 로드하고 처리하는 역할
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2MemberService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(userRequest);
log.debug("oAuth2User: {}", user.getAttributes());
ProviderType provider = ProviderType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase());
OAuth2Attribute attribute = OAuth2AttributeFactory.parseAttribute(provider, user.getAttributes());
log.debug("provider: {}, user: {}", provider, user);
Member member = memberRepository.findByProviderMemberId(attribute.getId())
.orElseGet(() -> {
Member newMember = Member.of(attribute.getId(), attribute.getEmail(), attribute.getName(), attribute.getProfileImage());
memberRepository.save(newMember);
return newMember;
});
log.debug("member : {}", member);
return new CustomOAuth2Member(member);
}
}

View File

@ -0,0 +1,9 @@
package com.worlabel.domain.auth.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class JwtTokenService {
}

View File

@ -38,6 +38,18 @@ public class Member extends BaseEntity {
@Enumerated(EnumType.STRING)
private ProviderType provider = ProviderType.GOOGLE;
/**
* 사용자 이메일
*/
@Column(name = "email",nullable = false, length = 40)
private String email;
/**
* 사용자 닉네임
*/
@Column(name = "nickname",nullable = false, length = 20)
private String nickname;
/**
* 사용자 역할
*/
@ -56,4 +68,13 @@ public class Member extends BaseEntity {
*/
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();
public static Member of(String providerMemberId, String email, String nickname, String profileImage) {
Member member = new Member();
member.providerMemberId = providerMemberId;
member.email = email;
member.nickname = nickname;
member.profileImage = profileImage;
return member;
}
}

View File

@ -0,0 +1,12 @@
package com.worlabel.domain.member.repository;
import com.worlabel.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface MemberRepository extends JpaRepository<Member, Integer> {
Optional<Member> findByProviderMemberId(String providerMemberId);
Optional<Member> findByEmail(String email);
}

View File

@ -11,6 +11,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@Slf4j
@RestControllerAdvice
@ -29,11 +31,11 @@ public class CustomControllerAdvice {
return ErrorResponse.of(new CustomException(ErrorCode.BAD_REQUEST));
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
@ExceptionHandler(NoResourceFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleNoHandlerFoundException(NoResourceFoundException e) {
log.error("", e);
return ResponseEntity.status(e.getErrorCode().getStatus())
.body(ErrorResponse.of(e));
return ErrorResponse.of(new CustomException(ErrorCode.INVALID_URL));
}
@ExceptionHandler({MissingServletRequestParameterException.class})
@ -41,4 +43,11 @@ public class CustomControllerAdvice {
log.error("",e);
return ErrorResponse.of(new CustomException(ErrorCode.EMPTY_REQUEST_PARAMETER));
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.error("", e);
return ResponseEntity.status(e.getErrorCode().getStatus())
.body(ErrorResponse.of(e));
}
}

View File

@ -0,0 +1,12 @@
package com.worlabel.global.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface CurrentUser {
}

View File

@ -0,0 +1,21 @@
package com.worlabel.global.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsMvcConfig implements WebMvcConfigurer {
@Value("${frontend.url}")
private String frontend;
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.exposedHeaders("Set-Cookie")
.allowedOrigins(frontend) // application.yml에서 가져온 사용
.allowCredentials(true);
}
}

View File

@ -0,0 +1,98 @@
package com.worlabel.global.config;
import com.worlabel.domain.auth.service.CustomOAuth2MemberService;
import com.worlabel.domain.member.repository.MemberRepository;
import com.worlabel.global.filter.JWTFilter;
import com.worlabel.global.handler.OAuth2SuccessHandler;
import com.worlabel.global.util.JWTUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Collections;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2MemberService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final JWTUtil jwtUtil;
@Value("${frontend.url}")
private String frontend;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, MemberRepository memberRepository) throws Exception {
// CSRF 비활성화
http
.csrf((auth) -> auth.disable());
// Form 로그인 방식 Disable
http
.formLogin((auth) -> auth.disable());
// HTTP Basic 인증 방식 disable
http.
httpBasic((auth) -> auth.disable());
// OAuth2
http
.oauth2Login(oauth2 -> oauth2
.authorizationEndpoint(authorizationEndpoint ->
authorizationEndpoint.baseUri("/api/oauth2/authorization"))
.redirectionEndpoint(redirectionEndpoint ->
redirectionEndpoint.baseUri("/api/login/oauth2/code/*"))
.userInfoEndpoint(userInfoEndpointConfig ->
userInfoEndpointConfig.userService(customOAuth2UserService))
.successHandler(oAuth2SuccessHandler)
);
// 경로별 인가 작업
http
.authorizeHttpRequests((auth)->auth
.requestMatchers("/favicon.ico").permitAll() // Allow access to favicon.ico
.requestMatchers("/").permitAll()
.anyRequest().authenticated());
// 세션 설정: STATELESS
http.sessionManagement((session)->session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// JWT 필터 추가
http
.addFilterAfter(new JWTFilter(jwtUtil, memberRepository), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Collections.singletonList(frontend)); // 프론트엔드 URL 사용
configuration.setAllowedMethods(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setMaxAge(3600L);
configuration.addExposedHeader("Authorization");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

View File

@ -0,0 +1,21 @@
package com.worlabel.global.config;
import com.worlabel.global.resolver.CurrentUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final CurrentUserArgumentResolver currentUserArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
}

View File

@ -13,7 +13,7 @@ public enum ErrorCode {
EMPTY_FILE(HttpStatus.BAD_REQUEST, 1001, "빈 파일입니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, 1002, "잘못된 요청입니다. 요청을 확인해주세요."),
EMPTY_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, 1003, "필수 요청 파라미터가 입력되지 않았습니다."),
INVALID_URL(HttpStatus.BAD_REQUEST, 1004, "제공하지 않는 주소입니다. 확인해주세요"),
// Auth & User - 2000
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),

View File

@ -0,0 +1,53 @@
package com.worlabel.global.filter;
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
import com.worlabel.domain.member.repository.MemberRepository;
import com.worlabel.global.util.JWTUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
@Slf4j
@RequiredArgsConstructor
public class JWTFilter extends OncePerRequestFilter {
private final JWTUtil jwtUtil;
private final MemberRepository memberRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Cookie[] cookies = request.getCookies();
String token = Arrays.stream(Optional.ofNullable(cookies).orElse(new Cookie[0]))
.filter(cookie -> "Authorization".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
if(token == null || jwtUtil.isExpired(token)){
log.debug("토큰 X");
filterChain.doFilter(request, response);
return;
}
String username = jwtUtil.getUsername(token);
memberRepository.findByEmail(username).ifPresent(member -> {
CustomOAuth2Member customOAuth2Member = new CustomOAuth2Member(member);
Authentication authToken = new UsernamePasswordAuthenticationToken(customOAuth2Member.getAuthMemberDto(), null, customOAuth2Member.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authToken);
});
filterChain.doFilter(request, response);
}
}

View File

@ -0,0 +1,64 @@
package com.worlabel.global.handler;
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
import com.worlabel.global.util.JWTUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
// TODO : 추후 도메인 혹은 배포시 설정
@Value("${frontend.url}")
private String frontEnd;
private final JWTUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
// OAuth2User
CustomOAuth2Member customOAuth2Member = (CustomOAuth2Member) authentication.getPrincipal();
log.debug("로그인 성공 : {}", customOAuth2Member);
String username = customOAuth2Member.getName();
String role = authentication.getAuthorities()
.stream()
.map(auth -> auth.getAuthority())
.toList()
.get(0);
// JWT 토큰 생성
String token = jwtUtil.createJwtToken(username, role, 60 * 60 * 1000L, "access");
// 쿠키에 JWT 토큰 추가
response.addCookie(createCookie("Authorization", token));
// 성공 리다이렉트할 URL 설정
// String redirectUrl = frontEnd + "/"; // 적절한 리다이렉트 URL로 수정
// getRedirectStrategy().sendRedirect(request, response, redirectUrl);
super.onAuthenticationSuccess(request, response, authentication);
}
private Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
// TODO: 추후 변경
cookie.setMaxAge(60 * 60 * 60); // 개발 단계에서는 유효기간 길게
cookie.setPath("/"); // 쿠키 경로를 전체 경로로 설정
cookie.setHttpOnly(true); // HttpOnly 설정, JavaScript 접근 불가
// cookie.setSecure(true); // TODO: 배포시 HTTPS 환경에서 사용
return cookie;
}
}

View File

@ -0,0 +1,32 @@
package com.worlabel.global.resolver;
import com.worlabel.domain.auth.dto.AuthMemberDto;
import com.worlabel.domain.auth.entity.CustomOAuth2Member;
import com.worlabel.global.annotation.CurrentUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
@Slf4j
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal instanceof AuthMemberDto){
return principal;
}
return null;
}
}

View File

@ -0,0 +1,51 @@
package com.worlabel.global.util;
import io.jsonwebtoken.Jwts;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Component
public class JWTUtil {
private final SecretKey secretKey;
public JWTUtil(@Value("${spring.jwt.secret}") String key) {
secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), Jwts.SIG.HS256.key().build().getAlgorithm());
}
public String getUsername(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class);
}
public String getRole(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class);
}
public boolean isExpired(String token) {
return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date());
}
/**
* AccessToken 생성
* @param username 사용자 아이디(이메일)
* @param role 사용자 권한
* @param expiredMs 토큰 만료시간
* @return AccessToken
*/
public String createJwtToken(String username, String role, Long expiredMs, String type) {
return Jwts.builder()
.claim("username", username)
.claim("role", role)
.claim("type", type)
.expiration(new Date(System.currentTimeMillis() + expiredMs))
.signWith(secretKey)
.compact();
}
}

View File

View File

@ -0,0 +1,53 @@
spring:
application:
name: worlabel
# MYSQL 데이터베이스 설정
datasource:
url: jdbc:mysql://localhost:3306/worlabel?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
# 하이버네이트 설정
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
# MultiPartFile MAXSIZE 설정(추후 구현)
servlet:
multipart:
# max-file-size: 10MB
# max-request-size: 10MB
# OAuth 설정
security:
oauth2:
client:
registration:
google:
client-name: google
client-id: 1044731352382-tphfhlmjit95c6k8ac5753e6g197acg2.apps.googleusercontent.com
client-secret: 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
redirect-uri: http://localhost:8080/api/login/oauth2/code/google
authorization-grant-type: authorization_code
scope:
- profile
- email
# JWT 설정
jwt:
secret: 9fsVIzHXKXAYkls8RaGmwex4PB7tNxqj
# 로깅 레벨 설정
logging:
level:
com.worlabel: DEBUG
# Frontend 도메인(추후 변경 예정)
frontend:
url: http://localhost:8080