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
33 changes: 28 additions & 5 deletions src/main/java/org/sopt/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package org.sopt.config;

//API가 인증이 필요한지, permitAll인지 알려주는 config!
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
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 jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

Expand All @@ -19,23 +25,40 @@
public class SecurityConfig {

private final JwtAuthFilter jwtAuthFilter;
private final ObjectMapper objectMapper;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/login", "/api/v1/auth/reissue").permitAll()
.requestMatchers("/api/v1/users", "/api/v1/users/**").permitAll()
// HTTP Method를 명시해야 의도치 않은 메서드까지 허용되는 보안 위험을 막을 수 있음 - 지현님 코드리뷰
.requestMatchers(HttpMethod.POST, "/api/v1/auth/login", "/api/v1/auth/reissue").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/users").permitAll()
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception.authenticationEntryPoint((request, response, authException) -> {
// 인증 실패 시 Spring 기본 응답 대신 커스텀 JSON 반환
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write("{\"message\": \"인증이 필요합니다.\"}");
}))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}

@Bean
public BCryptPasswordEncoder passwordEncoder() {
public PasswordEncoder passwordEncoder() {
// 구체 클래스(BCryptPasswordEncoder) 대신 인터페이스(PasswordEncoder) 반환
return new BCryptPasswordEncoder();
}
}
}

//TODO. CORS 설정해보기
//TODO. 기찬님의 corsConfigurationSource()를 별도 빈으로 분리하는 구조 참고해서 반영해보기
//TODO. httpBasic, formLogin 명시적 disable 구현해보기!
4 changes: 3 additions & 1 deletion src/main/java/org/sopt/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,6 @@ public OpenAPI openAPI() {
.components(new Components().addSecuritySchemes("bearerAuth", securityScheme))
.addSecurityItem(securityRequirement);
}
}
}
// TODO. "bearerAuth" 문자열을 상수로 뽑기 (private static final String SECURITY_SCHEME_NAME = "bearerAuth")
// TODO. Info 추가해서 API 제목과 버전 명시하기
11 changes: 10 additions & 1 deletion src/main/java/org/sopt/controller/AuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,13 @@ public ResponseEntity<ApiResponse<Void>> logout(Authentication authentication,
그러면 클라가 새 토큰으로 원래 요청을 재시도하게 된다..
지금은 @RequestParam으로 refresh token을 받게 구현을 해놓았는데
실무에서는 HttpOnly 쿠키로 자동 전송하는 방식을 많이 쓴다고 한다..
다음 세미나까지 더 찾아봐야지..*/
다음 세미나까지 더 찾아봐야지..*/

//TODO. 로그아웃 응답 코드 204로 변경하기 (이게 더 restful) 한듯
//TODO. Bearer 토큰 추출 메서드 분리해서 가독성과 재사용성 높이기
//TODO. Swagger에서 Authorization 헤더를 숨김 처리하기
//TODO. request.getHeader(HttpHeaders.AUTHORIZATION).substring(...) 에서 헤더가 null이면 NPE가 터지니까 null체크나, @RequestHeader 로 받자
//TODO. Refresh Token을 HttpOnly 쿠키로 관리해보자
//TODO. CookieUtil로 쿠키 생성 로직 분리해보자
//TODO. AuthSuccessCode.SIGN_UP_SUCCESS, LOCAL_LOGIN_SUCCESS 등 성공 응답도 코드로 관리해보기
//TODO. 회원가입과 로그인이 분리 — 내 기존코드는 회원가입(/users)과 로그인(/auth/login)이 분리되어 있는데, 기찬님 코드는 /auth/signup으로 회원가입도 인증 도메인에서 처리하며 가입 즉시 토큰을 발급해준다. 이게 더 자연스러운 흐름인 것 같다
6 changes: 5 additions & 1 deletion src/main/java/org/sopt/controller/LikeController.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,8 @@ public ResponseEntity<BaseResponse<Void>> deleteLike(
likeService.deleteLike(userId, postId);
return ResponseEntity.ok(BaseResponse.success(null));
}
}
}

//TODO. @RequestMapping 경로 통합 — 클래스 레벨에 /api/v1/posts/{postId}/likes로 올리면 메서드마다 반복 안 해도 되니까..!
//TODO. Long.parseLong(authentication.getName()) 중복 제거
//TODO. 좋아요 추가 응답을 201로 변경
4 changes: 3 additions & 1 deletion src/main/java/org/sopt/controller/PostController.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,6 @@ public ResponseEntity<Void> deletePost(@PathVariable Long id) {
postService.deletePost(id);
return ResponseEntity.noContent().build();
}
}
}

//TODO. updatePost, deletePost에 Authentication 추가하기
5 changes: 4 additions & 1 deletion src/main/java/org/sopt/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,7 @@ public ResponseEntity<BaseResponse<Void>> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok(BaseResponse.success("유저 삭제 성공", null));
}
}
}

//TODO. deleteUser에 Authentication 추가하기
//TODO. updateUser 기능 추가해보기
4 changes: 3 additions & 1 deletion src/main/java/org/sopt/domain/LoggedOutToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ public static LoggedOutToken of(String token, long expiresInSeconds) {

public String getToken() { return token; }
public LocalDateTime getExpiresAt() { return expiresAt; }
}
}
//TODO. token 컬럼에 length = 500 추가하고
//TODO. BlacklistedAccessToken 요걸로 이름 바꾸장
3 changes: 3 additions & 0 deletions src/main/java/org/sopt/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ public User(String nickname, String email, String password) {
public String getEmail() { return this.email; }
public String getPassword() { return this.password; }
}

// TODO.소프트 딜리트(@SQLDelete + @SQLRestriction) 패턴 적용해보기
// TODO. 룸복 사용해보자
4 changes: 4 additions & 0 deletions src/main/java/org/sopt/exception/AuthErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sopt.exception;

public class AuthErrorCode {
}
7 changes: 5 additions & 2 deletions src/main/java/org/sopt/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.springframework.http.HttpStatus;
public enum ErrorCode {
//400 BadRequest
INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "잘못된 입려값입니다."),
INVALID_INPUT_VALUE("COMMON_001", HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."),
TITLE_TOO_LONG("POST_002", HttpStatus.BAD_REQUEST, "게시글 제목은 50자를 초과할 수 없습니다."),
CONTENT_REQUIRED("POST_003", HttpStatus.BAD_REQUEST, "게시글 내용은 필수입니다."),

Expand Down Expand Up @@ -43,4 +43,7 @@ public String getMessage(){
public HttpStatus getHttpStatus(){
return httpStatus;
}
}
}

//TODO. ErrorCode를 인터페이스로 분리하고 도메인별 enum으로 나누기
//TODO. 인증 관련 에러 코드 추가하기
4 changes: 4 additions & 0 deletions src/main/java/org/sopt/exception/PostErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sopt.exception;

public class PostErrorCode {
}
4 changes: 4 additions & 0 deletions src/main/java/org/sopt/exception/UserErrorCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.sopt.exception;

public class UserErrorCode {
}
11 changes: 10 additions & 1 deletion src/main/java/org/sopt/service/AuthService.java
Original file line number Diff line number Diff line change
Expand Up @@ -138,4 +138,13 @@ public MemberResponse getMemberById(Long memberId) {
다시 로그인한다!!

(403으로 내려주도록 했었는데, 이걸 쓰다보니 401이 더 맞는 것 같아서 바꾸러..가봅니다..)
*/
*/

// TODO. IllegalArgumentException 대신 커스텀 예외 사용하기
//TODO.reissue 순서 변경 — JWT 검증을 DB 조회보다 먼저 하는 게 더 효율적이라고 한다
//TODO.logout에 중복 체크 추가 — existsByToken 확인 후 저장하기
//TODO.getMemberById의 불필요한 캐스팅 제거 — (Member)
//TODO.BCryptPasswordEncoder → PasswordEncoder 인터페이스로 바꾸자
//TODO. reissue에서 memberId 일치 여부 검증해보자
//TODO. logout에서 Access Token 블랙리스트 저장해보자
//TODO. jwtService.getExpiresAt(accessToken)으로 정확한 만료 시각 저장해보기
7 changes: 6 additions & 1 deletion src/main/java/org/sopt/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,9 @@ public UserResponse getUser(Long id) {
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
}

//TODO.이메일 중복 검사 추가
//TODO.deleteUser에 존재 여부 확인 추가 — deleteById 대신 findById 후 없으면 예외를 던지도록 바꾸기
//TODO.updateUser 구현하기
//TODO.BCryptPasswordEncoder → PasswordEncoder 인터페이스로 바꾸기