Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ dependencies {
// Spring Security + OAuth
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework:spring-webflux'
implementation 'org.springframework.boot:spring-boot-starter-web'


// AWS
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.661'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import Journey.Together.domain.member.dto.MemberRes;
import Journey.Together.domain.member.service.MemberService;
import Journey.Together.global.common.ApiResponse;
import Journey.Together.global.config.PublicEndpoint;
import Journey.Together.global.exception.Success;
import Journey.Together.global.security.PrincipalDetails;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -15,6 +16,7 @@

@RestController
@RequiredArgsConstructor
@PublicEndpoint
@RequestMapping("/v1/member")
@Tag(name = "Member", description = "사용자 관련 API")
public class MemberController {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import Journey.Together.domain.place.service.PlaceService;
import Journey.Together.domain.place.service.PublicDataService;
import Journey.Together.global.common.ApiResponse;
import Journey.Together.global.config.PublicEndpoint;
import Journey.Together.global.exception.ApplicationException;
import Journey.Together.global.exception.ErrorCode;
import Journey.Together.global.exception.Success;
Expand Down Expand Up @@ -42,6 +43,7 @@ public class PlaceController {
private final PlaceService placeService;
private final DataMigrationService dataMigrationService;

@PublicEndpoint
@GetMapping("/main")
public ApiResponse<MainRes> getMain(
@RequestParam String areacode, @RequestParam String sigungucode) {
Expand All @@ -54,6 +56,7 @@ public ApiResponse<PlaceDetailRes> getPlaceDetail(@AuthenticationPrincipal Princ
return ApiResponse.success(Success.GET_PLACE_DETAIL_SUCCESS, placeService.getPlaceDetail(principalDetails.getMember(), placeId));
}

@PublicEndpoint
@GetMapping("guest/{placeId}")
public ApiResponse<PlaceDetailGuestRes> getPlaceDetail(@PathVariable Long placeId){
return ApiResponse.success(Success.GET_PLACE_DETAIL_SUCCESS, placeService.getGeustPlaceDetail(placeId));
Expand All @@ -75,6 +78,8 @@ public ApiResponse<PlaceReviewRes> getPlaceReview(@AuthenticationPrincipal Princ
return ApiResponse.success(Success.GET_PLACE_REVIEW_LIST_SUCCESS, placeService.getReviews(principalDetails.getMember(), placeId, pageable));
}


@PublicEndpoint
@GetMapping("/review/guest/{placeId}")
public ApiResponse<PlaceReviewRes> getPlaceReview(
@PathVariable Long placeId, @PageableDefault(size = 5,page = 0) Pageable pageable) {
Expand Down Expand Up @@ -110,6 +115,7 @@ public ApiResponse<?> updatePlaceMyReview(
placeService.updateMyPlaceReview(principalDetails.getMember(),updateReviewDto,addImages,reviewId);
return ApiResponse.success(Success.UPDATE_MY_PLACE_REVIEW_SUCCESS);
}
@PublicEndpoint
@GetMapping("/search")
public ApiResponse<SearchPlaceRes> searchPlaceList(
@RequestParam(required = false) String category,
Expand All @@ -123,6 +129,7 @@ public ApiResponse<SearchPlaceRes> searchPlaceList(
return ApiResponse.success(Success.SEARCH_PLACE_LIST_SUCCESS, placeService.searchPlaceList(category,query,disabilityType,detailFilter,areacode,sigungucode,arrange,pageable));
}

@PublicEndpoint
@GetMapping("/search/map")
public ApiResponse<List<PlaceRes>> searchPlaceList(
@RequestParam(required = false) String category,
Expand All @@ -136,6 +143,7 @@ public ApiResponse<List<PlaceRes>> searchPlaceList(
return ApiResponse.success(Success.SEARCH_PLACE_LIST_SUCCESS, placeService.searchPlaceMap(category,disabilityType,detailFilter,arrange,minX,maxX,minY,maxY));
}

@PublicEndpoint
@GetMapping("/search/autocomplete")
public ApiResponse<List<Map<String,Object>>> searchPlaceComplete(
@RequestParam String query
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import Journey.Together.domain.plan.service.query.PlanQueryService;
import Journey.Together.domain.plan.service.query.PlanReviewQueryService;
import Journey.Together.global.common.ApiResponse;
import Journey.Together.global.config.PublicEndpoint;
import Journey.Together.global.exception.Success;
import Journey.Together.global.security.PrincipalDetails;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand Down Expand Up @@ -58,6 +59,7 @@ public ApiResponse updatePlanIsPublic(@AuthenticationPrincipal PrincipalDetails
return ApiResponse.success(Success.UPDATE_PLAN_SUCCESS,planService.updatePlanIsPublic(principalDetails.getMember(),planId));
}

@PublicEndpoint
@GetMapping("/search")
public ApiResponse<PlaceInfoPageRes> searchPlace(@RequestParam String word, @PageableDefault(size = 6,page = 0) Pageable pageable){
return ApiResponse.success(Success.SEARCH_SUCCESS,placeQueryService.searchPlace(word,pageable));
Expand Down Expand Up @@ -86,11 +88,13 @@ public ApiResponse deletePlanReview(@AuthenticationPrincipal PrincipalDetails pr
return ApiResponse.success(Success.DELETE_PLAN_REVIEW_SUCCESS);
}

@PublicEndpoint
@GetMapping("/guest/review/{plan_id}")
public ApiResponse<PlanReviewRes> findPlanReviewGuest(@PathVariable("plan_id")Long planId){
return ApiResponse.success(Success.GET_REVIEW_SUCCESS,planReviewQueryService.getReview(null,planId));
}

@PublicEndpoint
@GetMapping("/open")
public ApiResponse<OpenPlanPageRes> findOpenPlans(@PageableDefault(size = 6) Pageable pageable){
return ApiResponse.success(Success.SEARCH_SUCCESS,planQueryService.findOpenPlans(pageable));
Expand All @@ -101,6 +105,7 @@ public ApiResponse<PlanDetailRes> findPalnDetailInfo(@AuthenticationPrincipal Pr
return ApiResponse.success(Success.SEARCH_SUCCESS,planService.findPlanDetail(principalDetails.getMember(),planId));
}

@PublicEndpoint
@GetMapping("/guest/detail/{plan_id}")
public ApiResponse<PlanDetailRes> findPalnDetailInfo(@PathVariable("plan_id")Long planId){
return ApiResponse.success(Success.SEARCH_SUCCESS,planService.findPlanDetail(null,planId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import Journey.Together.domain.report.enumerate.ReviewType;
import Journey.Together.domain.report.service.ReportService;
import Journey.Together.global.common.ApiResponse;
import Journey.Together.global.config.PublicEndpoint;
import Journey.Together.global.exception.Success;
import Journey.Together.global.security.PrincipalDetails;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -18,6 +19,7 @@
@RestController
@RequiredArgsConstructor
@RequestMapping("/v1/report")
@PublicEndpoint
@Tag(name = "Report", description = "신고하기 관련 API")
public class ReportController {
private final ReportService reportService;
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/Journey/Together/global/config/PublicEndpoint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package Journey.Together.global.config;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PublicEndpoint {
}
93 changes: 62 additions & 31 deletions src/main/java/Journey/Together/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import Journey.Together.global.security.jwt.JwtAuthenticationEntryPoint;
import Journey.Together.global.security.jwt.JwtFilter;
import lombok.RequiredArgsConstructor;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
Expand All @@ -19,67 +21,83 @@
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtFilter jwtFilter;
private final ExceptionFilter exceptionFilter;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final RequestMappingHandlerMapping handlerMapping;

public SecurityConfig(
JwtFilter jwtFilter,
ExceptionFilter exceptionFilter,
JwtAccessDeniedHandler jwtAccessDeniedHandler,
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint,
@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping handlerMapping
) {
this.jwtFilter = jwtFilter;
this.exceptionFilter = exceptionFilter;
this.jwtAccessDeniedHandler = jwtAccessDeniedHandler;
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.handlerMapping = handlerMapping;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// CORS 허용, CSRF 비활성화
http.cors(Customizer.withDefaults())
.csrf(AbstractHttpConfigurer::disable);
.csrf(AbstractHttpConfigurer::disable);

http.sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));// Session 미사용
http.sessionManagement(
(session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));// Session 미사용

// httpBasic, httpFormLogin 비활성화
http.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable);
.formLogin(AbstractHttpConfigurer::disable);

// JWT 관련 필터 설정 및 예외 처리
http.exceptionHandling((exceptionHandling) ->
exceptionHandling
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
exceptionHandling
.accessDeniedHandler(jwtAccessDeniedHandler)
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
);
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(exceptionFilter, JwtFilter.class);

// 요청 URI별 권한 설정
http.authorizeHttpRequests((authorize) ->
// Swagger UI 외부 접속 허용
authorize.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
// 로그인 로직 접속 허용
.requestMatchers("/v1/auth/**", "/oauth2/**", "/login.html").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/v1/member/**").authenticated()
.requestMatchers("/v1/place/main").permitAll()
.requestMatchers("/v1/place/review/guest/**").permitAll()
.requestMatchers("/v1/plan/guest/**").permitAll()
.requestMatchers("/v1/plan/open").permitAll()
.requestMatchers("/v1/plan/search").permitAll()
.requestMatchers("/v1/place/search").permitAll()
.requestMatchers("/v1/place/search/**").permitAll()
.requestMatchers("/v1/place/search/map").permitAll()
.requestMatchers("/v1/place/guest/**").permitAll()
.requestMatchers("/v1/report/**").permitAll()

// 메인 페이지, 공고 페이지 등에 한해 인증 정보 없이 접근 가능 (추후 추가)
// 이외의 모든 요청은 인증 정보 필요
.anyRequest().authenticated());
http.authorizeHttpRequests(authorize -> {
// Swagger 등 기본 허용
authorize.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll();
authorize.requestMatchers("/v1/auth/**", "/oauth2/**", "/login.html").permitAll();
authorize.requestMatchers("/actuator/**").permitAll();

// 동적으로 추출된 @PublicEndpoint 허용 처리
for (String pattern : extractPublicUrls()) {
authorize.requestMatchers(pattern).permitAll();
}

// 나머지 인증 필요
authorize.anyRequest().authenticated();
});



// OAuth2 로그인 설정
http.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/login-success")
.failureUrl("/login-failure"));
.defaultSuccessUrl("/login-success")
.failureUrl("/login-failure"));

return http.build();
}
Expand All @@ -105,8 +123,21 @@ CorsConfigurationSource corsConfigurationSource() {
}

@Bean
public PasswordEncoder passwordEncoder(){
public PasswordEncoder passwordEncoder() {
// 비밀번호 암호화
return new BCryptPasswordEncoder();
}

private Set<String> extractPublicUrls() {
return handlerMapping.getHandlerMethods().entrySet().stream()
.filter(entry -> {
Method method = entry.getValue().getMethod();
return method.isAnnotationPresent(PublicEndpoint.class)
|| method.getDeclaringClass().isAnnotationPresent(PublicEndpoint.class);
})
.map(entry -> entry.getKey().getPathPatternsCondition())
.filter(Objects::nonNull)
.flatMap(condition -> condition.getPatternValues().stream())
.collect(Collectors.toSet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
//jwt 유효성 검사를 하지않음
if ("/v1/auth/sign-in".equals(requestURI) || "/actuator/health".equals(requestURI)
|| "/v1/place/main".equals(requestURI)) {

filterChain.doFilter(request, response);
return;
}
Expand Down