Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
483f11b
feat: 시큐리티 메인, getCurrentUserId() 유틸 구현 (#25)
kimyw1018 Jul 21, 2026
cf73e51
feat: 시큐리티 전용 userdetails 및 service 구현 (#25)
kimyw1018 Jul 23, 2026
60fb0e8
feat: jwt 토큰 생성 및 유효성 검증 유틸리티 구현 (#25)
kimyw1018 Jul 23, 2026
7c67b37
feat: http 요청 토큰 검증을 위한 jwt 인증 필터 구현 (#25)
kimyw1018 Jul 23, 2026
e1e71c2
feat: hjwt 인증 실패 및 권한 부족 예외 핸들러 구현 (#25)
kimyw1018 Jul 23, 2026
346270b
feat: securityconfig 내 jwt 필터 및 예외 핸들러 체인 등록 (#25)
kimyw1018 Jul 23, 2026
17e5f51
feat: 소셜 로그인 api 및 비즈니스 로직 뼈대 구현(#25)
kimyw1018 Jul 23, 2026
ff23e22
Merge branch 'develop' of https://github.com/Musereview/BE into feat/…
kimyw1018 Jul 23, 2026
e48ba51
feat: 코드 스타일 정리 (#25)
kimyw1018 Jul 23, 2026
bd65765
feat: CommonStatus 내 UNAUTHORIZED, FORBIDDEN 상수 추가 (#25)
kimyw1018 Jul 23, 2026
7ba95bb
feat: 예외 코드 변경 (#25)
kimyw1018 Jul 23, 2026
da3a472
feat: 액세스, 리프레시 토근 구분 (#25)
kimyw1018 Jul 23, 2026
0b6a6bd
feat: 액세스토큰 검증 함수 명확히 (#25)
kimyw1018 Jul 23, 2026
4926638
feat: email속성 제거 (#25)
kimyw1018 Jul 24, 2026
a636bb4
feat: 유저 엔티티 연결 (#25)
kimyw1018 Jul 24, 2026
6613435
feat: 주석 처리 (#25)
kimyw1018 Jul 24, 2026
879730a
feat: 다음 작업 분리 (#25)
kimyw1018 Jul 24, 2026
3bfaaaa
feat: Base64 기준으로 수정 (#25)
kimyw1018 Jul 24, 2026
809047b
feat: 소셜 로그인 구현 전 방어 (#25)
kimyw1018 Jul 24, 2026
1ffca71
feat: jwt 키 형식 체크 (#25)
kimyw1018 Jul 24, 2026
6119db5
feat: 500대신 401처리로 감싸기 (#25)
kimyw1018 Jul 24, 2026
bc04e7c
feat: 유저 역할 임의 부여 제거 (#25)
kimyw1018 Jul 24, 2026
678eb4e
feat: 이메일 속성 삭제 (#25)
kimyw1018 Jul 24, 2026
0ed59e6
feat: 타입 캐스팅 로직 수정 (#25)
kimyw1018 Jul 24, 2026
76ee530
feat: 토큰 유효성 private, jwt프로퍼티 반영 , 예외 처리(#25)
kimyw1018 Jul 24, 2026
8120ae7
feat: resolveToken 하드코딩 제거 (#25)
kimyw1018 Jul 24, 2026
f19f57c
feat: 앤드포인트 컨밴션 (#25)
kimyw1018 Jul 24, 2026
8c45bc1
feat: 비인증 허용 api 수정 (#25)
kimyw1018 Jul 24, 2026
5b69e50
feat: cors 추가 (#25)
kimyw1018 Jul 24, 2026
65967dc
feat: ObjectMapper주입 (#25)
kimyw1018 Jul 24, 2026
c0e7b86
feat: 유저 role 이넘 생성 (#25)
kimyw1018 Jul 24, 2026
f609452
feat: 유저 role 이넘관련 로직 추가 (#25)
kimyw1018 Jul 24, 2026
3754aa3
feat: jwt 토큰 응답 세분화 (#25)
kimyw1018 Jul 24, 2026
bb295aa
feat: 네이밍, 타입 수정(#25)
kimyw1018 Jul 24, 2026
4530b88
feat: 네이밍수정(#25)
kimyw1018 Jul 24, 2026
d8a49eb
Merge: 최신 상태 반영 (#25)
kimyw1018 Jul 24, 2026
1ab1d2a
feat: 와일드카드, 위험성 제거 (#25)
kimyw1018 Jul 24, 2026
1b90a4d
feat: 주석 정리(#25)
kimyw1018 Jul 24, 2026
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
5 changes: 3 additions & 2 deletions MR_config/local/application.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ spring:

# 3. Spring Security 및 JWT 인증 설정
jwt:
# [입력] HS256 알고리즘을 충족하는 256비트(32바이트) 이상의 임의의 비밀키를 채워주세요.
# 기입 예시: "your-local-custom-secret-key-must-be-very-long-and-secure-32bytes"
# [입력] Base64로 인코딩된 256비트(32바이트) 이상의 Secret Key를 채워주세요.
# (주의: Decoders.BASE64.decode를 사용하므로 Plain Text가 아닌 Base64 인코딩 문자열이어야 합니다.)
# 기입 예시: "c29tZS1zZWNyZXQta2V5LW11c3QtYmUtYXQtbGVhc3QtMzItYmF5dGVzLWxvbmc="
secret: ""
access-token-validity-in-seconds: 1800 # Access Token 만료 시간 (30분)
refresh-token-validity-in-seconds: 604800 # Refresh Token 만료 시간 (7일)
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/com/mr/domain/auth/controller/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.mr.domain.auth.controller;

import com.mr.domain.auth.dto.AuthRequestDTO;
import com.mr.domain.auth.dto.AuthResponseDTO;
import com.mr.domain.auth.entity.enums.SocialType;
import com.mr.domain.auth.service.AuthService;
import com.mr.global.apipayload.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
@Profile({"local", "dev"})
public class AuthController {
Comment thread
kimyw1018 marked this conversation as resolved.

private final AuthService authService;

@PostMapping("/login/{socialType}")
public ApiResponse<AuthResponseDTO.LoginResponse> socialLogin(
@PathVariable(name = "socialType") SocialType socialType,
Comment thread
kimyw1018 marked this conversation as resolved.
@RequestBody @Valid AuthRequestDTO.SocialLoginRequest request
) {
AuthResponseDTO.LoginResponse response = authService.socialLogin(socialType, request.accessToken());
return ApiResponse.onSuccess(response);
}
Comment thread
kimyw1018 marked this conversation as resolved.
}
16 changes: 16 additions & 0 deletions src/main/java/com/mr/domain/auth/dto/AuthRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.mr.domain.auth.dto;

import jakarta.validation.constraints.NotBlank;

public class AuthRequestDTO {

public record SocialLoginRequest(
@NotBlank(message = "소셜 액세스 토큰은 필수 입력값입니다.")
String accessToken
) {}

public record TokenRefreshRequest(
@NotBlank(message = "Refresh Token은 필수 입력값입니다.")
String refreshToken
) {}
}
21 changes: 21 additions & 0 deletions src/main/java/com/mr/domain/auth/dto/AuthResponseDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.mr.domain.auth.dto;

import lombok.Builder;

public class AuthResponseDTO {

@Builder
public record TokenResponse(
String accessToken,
String refreshToken,
Long accessTokenExpiresInSeconds
) {}

@Builder
public record LoginResponse(
Long userId,
String nickname,
boolean isNewUser,
TokenResponse tokenInfo
) {}
}
7 changes: 3 additions & 4 deletions src/main/java/com/mr/domain/auth/entity/SocialAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

@Getter
@Entity
// TODO: 추후 User 도메인 완성 시 단방향/양방향 인덱스 추가
// TODO: 추후 User 도메인 완성 시 인덱스 추가
@Table(
name = "social_auth",
uniqueConstraints = {
Expand All @@ -29,7 +29,7 @@ public class SocialAuth extends BaseCreatedEntity {
@Column(name = "social_auth_id")
private Long id;

// TODO: User 엔티티 연관관계 연결 예정
// TODO: User연결 예정
@Column(name = "user_id", nullable = false)
private Long userId;

Expand All @@ -55,7 +55,7 @@ public class SocialAuth extends BaseCreatedEntity {
@Builder(access = AccessLevel.PRIVATE)
private SocialAuth(Long userId, SocialType socialType, String socialId, String refreshToken,
String refreshTokenHash, LocalDateTime expiredAt, String deviceInfo) {
// 컴파일 에러 수정: 실제 정의된 validateUserAccount 메서드로 매핑

validateUserAccount(userId);
validateUserAccount(socialType);
validateUserAccount(socialId);
Expand Down Expand Up @@ -116,7 +116,6 @@ public void updateRefreshToken(String encryptedToken, String tokenHash, LocalDat
this.deviceInfo = deviceInfo;
}

// 최신 토큰 만료 및 폐기 처리
public void expireToken() {
this.refreshToken = null;
this.refreshTokenHash = null;
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/com/mr/domain/auth/entity/enums/SocialType.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.mr.domain.auth.entity.enums;

public enum SocialType {
KAKAO,
GOOGLE
kakao,
google
}
47 changes: 47 additions & 0 deletions src/main/java/com/mr/domain/auth/service/AuthService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.mr.domain.auth.service;

import com.mr.domain.auth.dto.AuthResponseDTO;
import com.mr.domain.auth.entity.enums.SocialType;
import com.mr.global.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AuthService {

private final JwtTokenProvider jwtTokenProvider;
// private final KakaoOAuthService kakaoOAuthService; (외부 API 파싱용 서비스)
// private final GoogleOAuthService googleOAuthService;

@Transactional
public AuthResponseDTO.LoginResponse socialLogin(SocialType socialType, String accessToken) {
// 1. 외부 소셜 API (카카오/구글) 통신하여 유저 프로필(email, socialId) 파싱
// SocialUserInfo userInfo = getSocialUserInfo(socialType, accessToken);

// 2. TODO: User 엔티티 연동 및 가입여부 검증 (Stub 구조)
// 만약 가입 안 되어있으면 DB User 생성 -> 저장
Long mockUserId = 1L;
String mockEmail = "user@example.com";
String mockNickname = "뮤즈유저";
boolean isNewUser = false;

String appAccessToken = jwtTokenProvider.createAccessToken(mockUserId);
String appRefreshToken = jwtTokenProvider.createRefreshToken(mockUserId);


AuthResponseDTO.TokenResponse tokenResponse = AuthResponseDTO.TokenResponse.builder()
.accessToken(appAccessToken)
.refreshToken(appRefreshToken)
.accessTokenExpiresInSeconds(3600L)
.build();
return AuthResponseDTO.LoginResponse.builder()
.userId(mockUserId)
.nickname(mockNickname)
.isNewUser(isNewUser)
.tokenInfo(tokenResponse)
.build();
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/mr/domain/user/entity/enums/UserRole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.mr.domain.user.entity.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum UserRole {
ROLE_STUDENT("ROLE_STUDENT", "학생"),
ROLE_TEACHER("ROLE_TEACHER", "강사"),
ROLE_ADMIN("ROLE_ADMIN", "관리자");

private final String key;
private final String title;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ public enum CommonStatus implements BaseCode {
SUCCESS(HttpStatus.OK, "COMMON_200", "요청에 성공하였습니다."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "COMMON_400_01", "입력값이 올바르지 않습니다."),
HTTP_MESSAGE_NOT_READABLE(HttpStatus.BAD_REQUEST, "COMMON_400_02", "요청 본문(JSON) 파싱에 실패했습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_500_01", "서버 에러가 발생했습니다.");
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON_500_01", "서버 에러가 발생했습니다."),
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON_401_01", "인증이 필요합니다."),
FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON_403_01", "금지된 접근입니다.");

private final HttpStatus status;
private final String code;
Expand Down
30 changes: 0 additions & 30 deletions src/main/java/com/mr/global/config/SecurityConfig.java

This file was deleted.

4 changes: 4 additions & 0 deletions src/main/java/com/mr/global/config/SwaggerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.mr.global.config;

public class SwaggerConfig {
}
82 changes: 82 additions & 0 deletions src/main/java/com/mr/global/security/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.mr.global.security;

import com.mr.global.security.jwt.JwtAccessDeniedHandler;
import com.mr.global.security.jwt.JwtAuthenticationEntryPoint;
import com.mr.global.security.jwt.JwtAuthenticationFilter;
import com.mr.global.security.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
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.annotation.web.configurers.AbstractHttpConfigurer;
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.List;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtTokenProvider jwtTokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;

private static final String[] PUBLIC_URLS = {
"/swagger-ui/**",
"/v3/api-docs/**",
"/api/auth/login/**",
"/api/auth/refactor"
};

@Bean
Comment thread
kimyw1018 marked this conversation as resolved.
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(PUBLIC_URLS).permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

return http.build();
}

// CORS 설정
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOriginPatterns(List.of(
"http://localhost:3000",
"http://localhost:5173",
"https://*.musereview.site"
));

configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));

configuration.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));

configuration.setExposedHeaders(List.of("Authorization"));

configuration.setAllowCredentials(true);

configuration.setMaxAge(3600L);

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/mr/global/security/SecurityUtil.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.mr.global.security;

import com.mr.global.apipayload.code.CommonStatus;
import com.mr.global.apipayload.exception.GeneralException;
import com.mr.global.security.principal.CustomUserDetails;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

public class SecurityUtil {

private SecurityUtil() {
}

public static Long getCurrentUserId() {
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication == null
|| !authentication.isAuthenticated()
|| authentication instanceof AnonymousAuthenticationToken) {
throw new GeneralException(CommonStatus.UNAUTHORIZED);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인증 정보가 없을 때 유입될 수 있는 anonymousUser 상황까지 놓치지 않고 꼼꼼하게 예외 처리해 주셨네요.

}

if (authentication.getPrincipal() instanceof CustomUserDetails userDetails) {
return userDetails.getUserId();
}

throw new GeneralException(CommonStatus.UNAUTHORIZED);
}
}
Loading
Loading