Merge branch 'be/feat/login' into 'be/develop'
Be/feat/login See merge request s11-s-project/S11P21S002!5
This commit is contained in:
commit
c1baae651c
@ -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'
|
||||
}
|
||||
|
@ -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)
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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.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));
|
||||
}
|
||||
}
|
||||
|
@ -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, "빈 파일입니다."),
|
||||
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의 사용자를 찾을 수 없습니다."),
|
||||
|
@ -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