Skip to content

Commit

Permalink
Merge pull request #27 from prgrms-web-devcourse-final-project/featur…
Browse files Browse the repository at this point in the history
…e/oauth-setting(WR9-24)

Feature/oauth-setting(wr9-24)
  • Loading branch information
leeys9423 authored Feb 19, 2025
2 parents 252e58d + 7466ddf commit ba66ff2
Show file tree
Hide file tree
Showing 28 changed files with 739 additions and 41 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ dependencies {
// Websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// Oauth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down Expand Up @@ -192,6 +195,7 @@ sonar {
property "sonar.tests", "src/test/java"
property "sonar.java.binaries", "${buildDir}/classes"
property "sonar.coverage.jacoco.xmlReportPaths", "${buildDir}/reports/jacoco/test/jacocoTestReport.xml"
property "sonar.security.hotspots.suppressions", "java:S4834" // CSRF 비활성화 경고 무시

property "sonar.coverage.exclusions", excludePatterns.collect {
it.replace('.class', '.java') // class 확장자를 java로 변경
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.crops.warmletter.domain.member.controller;

import io.crops.warmletter.domain.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/members")
@RequiredArgsConstructor
public class MemberController {

private final MemberService memberService;
}
57 changes: 57 additions & 0 deletions src/main/java/io/crops/warmletter/domain/member/entity/Member.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.crops.warmletter.domain.member.entity;

import io.crops.warmletter.domain.member.enums.Role;
import io.crops.warmletter.global.entity.BaseEntity;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@Entity
@Table(name = "members")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false, unique = true)
private String email;

@Column(unique = true)
private String zipCode;

private String password;

@Column(nullable = false)
private float temperature;

// @Enumerated(EnumType.STRING)
// private Category preferredLetterCategory

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private Role role;

private LocalDateTime lastMatchedAt;

@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<SocialAccount> socialAccounts = new ArrayList<>();

@Builder
public Member(String email, String zipCode, String password, float temperature, Role role, LocalDateTime lastMatchedAt) {
this.email = email;
this.zipCode = zipCode;
this.password = password;
this.temperature = temperature;
this.role = role;
this.lastMatchedAt = lastMatchedAt;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.crops.warmletter.domain.member.entity;

import io.crops.warmletter.domain.member.enums.SocialProvider;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@Table(name = "social_accounts")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class SocialAccount {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private SocialProvider provider;

@Column(nullable = false)
private String socialId; // 소셜 서비스의 고유 id

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;

@Builder
public SocialAccount(SocialProvider provider, String socialId) {
this.provider = provider;
this.socialId = socialId;
}

// 연관관계 편의 메서드
@SuppressWarnings("lombok")
public void setMember(Member member) {
this.member = member;
}
}
13 changes: 13 additions & 0 deletions src/main/java/io/crops/warmletter/domain/member/enums/Role.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.crops.warmletter.domain.member.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {
USER("ROLE_USER"),
ADMIN("ROLE_ADMIN");

private final String key;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.crops.warmletter.domain.member.enums;

public enum SocialProvider {
GOOGLE,
KAKAO,
NAVER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.crops.warmletter.domain.member.repository;

import io.crops.warmletter.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.crops.warmletter.domain.member.service;

import io.crops.warmletter.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MemberService {

private final MemberRepository memberRepository;
}
49 changes: 49 additions & 0 deletions src/main/java/io/crops/warmletter/global/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.crops.warmletter.global.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

// 허용할 Origin 설정
configuration.setAllowedOrigins(
List.of(
"http://localhost:3000", // 로컬 프론트엔드
"http://localhost:8080", // 개발 테스트
"https://your-domain.com" // 운영 프론트엔드
));

// 허용할 HTTP 메서드
configuration.setAllowedMethods(
List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));

// 허용할 헤더
configuration.setAllowedHeaders(
List.of("Authorization", "Content-Type", "Cache-Control", "x-requested-with"));

// 노출할 헤더 설정 추가
configuration.setExposedHeaders(List.of("Authorization"));

// 인증 정보 포함 허용
configuration.setAllowCredentials(true);

// preflight 요청의 캐시 시간 (1시간)
// API 요청 시 서버는 OPTIONS 메서드를 통해 제공하는 메서드인지를 먼저 체크한다.
// 캐싱을 통해 불필요한 preflight 요청을 줄여서 성능을 개선
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);

return source;
}
}
51 changes: 17 additions & 34 deletions src/main/java/io/crops/warmletter/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.crops.warmletter.global.config;

import java.util.List;

import io.crops.warmletter.global.oauth.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
Expand All @@ -19,9 +22,13 @@

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
@Profile("!test")
public class SecurityConfig {

private final CustomOAuth2UserService customOAuth2UserService;
private final CorsConfig corsConfig;

@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration)
throws Exception {
Expand All @@ -39,7 +46,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// CSRF 비활성화
.csrf(AbstractHttpConfigurer::disable)
// CORS 설정
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
// 세션 설정(비활성화)
.sessionManagement(
(sessionManagement) ->
Expand All @@ -58,41 +65,17 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.permitAll() // API Docs 허용
.anyRequest()
.authenticated() // 그 외 요청은 인증 필요
);
)
// OAuth2 설정 추가
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService))
// 나중에 Handler 구현 후 추가될 부분
// .successHandler(oAuth2AuthenticationSuccessHandler)
// .failureHandler(oAuth2AuthenticationFailureHandler)
);

return http.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

// 허용할 Origin 설정
configuration.setAllowedOrigins(
List.of(
"http://localhost:3000", // 로컬 프론트엔드
"https://your-domain.com" // 운영 프론트엔드
));

// 허용할 HTTP 메서드
configuration.setAllowedMethods(
List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));

// 허용할 헤더
configuration.setAllowedHeaders(
List.of("Authorization", "Content-Type", "Cache-Control", "x-requested-with"));

// 인증 정보 포함 허용
configuration.setAllowCredentials(true);

// preflight 요청의 캐시 시간 (1시간)
// API 요청 시 서버는 OPTIONS 메서드를 통해 제공하는 메서드인지를 먼저 체크한다.
// 캐싱을 통해 불필요한 preflight 요청을 줄여서 성능을 개선
configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);

return source;
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,61 @@
package io.crops.warmletter.global.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity
@Profile("test") // test 프로필에서만 적용되는 설정
@RequiredArgsConstructor
@Profile("test")
public class TestSecurityConfig {

private final CorsConfig corsConfig;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
auth -> auth.requestMatchers("/h2-console/**").permitAll()
// ... 다른 설정들
http
// CSRF 비활성화
.csrf(AbstractHttpConfigurer::disable)
// CORS 설정
.cors(cors -> cors.configurationSource(corsConfig.corsConfigurationSource()))
// 세션 설정(비활성화)
.sessionManagement(
sessionManagement -> sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS
)
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));
)
// 요청에 대한 권한 설정
.authorizeHttpRequests(
auth -> auth
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()
.anyRequest().permitAll()
)
.headers(headers -> headers
.frameOptions(frameOptions -> frameOptions.disable())
);

return http.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public enum ErrorCode {

//금치어
DUPLICATE_BANNED_WORD("MOD-001", HttpStatus.CONFLICT, "이미 등록된 금칙어입니다."),

// OAuth2 관련 에러 코드
UNSUPPORTED_SOCIAL_LOGIN("AUTH-001", HttpStatus.BAD_REQUEST, "지원하지 않는 소셜 로그인입니다."),
OAUTH2_PROCESSING_ERROR("AUTH-002", HttpStatus.INTERNAL_SERVER_ERROR, "소셜 로그인 처리 중 오류가 발생했습니다."),
OAUTH2_EMAIL_NOT_FOUND("AUTH-003", HttpStatus.BAD_REQUEST, "소셜 계정에서 이메일을 찾을 수 없습니다.")
;

private final String code;
Expand Down
Loading

0 comments on commit ba66ff2

Please sign in to comment.