Skip to content
Open
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ 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(SpringBoot3.0 부터는 jakarta 사용해야함)
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

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

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.UserAdminController.changeUserRole(..))")
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.

115 changes: 63 additions & 52 deletions src/main/java/org/example/expert/config/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,91 +4,102 @@
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
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.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {
public class JwtFilter extends OncePerRequestFilter {

private final JwtUtil jwtUtil;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;

String url = httpRequest.getRequestURI();
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String url = request.getRequestURI();

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

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

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

String jwt = jwtUtil.substringToken(bearerJwt);
processJwtToken(request, response, filterChain, bearerToken);
}

private void processJwtToken(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain,
String bearerToken
) throws IOException {
try {
// JWT 유효성 검사와 claims 추출
String jwt = jwtUtil.substringToken(bearerToken);
Claims claims = jwtUtil.extractClaims(jwt);
if (claims == null) {
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
return;
}

UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));

httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
httpRequest.setAttribute("email", claims.get("email"));
httpRequest.setAttribute("userRole", claims.get("userRole"));
AuthUser authUser = createAuthUserFromClaims(claims);
setSecurityContextAuthentication(authUser);

if (url.startsWith("/admin")) {
// 관리자 권한이 없는 경우 403을 반환합니다.
if (!UserRole.ADMIN.equals(userRole)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
return;
}
chain.doFilter(request, response);
return;
}

chain.doFilter(request, response);
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
filterChain.doFilter(request, response);
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
handleJwtException(response, "만료된 JWT 토큰입니다.", HttpServletResponse.SC_UNAUTHORIZED, e);
} catch (MalformedJwtException | SecurityException e) {
handleJwtException(response, "유효하지 않은 JWT 서명입니다.", HttpServletResponse.SC_UNAUTHORIZED, e);
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
handleJwtException(response, "지원되지 않는 JWT 토큰입니다.", HttpServletResponse.SC_BAD_REQUEST, e);
} catch (Exception e) {
log.error("Internal server error", e);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
handleJwtException(response, "서버 내부 오류", HttpServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}

@Override
public void destroy() {
Filter.super.destroy();
private AuthUser createAuthUserFromClaims(Claims claims) {
return new AuthUser(
Long.parseLong(claims.getSubject()),
claims.get("email", String.class),
claims.get("nickname", String.class),
UserRole.valueOf(claims.get("userRole", String.class))
);
}

private void setSecurityContextAuthentication(AuthUser authUser) {
Authentication authentication = new UsernamePasswordAuthenticationToken(
authUser,
null,
List.of(new SimpleGrantedAuthority("ROLE_" + authUser.getUserRole().name()))
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

private void handleJwtException(
HttpServletResponse response,
String message,
int statusCode,
Exception e
) throws IOException {
log.error(message, e);
response.sendError(statusCode, message);
}
}
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,13 +34,14 @@ public void init() {
key = Keys.hmacShaKeyFor(bytes);
}

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

return BEARER_PREFIX +
Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("email", email)
.claim("nickname", nickname)
.claim("userRole", userRole)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date) // 발급일
Expand Down
18 changes: 18 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,18 @@
package org.example.expert.config;

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

@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;

@Bean
public JPAQueryFactory jpaQueryFactory(){
return new JPAQueryFactory(entityManager);
}
}
36 changes: 36 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,36 @@
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 filterChain(HttpSecurity http) throws Exception {
return http
.httpBasic(AbstractHttpConfigurer::disable) // 기본 인증 비활성화
.formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화
.csrf(AbstractHttpConfigurer::disable) // CSRF 보호 비활성화
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() // 인증 없이 접근 허용
.requestMatchers("/admin/**").hasRole("ADMIN") // ADMIN만 접근 허용
.anyRequest().authenticated() // 그 외 모든 요청 인증 필요
)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) // JWT 필터 삽입
.build(); // SecurityFilterChain 반환
}
}
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 @@ -16,5 +16,7 @@ public class SignupRequest {
@NotBlank
private String password;
@NotBlank
private String nickname;
@NotBlank
private String userRole;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ public SignupResponse signup(SignupRequest signupRequest) {

User newUser = new User(
signupRequest.getEmail(),
signupRequest.getNickname(),
encodedPassword,
userRole
);
User savedUser = userRepository.save(newUser);

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

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.getNickname(), user.getUserRole());

return new SigninResponse(bearerToken);
}
Expand Down
Loading