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
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4'

}

Expand Down
3,583 changes: 3,544 additions & 39 deletions logs/app.log

Large diffs are not rendered by default.

Binary file added logs/app.log.2025-04-22.0.gz
Binary file not shown.
Binary file added logs/app.log.2025-04-23.0.gz
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package sw_workbook.spring.apiPayload.code.exception.handler;

import io.jsonwebtoken.ExpiredJwtException;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import sw_workbook.spring.apiPayload.ApiResponse;
import sw_workbook.spring.apiPayload.code.status.ErrorStatus;
import sw_workbook.spring.config.jwt.exception.JwtAuthenticationException;

import java.security.SignatureException;

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(JwtAuthenticationException.class)
public ResponseEntity<ApiResponse<Void>> handleJwtAuth(JwtAuthenticationException e) {
log.warn("[JwtAuthenticationException] {}", e.getMessage());
return buildErrorResponse(ErrorStatus._UNAUTHORIZED);
}

@ExceptionHandler(ExpiredJwtException.class)
public ResponseEntity<ApiResponse<Void>> handleExpiredJwt(ExpiredJwtException e) {
log.warn("[ExpiredJwtException] {}", e.getMessage());
return buildErrorResponse(ErrorStatus._UNAUTHORIZED);
}

@ExceptionHandler(SignatureException.class)
public ResponseEntity<ApiResponse<Void>> handleInvalidSignature(SignatureException e) {
log.warn("[SignatureException] {}", e.getMessage());
return buildErrorResponse(ErrorStatus._FORBIDDEN);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleMethodArgNotValid(MethodArgumentNotValidException e) {
String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
log.warn("[MethodArgumentNotValidException] {}", errorMessage);
return buildCustomMessage(ErrorStatus._BAD_REQUEST, errorMessage);
}

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException e) {
String errorMessage = e.getConstraintViolations().iterator().next().getMessage();
log.warn("[ConstraintViolationException] {}", errorMessage);
return buildCustomMessage(ErrorStatus._BAD_REQUEST, errorMessage);
}

@ExceptionHandler(BindException.class)
public ResponseEntity<ApiResponse<Void>> handleBindException(BindException e) {
String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
log.warn("[BindException] {}", errorMessage);
return buildCustomMessage(ErrorStatus._BAD_REQUEST, errorMessage);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleUnhandledException(Exception e) {
log.error("[Unhandled Exception]", e);
return buildErrorResponse(ErrorStatus._INTERNAL_SERVER_ERROR);
}

// 🔧 공통 응답 생성
private ResponseEntity<ApiResponse<Void>> buildErrorResponse(ErrorStatus status) {
return ResponseEntity
.status(status.getHttpStatus())
.body(ApiResponse.onFailure(status.getCode(), status.getMessage(), null));
}

private ResponseEntity<ApiResponse<Void>> buildCustomMessage(ErrorStatus status, String message) {
return ResponseEntity
.status(status.getHttpStatus())
.body(ApiResponse.onFailure(status.getCode(), message, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public ErrorReasonDTO getReasonHttpStatus() {
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build()
;
.build();
}
}

This file was deleted.

65 changes: 47 additions & 18 deletions src/main/java/sw_workbook/spring/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,47 +1,76 @@
package sw_workbook.spring.config;

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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
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 sw_workbook.spring.config.jwt.JwtAuthenticationFilter;
import sw_workbook.spring.config.jwt.JwtProvider;
import sw_workbook.spring.config.oauth.CustomAuthenticationEntryPoint;
import sw_workbook.spring.config.oauth.OAuth2LoginSuccessHandler;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtProvider jwtProvider;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/", "/home", "/signup", "/members/signup", "api/hello","/css/**").permitAll()
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정 적용
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagementConfigurer -> sessionManagementConfigurer
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))//세션비활성화
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/home","/login", "login/oauth2/**","/signup","/oauth2/**", "/members/signup", "api/hello","/css/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin((form) -> form
.anyRequest().authenticated()) // 그 외 모든 요청은 인증 필요
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home", true)
// .defaultSuccessUrl("/home",true)
.successHandler(oAuth2LoginSuccessHandler) //
.permitAll()
)
.logout((logout) -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
.exceptionHandling(exceptionHandling ->
exceptionHandling.authenticationEntryPoint(customAuthenticationEntryPoint))
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider),
UsernamePasswordAuthenticationFilter.class
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home",true)
.permitAll()
);
.build();
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowCredentials(true); // 쿠키 허용
configuration.addAllowedOrigin("http://localhost:5173"); // 허용할 프론트엔드 URL
configuration.addAllowedHeader("*"); // 모든 헤더 허용
configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용

return http.build();
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

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

}
10 changes: 10 additions & 0 deletions src/main/java/sw_workbook/spring/config/jwt/Dto/LogoutRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package sw_workbook.spring.config.jwt.Dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class LogoutRequest {
private String username;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sw_workbook.spring.config.jwt.Dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RefreshTokenRequest {
private String refreshToken;
}
10 changes: 10 additions & 0 deletions src/main/java/sw_workbook/spring/config/jwt/Dto/TokenResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package sw_workbook.spring.config.jwt.Dto;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TokenResponse {
private String accessToken;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import jakarta.servlet.http.HttpServletResponse;
Expand Down
26 changes: 22 additions & 4 deletions src/main/java/sw_workbook/spring/config/jwt/JwtGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import sw_workbook.spring.domain.Member;
import sw_workbook.spring.domain.enums.Role;

import java.security.Key;
Expand All @@ -17,6 +18,9 @@ public class JwtGenerator {
private final Key key;
private static final String GRANT_TYPE = "Bearer";

@Value("${spring.jwt.issuer}")
private String jwtIssuer;

@Value("${spring.jwt.access-token-expiration-millis}")
private long accessTokenExpirationMillis;

Expand All @@ -29,22 +33,24 @@ public JwtGenerator(@Value("${spring.jwt.secret}") String secretKey) {
this.key = Keys.hmacShaKeyFor(keyBytes);
}

public JwtToken generateToken(Long userId, Role roles) {
public JwtToken generateToken(Member member, Role roles) {

long now = (new Date()).getTime();

String authorities = roles.getValue();

String accessToken = Jwts.builder()
.setSubject(String.valueOf(userId))
.setIssuer(jwtIssuer)
//.setSubject(String.valueOf(userId))//userid로 할경우 jwt는 사양상 String 타입을 요구함
.setSubject(member.getEmail())
.claim("auth", authorities)// 권한 설정
.setExpiration(new Date(now+accessTokenExpirationMillis))
.setIssuedAt(Calendar.getInstance().getTime())
.signWith(key, SignatureAlgorithm.HS256)
.compact();
.compact();// 해더 페이로드 시그니처를 .으로 만든 문자열 인코딩&조합

String refreshToken = Jwts.builder()
.setSubject(String.valueOf(userId))
.setSubject(member.getEmail())
.setExpiration(new Date(now + refreshTokenExpirationMillis))//7일 만료
.setIssuedAt(Calendar.getInstance().getTime())
.signWith(key, SignatureAlgorithm.HS256)
Expand All @@ -56,4 +62,16 @@ public JwtToken generateToken(Long userId, Role roles) {
.refreshToken(refreshToken)
.build();
}
public String createAccessToken(String email, String role) {
long now = System.currentTimeMillis();

return Jwts.builder()
.setIssuer(jwtIssuer)
.setSubject(email)
.claim("auth", role)
.setExpiration(new Date(now + accessTokenExpirationMillis))
.setIssuedAt(new Date(now))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
}
Loading