Skip to content
Merged
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
Empty file modified gradlew
100644 → 100755
Empty file.
16 changes: 12 additions & 4 deletions src/main/java/io/github/petty/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import io.github.petty.users.jwt.LoginFilter;
import io.github.petty.users.oauth2.CustomOAuth2UserService;
import io.github.petty.users.oauth2.OAuth2SuccessHandler;
import io.github.petty.users.repository.UsersRepository;
import io.github.petty.users.service.RefreshTokenService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
Expand All @@ -24,16 +26,22 @@ public class SecurityConfig {
private final JWTUtil jwtUtil;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final RefreshTokenService refreshTokenService;
private final UsersRepository usersRepository;

public SecurityConfig(
AuthenticationConfiguration authenticationConfiguration,
JWTUtil jwtUtil,
CustomOAuth2UserService customOAuth2UserService,
OAuth2SuccessHandler oAuth2SuccessHandler) {
OAuth2SuccessHandler oAuth2SuccessHandler,
RefreshTokenService refreshTokenService,
UsersRepository usersRepository) {
this.authenticationConfiguration = authenticationConfiguration;
this.jwtUtil = jwtUtil;
this.customOAuth2UserService = customOAuth2UserService;
this.oAuth2SuccessHandler = oAuth2SuccessHandler;
this.refreshTokenService = refreshTokenService;
this.usersRepository = usersRepository;
}

@Bean
Expand All @@ -55,7 +63,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authorizeHttpRequests((auth) -> auth
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers("/user").authenticated()
.requestMatchers("/login/**", "/oauth2/**").permitAll()
.requestMatchers("/login/**", "/oauth2/**", "/api/auth/refresh").permitAll()
.anyRequest().permitAll())
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
Expand All @@ -67,12 +75,12 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.logout(logout -> logout
.logoutUrl("/logout") // 클라이언트 측 로그아웃 요청 URL과 일치
.logoutSuccessUrl("/") // 로그아웃 성공 후 리다이렉트 URL
.deleteCookies("jwt") // 로그아웃 시 jwt 쿠키 삭제
.deleteCookies("JSESSIONID", "jwt", "refresh_token") // 로그아웃 시 jwt 쿠키 삭제
.invalidateHttpSession(true)
.clearAuthentication(true)
)
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil), UsernamePasswordAuthenticationFilter.class)
.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshTokenService, usersRepository), UsernamePasswordAuthenticationFilter.class)
.sessionManagement((session) -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.github.petty.users.controller;

import io.github.petty.users.dto.EmailVerificationRequest;
import io.github.petty.users.dto.RefreshTokenResponseDTO;
import io.github.petty.users.dto.VerifyCodeRequest;
import io.github.petty.users.jwt.JWTUtil;
import io.github.petty.users.service.EmailService;
import io.github.petty.users.service.RefreshTokenService;
import io.github.petty.users.util.CookieUtils;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
Expand All @@ -13,17 +19,21 @@

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api")
public class UsersApiController {

private final JWTUtil jwtUtil;
private final EmailService emailService;
private final RefreshTokenService refreshTokenService;

public UsersApiController(JWTUtil jwtUtil, EmailService emailService) {

public UsersApiController(JWTUtil jwtUtil, EmailService emailService, RefreshTokenService refreshTokenService) {
this.jwtUtil = jwtUtil;
this.emailService = emailService;
this.refreshTokenService = refreshTokenService;
}

@GetMapping("/users/me")
Expand Down Expand Up @@ -62,4 +72,34 @@ public ResponseEntity<Map<String, Object>> verifyCode(@RequestBody VerifyCodeReq

return ResponseEntity.ok(response);
}

// 리프레시 토큰 엔드포인트 추가
@PostMapping("/auth/refresh")
public ResponseEntity<?> refreshToken(
@CookieValue(value = "refresh_token", required = false) String refreshToken,
HttpServletResponse response) {

if (refreshToken == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "리프레시 토큰이 없습니다."));
}

try {
UUID refreshTokenId = UUID.fromString(refreshToken);
RefreshTokenResponseDTO tokenResponse = refreshTokenService.refreshAccessToken(refreshTokenId);

// 쿠키 설정 코드
CookieUtils.setTokenCookies(response,
tokenResponse.getAccessToken(),
UUID.fromString(tokenResponse.getRefreshToken()));

return ResponseEntity.ok(Map.of("message", "토큰이 갱신되었습니다."));
} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "유효하지 않은 토큰 형식입니다."));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", e.getMessage()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.petty.users.dto;

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

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class RefreshTokenResponseDTO {
private String accessToken;
private String refreshToken;
}
49 changes: 49 additions & 0 deletions src/main/java/io/github/petty/users/entity/RefreshToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.github.petty.users.entity;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;
import java.util.UUID;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private Users user;

@Column(nullable = false)
private String provider; // 인증 제공자 (local, github 등)

@CreationTimestamp
private LocalDateTime createdAt;

@Column(nullable = false)
private LocalDateTime expiredAt;

@Column(nullable = false)
private boolean used;

// 토큰을 사용됨으로 표시
public void markUsed() {
this.used = true;
}

// 토큰이 만료되었는지 확인
public boolean isExpired() {
return LocalDateTime.now().isAfter(this.expiredAt);
}

}
7 changes: 7 additions & 0 deletions src/main/java/io/github/petty/users/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ public JWTFilter(JWTUtil jwtUtil) {

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

// 리프레시 엔드포인트는 JWTFilter 검증 Skip
if (request.getRequestURI().equals("/api/auth/refresh")) {
filterChain.doFilter(request, response); // 바로 다음 필터/컨트롤러로
return;
}

String token = null;

// 1. 쿠키에서 토큰 확인
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/github/petty/users/jwt/JWTUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
Expand Down Expand Up @@ -50,4 +52,15 @@ public String createJwt(String username, String role, Long expiredMs) {
.signWith(secretKey)
.compact();
}

public String extractRefreshToken(HttpServletRequest request) {
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
if ("refresh_token".equals(cookie.getName())) {
return cookie.getValue(); // UUID 문자열을 쿠키에서 가져옴
}
}
}
return null;
}
}
33 changes: 19 additions & 14 deletions src/main/java/io/github/petty/users/jwt/LoginFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.petty.users.dto.CustomUserDetails;
import io.github.petty.users.dto.LoginDTO;
import io.github.petty.users.entity.Users;
import io.github.petty.users.repository.UsersRepository;
import io.github.petty.users.service.RefreshTokenService;
import io.github.petty.users.util.CookieUtils;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -19,20 +23,23 @@
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.UUID;

public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JWTUtil jwtUtil;
private final RefreshTokenService refreshTokenService;
private final UsersRepository usersRepository;

// expirationTime 주입
@Value("${jwt.expiration-time}")
private long expirationTime;

public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil) {

public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil, RefreshTokenService refreshTokenService, UsersRepository usersRepository) {
this.authenticationManager = authenticationManager;
this.jwtUtil = jwtUtil;

this.refreshTokenService = refreshTokenService;
this.usersRepository = usersRepository;
}

@Override
Expand Down Expand Up @@ -61,26 +68,24 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal();

String username = customUserDetails.getUsername();

Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
Iterator<? extends GrantedAuthority> iterator = authorities.iterator();
GrantedAuthority auth = iterator.next();

String role = auth.getAuthority();

// 액세스 토큰 생성
String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime

// JWT 토큰을 쿠키에 저장
Cookie jwtCookie = new Cookie("jwt", token);
jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어)
jwtCookie.setPath("/"); // 쿠키의 유효 경로
// jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략)
int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환
jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정
response.addCookie(jwtCookie);
// 사용자 조회
Users user = usersRepository.findByUsername(username);

// 리프레시 토큰 생성
UUID refreshToken = refreshTokenService.createRefreshToken(user);

// response.addHeader("Authorization", "Bearer " + token);
// 쿠키 설정 코드
CookieUtils.setTokenCookies(response, token, refreshToken);
}
Comment on lines +78 to 89
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

사용자 조회 시 null 체크 및 예외 처리 추가 필요

현재 구현에서 usersRepository.findByUsername(username)의 결과에 대한 null 체크가 없습니다. 인증이 성공했다면 사용자가 존재해야 하지만, 동시성 문제나 예상치 못한 상황에서 안전성을 위해 체크가 필요합니다.

다음과 같이 개선하는 것을 권장합니다:

 // 사용자 조회
 Users user = usersRepository.findByUsername(username);
+if (user == null) {
+    throw new RuntimeException("인증된 사용자를 찾을 수 없습니다: " + username);
+}

 // 리프레시 토큰 생성
 UUID refreshToken = refreshTokenService.createRefreshToken(user);

 // 쿠키 설정 코드
 CookieUtils.setTokenCookies(response, token, refreshToken);

또는 Optional을 반환하는 메서드로 변경하는 것을 고려해보세요:

-Users user = usersRepository.findByUsername(username);
+Users user = usersRepository.findByUsername(username)
+    .orElseThrow(() -> new RuntimeException("인증된 사용자를 찾을 수 없습니다: " + username));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 액세스 토큰 생성
String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime
// JWT 토큰을 쿠키에 저장
Cookie jwtCookie = new Cookie("jwt", token);
jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어)
jwtCookie.setPath("/"); // 쿠키의 유효 경로
// jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략)
int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환
jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정
response.addCookie(jwtCookie);
// 사용자 조회
Users user = usersRepository.findByUsername(username);
// 리프레시 토큰 생성
UUID refreshToken = refreshTokenService.createRefreshToken(user);
// response.addHeader("Authorization", "Bearer " + token);
// 쿠키 설정 코드
CookieUtils.setTokenCookies(response, token, refreshToken);
}
// 액세스 토큰 생성
String token = jwtUtil.createJwt(username, role, 3600000L); // expirationTime
// 사용자 조회
Users user = usersRepository.findByUsername(username);
if (user == null) {
throw new RuntimeException("인증된 사용자를 찾을 수 없습니다: " + username);
}
// 리프레시 토큰 생성
UUID refreshToken = refreshTokenService.createRefreshToken(user);
// 쿠키 설정 코드
CookieUtils.setTokenCookies(response, token, refreshToken);
}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/jwt/LoginFilter.java around lines 78 to
89, add a null check after retrieving the user with
usersRepository.findByUsername(username). If the user is null, throw an
appropriate exception or handle the error to prevent null pointer issues.
Alternatively, modify the repository method to return an Optional and handle the
absence of the user accordingly before proceeding with refresh token creation
and cookie setting.


//로그인 실패시
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package io.github.petty.users.oauth2;

import io.github.petty.users.entity.Users;
import io.github.petty.users.jwt.JWTUtil;
import io.github.petty.users.repository.UsersRepository;
import io.github.petty.users.service.RefreshTokenService;
import io.github.petty.users.util.CookieUtils;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -11,33 +15,37 @@
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.UUID;

@Component
@RequiredArgsConstructor
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JWTUtil jwtUtil;
private final RefreshTokenService refreshTokenService;
private final UsersRepository usersRepository;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {

CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

// JWT 발급
String token = jwtUtil.createJwt(oAuth2User.getEmail(), oAuth2User.getAuthorities().iterator().next().getAuthority(), 3600000L);
// 액세스 토큰 생성
String token = jwtUtil.createJwt(oAuth2User.getEmail(),
oAuth2User.getAuthorities().iterator().next().getAuthority(),
3600000L); // 1시간

// JWT 토큰을 쿠키에 저장
Cookie jwtCookie = new Cookie("jwt", token);
jwtCookie.setHttpOnly(true); // JavaScript 접근 방지 (XSS 방어)
jwtCookie.setPath("/"); // 쿠키의 유효 경로
// jwtCookie.setSecure(true); // HTTPS 환경에서만 전송 (로컬호스트에서는 생략)
int maxAgeSeconds = (int) (3600000L / 1000); // 만료 시간을 초 단위로 변환
jwtCookie.setMaxAge(maxAgeSeconds); // 쿠키의 만료 시간 설정
response.addCookie(jwtCookie);
// 사용자 조회
Users user = usersRepository.findByUsername(oAuth2User.getEmail());

Comment on lines +39 to 41
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

사용자 조회 결과에 대한 null 체크가 필요합니다.

usersRepository.findByUsername()이 null을 반환할 수 있으므로, 리프레시 토큰 생성 전에 사용자 존재 여부를 확인해야 합니다.

다음과 같이 수정하세요:

        // 사용자 조회
        Users user = usersRepository.findByUsername(oAuth2User.getEmail());
+       if (user == null) {
+           throw new IllegalStateException("사용자를 찾을 수 없습니다: " + oAuth2User.getEmail());
+       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 사용자 조회
Users user = usersRepository.findByUsername(oAuth2User.getEmail());
// 사용자 조회
Users user = usersRepository.findByUsername(oAuth2User.getEmail());
if (user == null) {
throw new IllegalStateException("사용자를 찾을 수 없습니다: " + oAuth2User.getEmail());
}
🤖 Prompt for AI Agents
In src/main/java/io/github/petty/users/oauth2/OAuth2SuccessHandler.java around
lines 39 to 41, add a null check after calling
usersRepository.findByUsername(oAuth2User.getEmail()) to verify if the user
exists before proceeding with refresh token creation. If the user is null,
handle this case appropriately, such as by throwing an exception or returning an
error response, to prevent null pointer exceptions later in the code.

String targetUrl = "/";
// 리프레시 토큰 생성
UUID refreshToken = refreshTokenService.createRefreshToken(user);

// 쿠키 설정 코드
CookieUtils.setTokenCookies(response, token, refreshToken);

String targetUrl = "/";
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
}
Loading