Skip to content

Commit

Permalink
refactor: sns 컨트롤러, 서비스 구조 변경
Browse files Browse the repository at this point in the history
  • Loading branch information
nahowo committed Nov 15, 2024
2 parents d004056 + 6e52a4a commit ebae369
Show file tree
Hide file tree
Showing 24 changed files with 748 additions and 639 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public OpenAPI openAPI() {
private Info apiInfo() {
return new Info()
.title("Alchive API")
.description("[ Base URL: http://localhost:8080/api/v1]\n\nAlchive의 API 문서")
.description("[ Base URL: http://localhost:8080/api/v2]\n\nAlchive의 API 문서")
.version("1.0.0");
}
}
20 changes: 15 additions & 5 deletions src/main/java/com/Alchive/backend/config/auth/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.Alchive.backend.config.auth.handler.OAuth2FailureHandler;
import com.Alchive.backend.config.auth.handler.OAuth2SuccessHandler;
import com.Alchive.backend.config.auth.service.CustomOAuth2UserService;
import com.Alchive.backend.config.jwt.JwtAuthenticationFilter;
import com.Alchive.backend.config.jwt.JwtTokenProvider;
import com.Alchive.backend.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -12,6 +15,7 @@
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@RequiredArgsConstructor
Expand All @@ -21,20 +25,26 @@ public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final OAuth2FailureHandler oAuth2FailureHandler;
private final JwtTokenProvider jwtTokenProvider;
private final UserService userService;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(
AbstractHttpConfigurer::disable
)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) // H2 콘솔 사용을 위한 설정
// CSRF 보호 비활성화
.csrf(AbstractHttpConfigurer::disable)
// H2 콘솔 사용을 위한 Frame-Options 설정
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.authorizeHttpRequests(requests ->
requests.anyRequest().permitAll() // 모든 요청을 모든 사용자에게 허용
)
.addFilterBefore(
new JwtAuthenticationFilter(jwtTokenProvider, userService),
UsernamePasswordAuthenticationFilter.class
)
.sessionManagement(sessionManagement ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
) // 세션을 사용하지 않으므로 STATELESS 설정
) // 세션을 사용하지 않으므로 STATELESS 설정
.logout( // 로그아웃 성공 시 / 주소로 이동
(logoutConfig) -> logoutConfig.logoutSuccessUrl("/")
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.Alchive.backend.config.auth.handler;

import com.Alchive.backend.config.jwt.TokenService;
import com.Alchive.backend.config.jwt.JwtTokenProvider;
import com.Alchive.backend.domain.user.User;
import com.Alchive.backend.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -20,8 +20,8 @@
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final TokenService tokenService;
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;

// 검증 완료된 유저의 정보를 가져와서 토큰 생성, 로그인/회원가입 요청에 맞게 리다이렉트
@Override
Expand All @@ -32,20 +32,19 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo
String email = oAuth2User.getAttribute("email");
Optional<User> user = userRepository.findByEmail(email);
String targetUrl;
Long userId;
String userEmail;

if ( user.isPresent() ) { // 로그인인 경우
userId = user.get().getId();
String accessToken = tokenService.generateAccessToken(userId);
String refreshToken = tokenService.generateRefreshToken();
if (user.isPresent()) { // 로그인인 경우
userEmail = user.get().getEmail();
String accessToken = jwtTokenProvider.createAccessToken(userEmail);
String refreshToken = jwtTokenProvider.createRefreshToken(userEmail);
targetUrl = UriComponentsBuilder.fromUriString("/")
.queryParam("access", accessToken)
.queryParam("refresh", refreshToken)
.build().toUriString();
}
else { // 회원가입인 경우
} else { // 회원가입인 경우
targetUrl = UriComponentsBuilder.fromUriString("http://localhost:5173/sign")
.queryParam("email",email)
.queryParam("email", email)
.build().toUriString();
}
getRedirectStrategy().sendRedirect(request, response, targetUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException exception) {
log.info(exception.getMessage() + ", " + exception.getClass());
log.error(exception.getMessage() + ", " + exception.getClass());
ErrorCode errorCode = ErrorCode._INTERNAL_SERVER_ERROR;
return ResponseEntity.status(errorCode.getHttpStatus())
.body(ErrorResponse.builder()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.Alchive.backend.config.jwt;

import com.Alchive.backend.config.error.ErrorCode;
import com.Alchive.backend.config.error.ErrorResponse;
import com.Alchive.backend.config.error.exception.BusinessException;
import com.Alchive.backend.config.error.exception.token.TokenNotExistsException;
import com.Alchive.backend.domain.user.User;
import com.Alchive.backend.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

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

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
private final UserService userService;
private final AntPathMatcher pathMatcher = new AntPathMatcher();


// 인증에서 제외할 URL과 메서드
private static final Map<String, List<String>> EXCLUDE_URL = Map.ofEntries(
Map.entry("/api/v2", List.of("GET")),
Map.entry("/api/v2/api-docs/**", List.of("GET")),
Map.entry("/api/swagger-ui/**", List.of("GET")),
Map.entry("/api/v2/boards", List.of("GET")),
Map.entry("/api/v2/boards/{boardId}", List.of("GET")),
Map.entry("/api/v2/users", List.of("GET", "POST")),
Map.entry("/api/v2/users/{userId}", List.of("GET")),
Map.entry("/api/v2/users/username/{name}", List.of("GET")),
Map.entry("/api/v2/sns/{snsId}", List.of("GET")),
Map.entry("/api/v2/slack/reminder", List.of("GET")),
Map.entry("/api/v2/slack/added", List.of("GET"))
);

// EXCLUDE_URL과 메서드에 일치할 경우 현재 필터를 진행하지 않고 다음 필터 진행
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
String path = request.getServletPath();
String method = request.getMethod();
return EXCLUDE_URL.entrySet().stream()
.anyMatch(entry -> pathMatches(entry.getKey(), path) && entry.getValue().contains(method));
}

// 경로 패턴을 비교하는 유틸리티 메서드 (단순히 String 비교를 넘어 패턴 매칭이 필요할 경우 활용)
private boolean pathMatches(String pattern, String path) {
return pathMatcher.match(pattern, path);
}

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
// 액세스 토큰 추출 및 검증
String accessToken = jwtTokenProvider.resolveAccessToken(request);
if (accessToken != null && jwtTokenProvider.validateToken(accessToken)) {
authenticateWithToken(accessToken);
}
// 액세스 토큰이 없거나 만료된 경우 리프레시 토큰 확인
else {
// 리프레시 토큰 추출 및 검증
String refreshToken = jwtTokenProvider.resolveRefreshToken(request);
if (refreshToken != null && jwtTokenProvider.validateToken(refreshToken)) {
String email = jwtTokenProvider.getEmailFromToken(refreshToken);
// 새로운 액세스, 리프레시 토큰 발급
String newAccessToken = jwtTokenProvider.createAccessToken(email);
String newRefreshToken = jwtTokenProvider.createRefreshToken(email);
response.setHeader("Authorization", "Bearer " + newAccessToken);
response.setHeader("Refresh-Token", newRefreshToken);
// 새로 발급된 액세스 토큰으로 인증 처리
authenticateWithToken(newAccessToken);
} else {
// 토큰이 없는 경우
throw new TokenNotExistsException();
}
}

filterChain.doFilter(request, response);
} catch (BusinessException e) {
handleException(response, e);
}
}

private void authenticateWithToken(String token) {
// 토큰에서 이메일 추출 후 이메일로 사용자 조회
String email = jwtTokenProvider.getEmailFromToken(token);
User user = userService.findByEmail(email);
// 인증 객체 생성
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(user, null);
// SecurityContext에 인증 정보 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
}

// 토큰 검증 중 토큰이 없거나 유효하지 않은 경우 예외 처리
private void handleException(HttpServletResponse response, BusinessException exception) throws IOException {
ErrorCode errorCode = exception.getErrorCode();
ErrorResponse errorResponse = ErrorResponse.builder()
.code(String.valueOf(errorCode.getCode()))
.message(errorCode.getMessage())
.build();
response.setStatus(errorCode.getHttpStatus());
response.setContentType("application/json; charset=UTF-8"); // 한글을 위해 UTF-8 인코딩 설정

// ErrorResponse를 JSON 형식으로 변환하여 응답
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(errorResponse);

response.getWriter().write(jsonResponse);
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/Alchive/backend/config/jwt/JwtController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.Alchive.backend.config.jwt;

import com.Alchive.backend.config.result.ResultResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static com.Alchive.backend.config.result.ResultCode.TOKEN_ACCESS_SUCCESS;

@Tag(name = "JWT", description = "[Test] JWT 관련 api입니다.")
@RequiredArgsConstructor
@RestController
@RequestMapping("/api/v2/jwt") // 공통 url
public class JwtController {
private final JwtTokenProvider jwtTokenProvider;

@Operation(summary = "토큰 재발급 메서드", description = "액세스 토큰을 재발급하는 메서드입니다.")
@GetMapping("")
public ResponseEntity<ResultResponse> createToken(String email) {
String accessToken = jwtTokenProvider.createAccessToken(email);
return ResponseEntity.ok(ResultResponse.of(TOKEN_ACCESS_SUCCESS, accessToken));
}
}
95 changes: 95 additions & 0 deletions src/main/java/com/Alchive/backend/config/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.Alchive.backend.config.jwt;

import com.Alchive.backend.config.error.exception.token.TokenExpiredException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Date;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
// JWT 토큰 생성
private Key secretKey;
@Value("${jwt.token.secret-key}")
private String SECRET_KEY;
@Value("${jwt.token.access-expire-length}")
private Long ACCESS_EXPIRE_LENGTH;
@Value("${jwt.token.refresh-expire-length}")
private Long REFRESH_EXPIRE_LENGTH;

@PostConstruct
protected void init() {
secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(SECRET_KEY));
}

// 액세스 및 리프레시 토큰 생성
public String createAccessToken(String email) {
return createToken(email, ACCESS_EXPIRE_LENGTH);
}

public String createRefreshToken(String email) {
return createToken(email, REFRESH_EXPIRE_LENGTH);
}

private String createToken(String email, Long expireLength) {
Claims claims = Jwts.claims().setSubject(email);
return Jwts.builder().setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expireLength))
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
}

// 액세스 토큰 추출
public String resolveAccessToken(HttpServletRequest request) {
return resolveToken(request, "Authorization", "Bearer ");
}

// 리프레시 토큰 추출
public String resolveRefreshToken(HttpServletRequest request) {
return resolveToken(request, "Refresh-Token", "");
}

// HTTP 요청 헤더에서 토큰 추출
private String resolveToken(HttpServletRequest request, String headerName, String prefix) {
try {
String header = request.getHeader(headerName);
return prefix.isEmpty() ? header : header.substring(prefix.length());
} catch (NullPointerException | IllegalArgumentException e) {
return null;
}
}

// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
throw new TokenExpiredException();
}
}

// 이메일 추출
public String getEmailFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
Loading

0 comments on commit ebae369

Please sign in to comment.