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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.teamEWSN.gitdeun.common.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ public class SecurityPath {

// permitAll
public static final String[] PUBLIC_ENDPOINTS = {
"/api/signup",
"/api/login",
"/api/token/refresh",
"/api/users/check-duplicate",
"/"
"/api/auth/oauth/refresh/*",
"/",

};

// hasRole("USER")
public static final String[] USER_ENDPOINTS = {
"/api/auth/connect/github/state",
"/api/users/me",
"/api/users/me/**",
"/api/logout"
"/api/logout",
"/api/repos/**"
};

// hasRole("ADMIN")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
public class AnalysisResultDto {
// FastAPI가 반환하는 Repo 관련 정보
private String defaultBranch;
private String language;
private String description;
private LocalDateTime githubLastUpdatedAt;

Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/teamEWSN/gitdeun/common/jwt/JwtToken.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ public class JwtToken {
private String grantType;
private String accessToken;
private String refreshToken;

// 정적 메서드
public static JwtToken of(String accessToken, String refreshToken) {
return JwtToken.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
182 changes: 124 additions & 58 deletions src/main/java/com/teamEWSN/gitdeun/common/jwt/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.teamEWSN.gitdeun.common.jwt;

import com.teamEWSN.gitdeun.common.exception.GlobalException;
import com.teamEWSN.gitdeun.common.exception.ErrorCode;
import com.teamEWSN.gitdeun.user.entity.Role;
import com.teamEWSN.gitdeun.user.entity.User;
import com.teamEWSN.gitdeun.user.service.UserService;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
Expand All @@ -11,11 +11,11 @@
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.UUID;
Expand All @@ -27,6 +27,9 @@ public class JwtTokenProvider {

private final SecretKey secretKey;

@Autowired
private UserService userService;

@Autowired
private RefreshTokenService refreshTokenService;

Expand All @@ -48,76 +51,139 @@ public JwtTokenProvider(@Value("${jwt.secret-key}") String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
}

// 토큰 생성 - 유저 정보 이용
public JwtToken generateToken(Authentication authentication) {
Object principal = authentication.getPrincipal();

Long userId;
Role role;

switch (principal) {
case CustomUserPrincipal p -> {
userId = p.getId();
role = Role.valueOf(p.getRole());
}
case OidcUser oidc -> {
userId = userService.upsertAndGetId(
oidc.getEmail(), oidc.getFullName(), oidc.getPicture(), oidc.getFullName());
role = Role.USER;
}
case OAuth2User oauth2 -> {
String email = (String) oauth2.getAttributes().get("email");
userId = userService.upsertAndGetId(
email, (String) oauth2.getAttributes().get("name"),
(String) oauth2.getAttributes().get("avatar_url"), (String) oauth2.getAttributes().get("login"));
role = Role.USER;
}
case null, default -> throw new IllegalStateException("Unsupported principal");
}

long now = (new Date()).getTime();
Date accessTokenExpiration = new Date(now + accessTokenExpired * 1000);

CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal();

Long userId = ((CustomUserDetails) userPrincipal).getId();
long now = System.currentTimeMillis();
Date exp = new Date(now + accessTokenExpired * 1000);

String jti = UUID.randomUUID().toString();
// Access Token 생성

String accessToken = Jwts.builder()
.subject(String.valueOf(userId)) // Subject를 불변값인 userId로 설정
.issuedAt(new Date()) // 발행 시간
.id(jti) // blacklist 관리를 위한 jwt token id
.claim("email", userPrincipal.getEmail()) // 이메일
.claim("nickname", userPrincipal.getNickname()) // 닉네임
.claim("role", userPrincipal.getRole()) // 사용자 역할(Role)
.claim("name",userPrincipal.getName())
.claim("profileImage", userPrincipal.getProfileImage()) // 프로필 이미지 추가
.expiration(accessTokenExpiration) // 만료 시간
.signWith(secretKey) // 서명
.subject(String.valueOf(userId))
.issuedAt(new Date(now))
.id(jti)
.claim("role", role.name())
.expiration(exp)
.signWith(secretKey)
.compact();

// Refresh Token 생성 (임의의 값 생성)
String refreshToken = UUID.randomUUID().toString();
refreshTokenService.saveRefreshToken(refreshToken, userId, refreshTokenExpired);

// Redis에 Refresh Token 정보 저장
refreshTokenService.saveRefreshToken( refreshToken, userPrincipal.getEmail(), refreshTokenExpired);


// JWT Token 객체 반환
return JwtToken.builder()
.grantType("Bearer")
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();

return JwtToken.of(accessToken, refreshToken);
}

// DB 조회 후 UserDetails 생성
public Authentication getAuthentication(String token) {
Claims claims = jwtTokenParser.parseClaims(token);
Long userId = Long.valueOf(claims.getSubject());
Role role = Role.valueOf(claims.get("role", String.class));

// 토큰에서 유저 정보 추출
public Authentication getAuthentication(String accessToken) {
// 토큰에서 Claims 추출
Claims claims = jwtTokenParser.parseClaims(accessToken);

// 권한 정보 확인
if (claims.get("role") == null) {
throw new GlobalException(ErrorCode.ROLE_NOT_FOUND);
}

// 클레임에서 모든 사용자 정보 추출
Long id = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
String nickname = claims.get("nickname", String.class);
String name = claims.get("name", String.class);
String profileImage = claims.get("profileImage", String.class);
Role role = Role.valueOf(claims.get("role", String.class));
User user = userService.findById(userId);

CustomUserDetails userDetails = new CustomUserDetails(id, email, nickname, profileImage, role, name);

Collection<? extends GrantedAuthority> authorities =
Collections.singletonList(role::name);

// Authentication 객체 반환
return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
CustomUserDetails userDetails =
new CustomUserDetails(user.getId(), user.getEmail(),
user.getNickname(), user.getProfileImage(),
role, user.getName());

return new UsernamePasswordAuthenticationToken(
userDetails, null, Collections.singletonList(role::name));
}

// // 토큰 생성 - 유저 정보 이용
// public JwtToken generateToken(Authentication authentication) {
//
// long now = (new Date()).getTime();
// Date accessTokenExpiration = new Date(now + accessTokenExpired * 1000);
//
// CustomUserPrincipal userPrincipal = (CustomUserPrincipal) authentication.getPrincipal();
//
// Long userId = ((CustomUserDetails) userPrincipal).getId();
//
// String jti = UUID.randomUUID().toString();
// // Access Token 생성
// String accessToken = Jwts.builder()
// .subject(String.valueOf(userId)) // Subject를 불변값인 userId로 설정
// .issuedAt(new Date()) // 발행 시간
// .id(jti) // blacklist 관리를 위한 jwt token id
// .claim("email", userPrincipal.getEmail()) // 이메일
// .claim("nickname", userPrincipal.getNickname()) // 닉네임
// .claim("role", userPrincipal.getRole()) // 사용자 역할(Role)
// .claim("name",userPrincipal.getName())
// .claim("profileImage", userPrincipal.getProfileImage()) // 프로필 이미지 추가
// .expiration(accessTokenExpiration) // 만료 시간
// .signWith(secretKey) // 서명
// .compact();
//
// // Refresh Token 생성 (임의의 값 생성)
// String refreshToken = UUID.randomUUID().toString();
//
// // Redis에 Refresh Token 정보 저장
// refreshTokenService.saveRefreshToken( refreshToken, userPrincipal.getEmail(), refreshTokenExpired);
//
//
// // JWT Token 객체 반환
// return JwtToken.builder()
// .grantType("Bearer")
// .accessToken(accessToken)
// .refreshToken(refreshToken)
// .build();
//
// }
//
//
// // 토큰에서 유저 정보 추출
// public Authentication getAuthentication(String accessToken) {
// // 토큰에서 Claims 추출
// Claims claims = jwtTokenParser.parseClaims(accessToken);
//
// // 권한 정보 확인
// if (claims.get("role") == null) {
// throw new GlobalException(ErrorCode.ROLE_NOT_FOUND);
// }
//
// // 클레임에서 모든 사용자 정보 추출
// Long id = Long.parseLong(claims.getSubject());
// String email = claims.get("email", String.class);
// String nickname = claims.get("nickname", String.class);
// String name = claims.get("name", String.class);
// String profileImage = claims.get("profileImage", String.class);
// Role role = Role.valueOf(claims.get("role", String.class));
//
// CustomUserDetails userDetails = new CustomUserDetails(id, email, nickname, profileImage, role, name);
//
// Collection<? extends GrantedAuthority> authorities =
// Collections.singletonList(role::name);
//
// // Authentication 객체 반환
// return new UsernamePasswordAuthenticationToken(userDetails, null, authorities);
//
// }

// 토큰 정보 검증
public boolean validateToken(String token) {
log.debug("validateToken start");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;
import org.springframework.data.redis.core.index.Indexed;

@Getter
@AllArgsConstructor
Expand All @@ -18,7 +19,8 @@ public class RefreshToken {
@Id
private String refreshToken;

private String email;
@Indexed
private Long userId;
private Long issuedAt;

// Time to live (TTL) 설정, Redis에 만료 시간을 설정
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
public class RefreshTokenService {
private final RefreshTokenRepository refreshTokenRepository;

public void saveRefreshToken(String refreshToken, String email, long refreshTokenExpired) {
public void saveRefreshToken(String refreshToken, Long userId, long refreshTokenExpired) {
RefreshToken token = RefreshToken.builder()
.refreshToken(refreshToken)
.email(email)
.userId(userId)
.issuedAt(System.currentTimeMillis())
.ttl(refreshTokenExpired) // @TimeToLive에 사용될 만료 시간
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;

Expand All @@ -32,23 +32,51 @@ public class CustomOAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHa
private String frontUrl;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication;
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
String state = request.getParameter("state");
public void onAuthenticationSuccess(HttpServletRequest req,
HttpServletResponse res,
Authentication auth) throws IOException {

String purpose = oAuthStateService.consumeState(state); // "connect:42" 또는 null
String state = req.getParameter("state");
String purpose = state != null ? oAuthStateService.consumeState(state) : null;

// 1) 계정 연동 시나리오
if (purpose != null && purpose.startsWith("connect:")) {
Long userId = Long.parseLong(purpose.split(":")[1]);
authService.connectGithubAccount(oAuth2User, userId);
response.sendRedirect(frontUrl + "/oauth/callback#connected=true");
handleAccountConnection(purpose, (OAuth2User) auth.getPrincipal(), res);
return;
}

// 일반 로그인 흐름
// 2) 일반 로그인
handleStandardLogin(req, res, auth);
}

/**
* 일반 로그인 성공 시 JWT 토큰을 발급 및 클라이언트로 리디렉션
*/
private void handleStandardLogin(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
// JWT 액세스 토큰과 리프레시 토큰을 생성합니다.
JwtToken jwtToken = jwtTokenProvider.generateToken(authentication);

// 리프레시 토큰은 보안을 위해 HttpOnly 쿠키에 저장합니다.
cookieUtil.setCookie(response, "refreshToken", jwtToken.getRefreshToken(), jwtTokenProvider.getRefreshTokenExpired());
String targetUrl = frontUrl + "/oauth/callback#accessToken=" + jwtToken.getAccessToken();

// 액세스 토큰은 URL 프래그먼트로 프론트엔드에 전달합니다.
String targetUrl = UriComponentsBuilder.fromUriString(frontUrl + "/oauth/callback")
.fragment("accessToken=" + jwtToken.getAccessToken())
.build()
.toUriString();

clearAuthenticationAttributes(request); // 세션 클린업
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}

/**
* 기존 계정에 새로운 소셜 계정을 연동하는 흐름을 처리
*/
private void handleAccountConnection(String purpose, OAuth2User oAuth2User, HttpServletResponse response) throws IOException {
Long userId = Long.parseLong(purpose.split(":")[1]);
authService.connectGithubAccount(oAuth2User, userId); // 계정 연동 로직 호출

String targetUrl = frontUrl + "/oauth/callback#connected=true";
response.sendRedirect(targetUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public class RepoResponseDto {
private final Long repoId;
private final String githubRepoUrl;
private final String defaultBranch;
private final String language;
private final String description;
private final LocalDateTime githubLastUpdatedAt;

Expand Down
Loading