Merge branch 'be/develop' of https://lab.ssafy.com/s11-s-project/S11P21S002 into be/feat/workspace
This commit is contained in:
commit
519a17c830
@ -25,10 +25,14 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Spring Boot
|
// 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'
|
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
|
// Lombok
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
|
|
||||||
@ -44,6 +48,14 @@ dependencies {
|
|||||||
// GJson
|
// GJson
|
||||||
implementation 'com.google.code.gson:gson:2.7'
|
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
|
//Swagger
|
||||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
|
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2'
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
}
|
@ -38,6 +38,18 @@ public class Member extends BaseEntity {
|
|||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
private ProviderType provider = ProviderType.GOOGLE;
|
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)
|
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY,cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
private List<Comment> commentList = new ArrayList<>();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
@ -11,6 +11,8 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
|
|||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestControllerAdvice
|
@RestControllerAdvice
|
||||||
@ -29,11 +31,11 @@ public class CustomControllerAdvice {
|
|||||||
return ErrorResponse.of(new CustomException(ErrorCode.BAD_REQUEST));
|
return ErrorResponse.of(new CustomException(ErrorCode.BAD_REQUEST));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(CustomException.class)
|
@ExceptionHandler(NoResourceFoundException.class)
|
||||||
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
|
public ErrorResponse handleNoHandlerFoundException(NoResourceFoundException e) {
|
||||||
log.error("", e);
|
log.error("", e);
|
||||||
return ResponseEntity.status(e.getErrorCode().getStatus())
|
return ErrorResponse.of(new CustomException(ErrorCode.INVALID_URL));
|
||||||
.body(ErrorResponse.of(e));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler({MissingServletRequestParameterException.class})
|
@ExceptionHandler({MissingServletRequestParameterException.class})
|
||||||
@ -41,4 +43,11 @@ public class CustomControllerAdvice {
|
|||||||
log.error("",e);
|
log.error("",e);
|
||||||
return ErrorResponse.of(new CustomException(ErrorCode.EMPTY_REQUEST_PARAMETER));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ public enum ErrorCode {
|
|||||||
EMPTY_FILE(HttpStatus.BAD_REQUEST, 1001, "빈 파일입니다."),
|
EMPTY_FILE(HttpStatus.BAD_REQUEST, 1001, "빈 파일입니다."),
|
||||||
BAD_REQUEST(HttpStatus.BAD_REQUEST, 1002, "잘못된 요청입니다. 요청을 확인해주세요."),
|
BAD_REQUEST(HttpStatus.BAD_REQUEST, 1002, "잘못된 요청입니다. 요청을 확인해주세요."),
|
||||||
EMPTY_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, 1003, "필수 요청 파라미터가 입력되지 않았습니다."),
|
EMPTY_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST, 1003, "필수 요청 파라미터가 입력되지 않았습니다."),
|
||||||
|
INVALID_URL(HttpStatus.BAD_REQUEST, 1004, "제공하지 않는 주소입니다. 확인해주세요"),
|
||||||
|
|
||||||
// Auth & User - 2000
|
// Auth & User - 2000
|
||||||
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
|
USER_NOT_FOUND(HttpStatus.NOT_FOUND, 2000, "해당 ID의 사용자를 찾을 수 없습니다."),
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
51
backend/src/main/java/com/worlabel/global/util/JWTUtil.java
Normal file
51
backend/src/main/java/com/worlabel/global/util/JWTUtil.java
Normal 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
0
backend/src/main/resources/.gitkeep
Normal file
0
backend/src/main/resources/.gitkeep
Normal file
53
backend/src/main/resources/application.yml
Normal file
53
backend/src/main/resources/application.yml
Normal 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
|
Loading…
Reference in New Issue
Block a user