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
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ dependencies {
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

// QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.1.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
Expand All @@ -18,7 +18,7 @@ public class AdminAccessLoggingAspect {

private final HttpServletRequest request;

@After("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))")
@Before("execution(* org.example.expert.domain.user.controller.UserController.getUser(..))")
public void logAfterChangeUserRole(JoinPoint joinPoint) {
String userId = String.valueOf(request.getAttribute("userId"));
String requestUrl = request.getRequestURI();
Expand Down

This file was deleted.

22 changes: 0 additions & 22 deletions src/main/java/org/example/expert/config/FilterConfig.java

This file was deleted.

84 changes: 40 additions & 44 deletions src/main/java/org/example/expert/config/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,33 @@
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.expert.domain.common.dto.AuthUser;
import org.example.expert.domain.user.enums.UserRole;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {
// Been 등록
@Component
public class JwtFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain
) throws ServletException, IOException {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// auth 경로 Spring Security에서 설정

String url = httpRequest.getRequestURI();

if (url.startsWith("/auth")) {
chain.doFilter(request, response);
return;
}

String bearerJwt = httpRequest.getHeader("Authorization");
String bearerJwt = request.getHeader("Authorization");

if (bearerJwt == null) {
// 토큰이 없는 경우 400을 반환합니다.
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
return;
}

Expand All @@ -51,44 +46,45 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
// JWT 유효성 검사와 claims 추출
Claims claims = jwtUtil.extractClaims(jwt);
if (claims == null) {
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
return;
}

UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));
// 클레임에서 사용자 정보 추출
Long id = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
String userRoleString = claims.get("userRole", String.class);
String nickname = claims.get("nickname", String.class);

httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
httpRequest.setAttribute("email", claims.get("email"));
httpRequest.setAttribute("userRole", claims.get("userRole"));
UserRole userRole = UserRole.valueOf(userRoleString);

if (url.startsWith("/admin")) {
// 관리자 권한이 없는 경우 403을 반환합니다.
if (!UserRole.ADMIN.equals(userRole)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
return;
}
chain.doFilter(request, response);
return;
}
// authUser 가져온 값들을 주입
AuthUser authUser = new AuthUser(id, email, userRole, nickname);

// Spring Security의 Authentication 객체 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
authUser, null, authUser.getAuthorities());

// SecurityContextHolder에 Authentication 객체를 설정합니다.
SecurityContextHolder.getContext().setAuthentication(authentication);

chain.doFilter(request, response);
filterChain.doFilter(request, response);
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
} catch (IllegalArgumentException e) { // UserRole.valueOf() 실패 등 추가 예외 처리
log.error("JWT claims parsing error or invalid user role.", e);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 클레임 처리 오류 또는 유효하지 않은 사용자 역할입니다.");
} catch (Exception e) {
log.error("Internal server error", e);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
log.error("Internal server error during JWT processing.", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "JWT 처리 중 서버 내부 오류가 발생했습니다.");
}
}

@Override
public void destroy() {
Filter.super.destroy();
}
}
}
3 changes: 2 additions & 1 deletion src/main/java/org/example/expert/config/JwtUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,15 @@ public void init() {
key = Keys.hmacShaKeyFor(bytes);
}

public String createToken(Long userId, String email, UserRole userRole) {
public String createToken(Long userId, String email, UserRole userRole, String nickname) {
Date date = new Date();

return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("userRole", userRole)
.claim("nickname", nickname)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/example/expert/config/QueryDslConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.example.expert.config;

import com.querydsl.jpa.impl.JPAQueryFactory;
import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QueryDslConfig {

private EntityManager entityManager;

public QueryDslConfig(EntityManager entityManager) {
this.entityManager = entityManager;
}

@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}

44 changes: 44 additions & 0 deletions src/main/java/org/example/expert/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package org.example.expert.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.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity //시큐리티 활성화
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtFilter jwtFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 보호 비활성화 (JWT 토큰 기반 인증 사용)
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable) // BasicAuthenticationFilter 비활성화
.formLogin(AbstractHttpConfigurer::disable) // UsernamePasswordAuthenticationFilter, DefaultLoginPageGeneratingFilter 비활성화
// 세션 관리 정책: STATELESS (서버에 세션 저장 안함)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 접근 권한 설정
.authorizeHttpRequests(authorize -> authorize
//허용 경로지정
// "/auth"로 시작하는 모든 요청은 인증 없이 허용 (회원가입, 로그인)
.requestMatchers("/auth/**").permitAll()
// "/admin"으로 시작하는 요청은 'ADMIN' 역할을 가진 사용자만 허용
.requestMatchers("/admin/**").hasRole("ADMIN")
// 나머지는 인증 필요
.anyRequest().authenticated()
).addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}
19 changes: 0 additions & 19 deletions src/main/java/org/example/expert/config/WebConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ public class SignupRequest {
private String password;
@NotBlank
private String userRole;
@NotBlank
private String nickname;
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ public SignupResponse signup(SignupRequest signupRequest) {
User newUser = new User(
signupRequest.getEmail(),
encodedPassword,
userRole
userRole,
signupRequest.getNickname()
);
User savedUser = userRepository.save(newUser);

String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole);
String bearerToken = jwtUtil.createToken(savedUser.getId(), savedUser.getEmail(), userRole, savedUser.getNickname());

return new SignupResponse(bearerToken);
}
Expand All @@ -56,7 +57,7 @@ public SigninResponse signin(SigninRequest signinRequest) {
throw new AuthException("잘못된 비밀번호입니다.");
}

String bearerToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole());
String bearerToken = jwtUtil.createToken(user.getId(), user.getEmail(), user.getUserRole(),user.getNickname());

return new SigninResponse(bearerToken);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import org.example.expert.domain.comment.dto.response.CommentResponse;
import org.example.expert.domain.comment.dto.response.CommentSaveResponse;
import org.example.expert.domain.comment.service.CommentService;
import org.example.expert.domain.common.annotation.Auth;
import org.example.expert.domain.common.dto.AuthUser;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -21,7 +21,7 @@ public class CommentController {

@PostMapping("/todos/{todoId}/comments")
public ResponseEntity<CommentSaveResponse> saveComment(
@Auth AuthUser authUser,
@AuthenticationPrincipal AuthUser authUser,
@PathVariable long todoId,
@Valid @RequestBody CommentSaveRequest commentSaveRequest
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@

public interface CommentRepository extends JpaRepository<Comment, Long> {

@Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId")
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
}
Loading