From e85ce81ddbd77c3bdecf0bd09540f2bec200c285 Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Sun, 11 May 2025 21:38:35 +0900 Subject: [PATCH 1/2] =?UTF-8?q?SecurityConfig=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20=EB=B0=8F=20=EA=B0=80?= =?UTF-8?q?=EB=8F=85=EC=84=B1=20=ED=96=A5=EC=83=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back/zo6/auth/config/SecurityConfig.java | 99 +++++++++++-------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/src/main/java/goorm/back/zo6/auth/config/SecurityConfig.java b/src/main/java/goorm/back/zo6/auth/config/SecurityConfig.java index c7ef8d5..09859af 100644 --- a/src/main/java/goorm/back/zo6/auth/config/SecurityConfig.java +++ b/src/main/java/goorm/back/zo6/auth/config/SecurityConfig.java @@ -8,14 +8,13 @@ import goorm.back.zo6.auth.util.JwtUtil; import goorm.back.zo6.user.application.OAuth2UserServiceFactory; import goorm.back.zo6.user.domain.Role; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; -import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.web.SecurityFilterChain; @@ -29,7 +28,6 @@ @Configuration @EnableWebSecurity -@EnableMethodSecurity(prePostEnabled = true) @RequiredArgsConstructor public class SecurityConfig { @@ -40,63 +38,80 @@ public class SecurityConfig { @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ - //cors 설정 - http.cors((cors -> cors.configurationSource(configurationSource()))); - // csfr disable - http.csrf((auth) -> auth.disable()); - // form 로그인 disable - http.formLogin((auth) -> auth.disable()); - // HTTP Basic 인증 방식 disable - http.httpBasic((auth) -> auth.disable()); + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // CORS 설정 (프론트엔드 도메인 허용 필요 시 커스터마이징) + http.cors(cors -> cors.configurationSource(configurationSource())); - //경로별 인가 작업 - http.authorizeHttpRequests((auth) -> auth - .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**","/actuator/**").permitAll() // Swagger 관련 경로 허용 - .requestMatchers("/api/v1/users/signup","/api/v1/auth/login","/api/v1/users/signup-link","/api/v1/users/check-email").permitAll() - .requestMatchers("/api/v1/users/code","/api/v1/users/verify").permitAll() - .requestMatchers("/api/v1/rekognition/authentication").permitAll() + // CSRF, Form 로그인, HTTP Basic 인증 비활성화 (JWT 기반 인증 사용) + http.csrf(AbstractHttpConfigurer::disable); + http.formLogin(AbstractHttpConfigurer::disable); + http.httpBasic(AbstractHttpConfigurer::disable); + + // URL 경로별 접근 제어 설정 + http.authorizeHttpRequests(auth -> auth + // Swagger 및 시스템 모니터링 경로 - 문서 확인 및 상태 체크용 + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**", "/actuator/**").permitAll() + // 사용자 인증 관련 공개 API - 회원가입, 로그인, 이메일 인증 등 + .requestMatchers( + "/api/v1/users/signup", "/api/v1/auth/login", + "/api/v1/users/signup-link", "/api/v1/users/check-email", + "/api/v1/users/code", "/api/v1/users/verify" + ).permitAll() + // 얼굴 인식 및 Rekognition 인증 관련 + .requestMatchers( + "/api/v1/rekognition/authentication", + "/api/v1/face/authentication", + "/api/v1/face/collection" + ).permitAll() + // 회의 정보 및 세션 조회 (비로그인 사용자도 접근 가능) + .requestMatchers( + "/conferences", "/sessions", + "/api/v1/conference/**", "/api/v1/conferences/**", + "/api/v1/conferences/image/**" + ).permitAll() + // 예약 임시 저장 (비회원도 접근 허용 대상이라면) .requestMatchers("/api/v1/reservation/temp").permitAll() - .requestMatchers("/conferences").permitAll() - .requestMatchers("/sessions").permitAll() - .requestMatchers("/api/v1/conference/**").permitAll() - .requestMatchers("/api/v1/conferences/**").permitAll() - .requestMatchers("/api/v1/face/authentication").permitAll() - .requestMatchers("/api/v1/conferences/image/**").permitAll() - .requestMatchers("/api/v1/face/authentication","/api/v1/face/collection").permitAll() + // Redis 테스트용 또는 캐시 데이터 확인용 엔드포인트 .requestMatchers("/api/v1/redis").permitAll() + // SSE 실시간 구독 관련 - 로그인 없이도 알림 구독 가능 .requestMatchers("/api/v1/sse/subscribe", "/api/v1/sse/unsubscribe", "/api/v1/sse/last-count").permitAll() + // 관리자 회원가입 (최초 관리자 등록 목적) .requestMatchers("/api/v1/admin/signup").permitAll() - .requestMatchers("/api/v1/admin/conference/**").hasRole(Role.ADMIN.getRoleName()) - .requestMatchers(HttpMethod.GET,"/api/v1/notices/**").permitAll() - .requestMatchers("/api/v1/notices/**").hasRole(Role.ADMIN.getRoleName()) + // 관리자 전용 회의 제어 API + .requestMatchers("/api/v1/admin/conference/**").hasAuthority(Role.ADMIN.getRoleName()) + // 알림 조회는 누구나 가능 + .requestMatchers(HttpMethod.GET, "/api/v1/notices/**").permitAll() + // 알림 등록/수정/삭제는 관리자만 가능 + .requestMatchers("/api/v1/notices/**").hasAuthority(Role.ADMIN.getRoleName()) + // OAuth2 로그인 (카카오 등) .requestMatchers("/oauth2/**", "/auth/login/kakao/**").permitAll() - .anyRequest().authenticated()); + // 그 외 모든 요청은 인증 필요 + .anyRequest().authenticated() + ); - //세션 설정 : STATELESS - http.sessionManagement((session) -> session - .sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + // 세션 비활성화 - JWT 기반이므로 STATELESS로 설정 + http.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); - // JWTFilter 추가 + // JWT 인증 필터 등록 (UsernamePasswordAuthenticationFilter 이전에 실행) http.addFilterBefore(new JwtAuthFilter(jwtUtil, objectMapper), UsernamePasswordAuthenticationFilter.class); - // Exception handler 추가 - http.exceptionHandling(exceptionHandling -> - exceptionHandling - .accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper)) - .authenticationEntryPoint(new CustomAuthenticationEntryPoint(objectMapper))); + // 인증/인가 실패 시 처리 핸들러 등록 + http.exceptionHandling(exception -> exception + .accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper)) + .authenticationEntryPoint(new CustomAuthenticationEntryPoint(objectMapper)) + ); - // OAuth2 로그인 설정 + // OAuth2 로그인 설정 - provider 별 후처리 핸들러 지정 http.oauth2Login(oauth2 -> oauth2 .userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserServiceFactory::loadUser)) .successHandler((request, response, authentication) -> { - OAuth2AuthenticationToken oAuth2Token = (OAuth2AuthenticationToken) authentication; - String provider = oAuth2Token.getAuthorizedClientRegistrationId(); - + OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication; + String provider = token.getAuthorizedClientRegistrationId(); AuthenticationSuccessHandler handler = successHandlerFactory.getHandler(provider); handler.onAuthenticationSuccess(request, response, authentication); }) ); + return http.build(); } From f88c0165e9c3d67497bcb6b235f3e39bdbc394f4 Mon Sep 17 00:00:00 2001 From: Park JeongHyun Date: Sun, 11 May 2025 21:39:08 +0900 Subject: [PATCH 2/2] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EB=B0=8F=20=ED=8F=AC=EB=A7=B7=ED=8C=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AbstractOAuth2LoginSuccessHandler.java | 2 ++ .../zo6/auth/application/AuthService.java | 6 ++--- .../back/zo6/auth/config/PasswordConfig.java | 2 +- .../goorm/back/zo6/auth/domain/LoginUser.java | 5 +++- .../zo6/auth/dto/request/LoginRequest.java | 3 ++- .../exception/CustomAccessDeniedHandler.java | 1 + .../CustomAuthenticationEntryPoint.java | 1 + .../back/zo6/auth/filter/JwtAuthFilter.java | 16 ++++++------- .../zo6/auth/presentation/AuthController.java | 2 +- .../goorm/back/zo6/auth/util/CookieUtil.java | 10 ++++---- .../goorm/back/zo6/auth/util/JwtUtil.java | 24 ++++++++++--------- 11 files changed, 41 insertions(+), 31 deletions(-) diff --git a/src/main/java/goorm/back/zo6/auth/application/AbstractOAuth2LoginSuccessHandler.java b/src/main/java/goorm/back/zo6/auth/application/AbstractOAuth2LoginSuccessHandler.java index 62d61d5..ccbb1db 100644 --- a/src/main/java/goorm/back/zo6/auth/application/AbstractOAuth2LoginSuccessHandler.java +++ b/src/main/java/goorm/back/zo6/auth/application/AbstractOAuth2LoginSuccessHandler.java @@ -24,7 +24,9 @@ public abstract class AbstractOAuth2LoginSuccessHandler implements Authenticatio private final UserRepository userRepository; protected abstract String getEmail(OAuth2User oAuth2User); + protected abstract Long getUserId(OAuth2User oAuth2User); + protected abstract Role getRole(OAuth2User oAuth2User); @Override diff --git a/src/main/java/goorm/back/zo6/auth/application/AuthService.java b/src/main/java/goorm/back/zo6/auth/application/AuthService.java index 77b1f3d..cf78c25 100644 --- a/src/main/java/goorm/back/zo6/auth/application/AuthService.java +++ b/src/main/java/goorm/back/zo6/auth/application/AuthService.java @@ -19,10 +19,10 @@ public class AuthService { private final JwtUtil jwtUtil; private final PasswordEncoder passwordEncoder; - public LoginResponse login(LoginRequest loginRequest){ + public LoginResponse login(LoginRequest loginRequest) { User user = userRepository.findByEmail(loginRequest.email()) - .filter(m -> passwordEncoder.matches(loginRequest.password(),m.getPassword().getValue())) - .orElseThrow(()-> new CustomException(ErrorCode.USER_NOT_MATCH_LOGIN_INFO)); + .filter(m -> passwordEncoder.matches(loginRequest.password(), m.getPassword().getValue())) + .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_MATCH_LOGIN_INFO)); String accessToken = jwtUtil.createAccessToken(user.getId(), user.getEmail(), user.getRole()); diff --git a/src/main/java/goorm/back/zo6/auth/config/PasswordConfig.java b/src/main/java/goorm/back/zo6/auth/config/PasswordConfig.java index 6a39b87..9aa86f5 100644 --- a/src/main/java/goorm/back/zo6/auth/config/PasswordConfig.java +++ b/src/main/java/goorm/back/zo6/auth/config/PasswordConfig.java @@ -8,7 +8,7 @@ public class PasswordConfig { @Bean - public BCryptPasswordEncoder getPasswordEncoder(){ + public BCryptPasswordEncoder getPasswordEncoder() { return new BCryptPasswordEncoder(); } } diff --git a/src/main/java/goorm/back/zo6/auth/domain/LoginUser.java b/src/main/java/goorm/back/zo6/auth/domain/LoginUser.java index 41a3296..e689ece 100644 --- a/src/main/java/goorm/back/zo6/auth/domain/LoginUser.java +++ b/src/main/java/goorm/back/zo6/auth/domain/LoginUser.java @@ -8,7 +8,10 @@ public record LoginUser(Long id, String email, String role) implements UserDetails { - public Long getId(){return id; } + public Long getId() { + return id; + } + @Override public String getUsername() { return email; diff --git a/src/main/java/goorm/back/zo6/auth/dto/request/LoginRequest.java b/src/main/java/goorm/back/zo6/auth/dto/request/LoginRequest.java index 196d450..d457e70 100644 --- a/src/main/java/goorm/back/zo6/auth/dto/request/LoginRequest.java +++ b/src/main/java/goorm/back/zo6/auth/dto/request/LoginRequest.java @@ -11,4 +11,5 @@ public record LoginRequest( @Schema(description = "비밀번호", example = "12345") @NotBlank(message = "비밀번호를 입력해 주세요.") String password -){} +) { +} diff --git a/src/main/java/goorm/back/zo6/auth/exception/CustomAccessDeniedHandler.java b/src/main/java/goorm/back/zo6/auth/exception/CustomAccessDeniedHandler.java index 98b4049..9a3e25f 100644 --- a/src/main/java/goorm/back/zo6/auth/exception/CustomAccessDeniedHandler.java +++ b/src/main/java/goorm/back/zo6/auth/exception/CustomAccessDeniedHandler.java @@ -16,6 +16,7 @@ @RequiredArgsConstructor public class CustomAccessDeniedHandler implements AccessDeniedHandler { private final ObjectMapper objectMapper; + @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); diff --git a/src/main/java/goorm/back/zo6/auth/exception/CustomAuthenticationEntryPoint.java b/src/main/java/goorm/back/zo6/auth/exception/CustomAuthenticationEntryPoint.java index 73e85fe..a716f86 100644 --- a/src/main/java/goorm/back/zo6/auth/exception/CustomAuthenticationEntryPoint.java +++ b/src/main/java/goorm/back/zo6/auth/exception/CustomAuthenticationEntryPoint.java @@ -16,6 +16,7 @@ @RequiredArgsConstructor public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { private final ObjectMapper objectMapper; + @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); diff --git a/src/main/java/goorm/back/zo6/auth/filter/JwtAuthFilter.java b/src/main/java/goorm/back/zo6/auth/filter/JwtAuthFilter.java index 128195d..b5ce843 100644 --- a/src/main/java/goorm/back/zo6/auth/filter/JwtAuthFilter.java +++ b/src/main/java/goorm/back/zo6/auth/filter/JwtAuthFilter.java @@ -52,8 +52,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse setSecuritySession(loginUser); filterChain.doFilter(request, response); - }catch (CustomException e){ - ErrorCode errorCode = e.getErrorCode(); + } catch (CustomException e) { + ErrorCode errorCode = e.getErrorCode(); switch (errorCode) { case WRONG_TYPE_TOKEN, UNSUPPORTED_TOKEN, EXPIRED_TOKEN, UNKNOWN_TOKEN_ERROR -> setResponse(response, errorCode); @@ -65,16 +65,16 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } - private static void setSecuritySession(LoginUser loginUser){ + private static void setSecuritySession(LoginUser loginUser) { log.info("SessionLoginUser : {}", loginUser.getUsername()); Collection authorities = List.of(new SimpleGrantedAuthority(loginUser.getRole().getRoleSecurity())); - Authentication authToken = new UsernamePasswordAuthenticationToken(loginUser,null, authorities); + Authentication authToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities); SecurityContextHolder.getContext().setAuthentication(authToken); } - private LoginUser getUser(String token){ + private LoginUser getUser(String token) { Long userId = jwtUtil.getUserId(token); String email = jwtUtil.getUsername(token); String role = jwtUtil.getRole(token); @@ -82,13 +82,13 @@ private LoginUser getUser(String token){ return new LoginUser(userId, email, role); } - private boolean verifyToken(HttpServletRequest request,String token){ + private boolean verifyToken(HttpServletRequest request, String token) { Boolean isValid = (Boolean) request.getAttribute("isTokenValid"); - if(isValid != null) return isValid; + if (isValid != null) return isValid; if (token == null || !jwtUtil.validateToken(token)) { log.debug("token null or not validate"); - request.setAttribute("isTokenValid",false); + request.setAttribute("isTokenValid", false); return false; } diff --git a/src/main/java/goorm/back/zo6/auth/presentation/AuthController.java b/src/main/java/goorm/back/zo6/auth/presentation/AuthController.java index cf68e46..5b7ce2b 100644 --- a/src/main/java/goorm/back/zo6/auth/presentation/AuthController.java +++ b/src/main/java/goorm/back/zo6/auth/presentation/AuthController.java @@ -45,7 +45,7 @@ public ResponseEntity> login(@Validated @RequestBody @DeleteMapping("/logout") @Operation(summary = "로그아웃", description = "api 요청 시 쿠키를 강제 만료시켜 로그아웃합니다.") - public ResponseEntity logout(){ + public ResponseEntity logout() { ResponseCookie cookie = CookieUtil.deleteCookie(COOKIE_NAME); return ResponseEntity.ok() .header(HttpHeaders.SET_COOKIE, cookie.toString()) diff --git a/src/main/java/goorm/back/zo6/auth/util/CookieUtil.java b/src/main/java/goorm/back/zo6/auth/util/CookieUtil.java index baaffe6..f63d783 100644 --- a/src/main/java/goorm/back/zo6/auth/util/CookieUtil.java +++ b/src/main/java/goorm/back/zo6/auth/util/CookieUtil.java @@ -16,7 +16,7 @@ public static ResponseCookie createCookie(String name, String value, long cookie .build(); } - public static ResponseCookie deleteCookie(String name){ + public static ResponseCookie deleteCookie(String name) { return ResponseCookie.from(name, null) .maxAge(0) .path("/") @@ -26,16 +26,16 @@ public static ResponseCookie deleteCookie(String name){ .build(); } - public static String findToken(HttpServletRequest request){ + public static String findToken(HttpServletRequest request) { String token = null; Cookie[] cookies = request.getCookies(); - if(cookies == null){ + if (cookies == null) { return null; } - for(Cookie cookie : cookies){ - if(cookie.getName().equals("Authorization")){ + for (Cookie cookie : cookies) { + if (cookie.getName().equals("Authorization")) { token = cookie.getValue(); } } diff --git a/src/main/java/goorm/back/zo6/auth/util/JwtUtil.java b/src/main/java/goorm/back/zo6/auth/util/JwtUtil.java index 26f5eb1..7308d3a 100644 --- a/src/main/java/goorm/back/zo6/auth/util/JwtUtil.java +++ b/src/main/java/goorm/back/zo6/auth/util/JwtUtil.java @@ -21,18 +21,18 @@ public class JwtUtil { @Value("${jwt.valid-time}") private long TOKEN_VALID_TIME; - public JwtUtil(@Value("${jwt.secret}") String secret){ + public JwtUtil(@Value("${jwt.secret}") String secret) { secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } - public String createAccessToken(Long userId, String email, Role role){ + public String createAccessToken(Long userId, String email, Role role) { Date timeNow = new Date(System.currentTimeMillis()); Date expirationTime = new Date(timeNow.getTime() + TOKEN_VALID_TIME); return Jwts.builder() .claim("userId", userId) - .claim("email",email) - .claim("role",role.getRoleSecurity()) + .claim("email", email) + .claim("role", role.getRoleSecurity()) .setIssuedAt(timeNow) .setExpiration(expirationTime) .signWith(secretKey, SignatureAlgorithm.HS256) @@ -43,35 +43,37 @@ public Long getUserId(String token) { return Jwts.parserBuilder().setSigningKey(secretKey).build() .parseClaimsJws(token).getBody().get("userId", Long.class); } + public String getUsername(String token) { return Jwts.parserBuilder().setSigningKey(secretKey).build() .parseClaimsJws(token).getBody().get("email", String.class); } + public String getRole(String token) { return Jwts.parserBuilder().setSigningKey(secretKey).build() .parseClaimsJws(token).getBody().get("role", String.class); } - public boolean validateToken(String token){ + public boolean validateToken(String token) { //log.info("토큰 유효성 검증 시작"); return valid(secretKey, token); } - private boolean valid(SecretKey secretKey, String token){ + private boolean valid(SecretKey secretKey, String token) { if (token == null) { throw new CustomException(ErrorCode.MISSING_TOKEN); } - try{ + try { Jws claims = Jwts.parserBuilder().setSigningKey(secretKey).build() .parseClaimsJws(token); return !claims.getBody().getExpiration().before(new Date()); - }catch (SignatureException ex){ + } catch (SignatureException ex) { throw new CustomException(ErrorCode.WRONG_TYPE_TOKEN); - }catch (MalformedJwtException ex){ + } catch (MalformedJwtException ex) { throw new CustomException(ErrorCode.UNSUPPORTED_TOKEN); - }catch (ExpiredJwtException ex){ + } catch (ExpiredJwtException ex) { throw new CustomException(ErrorCode.EXPIRED_TOKEN); - }catch (IllegalArgumentException ex){ + } catch (IllegalArgumentException ex) { throw new CustomException(ErrorCode.UNKNOWN_TOKEN_ERROR); } }