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
Expand Up @@ -15,6 +15,7 @@
import org.sejongisc.backend.user.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -23,7 +24,7 @@

@Slf4j
@RestController
@RequestMapping("/auth")
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class OauthLoginController {

Expand Down Expand Up @@ -52,6 +53,16 @@ public class OauthLoginController {
@Value("${github.redirect.uri}")
private String githubRedirectUri;




@PostMapping("/signup")
public ResponseEntity<SignupResponse> signup(@Valid @RequestBody SignupRequest request) {
log.info("[SIGNUP] request: {}", request.getEmail());
SignupResponse response = userService.signUp(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {

Expand Down Expand Up @@ -144,7 +155,7 @@ public ResponseEntity<LoginResponse> OauthLogin(@PathVariable("provider") String
};

// Access 토큰 발급
String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole());
String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail());

String refreshToken = jwtProvider.createRefreshToken(user.getUserId());

Expand Down Expand Up @@ -176,15 +187,15 @@ public ResponseEntity<LoginResponse> OauthLogin(@PathVariable("provider") String

@PostMapping("/logout")
public ResponseEntity<?> logout(@RequestHeader(value = "Authorization", required = false) String authorizationHeader) {
// 1️⃣ 헤더 유효성 검사
// 헤더 유효성 검사
if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
return ResponseEntity.badRequest()
.body(Map.of("message", "잘못된 Authorization 헤더 형식입니다."));
}

String token = authorizationHeader.substring(7);

// 2️⃣ 예외 처리 및 멱등성 보장
// 예외 처리 및 멱등성 보장
try {
loginService.logout(token);
} catch (JwtException | IllegalArgumentException e) {
Expand All @@ -195,7 +206,7 @@ public ResponseEntity<?> logout(@RequestHeader(value = "Authorization", required
// 내부 예외는 500으로 보내지 않고 안전하게 처리
}

// 3️⃣ Refresh Token 쿠키 삭제
// Refresh Token 쿠키 삭제
ResponseCookie deleteCookie = ResponseCookie.from("refresh", "")
.httpOnly(true)
.secure(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class RefreshToken {

@Id
@Column(name = "user_id", nullable = false, columnDefinition = "uuid")
private UUID userid;
private UUID userId;

@Column(nullable = false, length = 500)
private String token;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

public interface RefreshTokenRepository extends JpaRepository<RefreshToken, UUID> {

Optional<RefreshToken> findByUserid(UUID userId);
Optional<RefreshToken> findByUserId(UUID userId);

void deleteByUserId(UUID userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,22 @@ public LoginResponse login(LoginRequest request) {
throw new CustomException(ErrorCode.UNAUTHORIZED);
}

String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole());
String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail());
String refreshToken = jwtProvider.createRefreshToken(user.getUserId());

// 기존 토큰 삭제 후 새로 저장
refreshTokenRepository.findByUserId(user.getUserId())
.ifPresent(refreshTokenRepository::delete);

refreshTokenRepository.save(
org.sejongisc.backend.auth.entity.RefreshToken.builder()
.userId(user.getUserId())
.token(refreshToken)
.build()
);

log.info("RefreshToken 저장 완료: userId={}", user.getUserId());

return LoginResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> {
auth
.requestMatchers("/user/signup",
"/auth/login",
"/auth/login/kakao",
"/auth/login/google",
"/auth/login/github",
.requestMatchers("/api/auth/signup",
"/api/auth/login",
"/api/auth/login/kakao",
"/api/auth/login/google",
"/api/auth/login/github",
"/api/auth/oauth/**",
// "/api/auth/refresh",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-resources/**",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
package org.sejongisc.backend.common.auth.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;

import java.util.*;
import javax.crypto.SecretKey;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetails;
import org.sejongisc.backend.common.auth.springsecurity.CustomUserDetailsService;
import org.sejongisc.backend.user.entity.Role;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.*;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtParser {
Expand All @@ -30,54 +26,31 @@ public class JwtParser {

private SecretKey secretKey;

@Value("${jwt.expireDate.accessToken}")
private long accessTokenValidityInMillis;

@Value("${jwt.expireDate.refreshToken}")
private long refreshTokenValidityInMillis;

@PostConstruct
public void init() {
byte[] keyBytes = Base64.getDecoder().decode(rawSecretKey);
this.secretKey = Keys.hmacShaKeyFor(keyBytes);
}

// 토큰에서 사용자 ID 추출
public UUID getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
return UUID.fromString(claims.getSubject());
}

// 토큰에서 사용자 role 추출
public Role getRoleFromToken(String token) {
Claims claims = parseClaims(token);
String roleStr = claims.get("role", String.class);
if (roleStr == null) {
throw new JwtException("JWT에 role 클레임이 없습니다."); // 명확한 인증 실패 예외
}
try {
return Role.valueOf(roleStr);
} catch (IllegalArgumentException e) {
throw new JwtException("JWT의 role 클레임이 잘못되었습니다: " + roleStr);
}
}

// 토큰 유효성 검증
// 토큰 유효성 검사
public boolean validationToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
log.info("Token validation success");
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Token validation failed: {}", e.getMessage());
return false;
}
}

// Authentication 객체 생성
// Authentication 생성
public UsernamePasswordAuthenticationToken getAuthentication(String token) {
Claims claims = parseClaims(token);
String userId = claims.get("uid", String.class);

String roleStr = claims.get("role", String.class);
if(roleStr == null) {
if (roleStr == null) {
throw new JwtException("JWT에 role 클레임이 없습니다.");
}

Expand All @@ -88,23 +61,49 @@ public UsernamePasswordAuthenticationToken getAuthentication(String token) {
throw new JwtException("JWT의 role 클레임이 잘못되었습니다.: " + roleStr);
}

Collection<? extends GrantedAuthority> authorities =
List.of(new SimpleGrantedAuthority("ROLE_" + role.name())); // "ROLE_TEAM_MEMBER"
if (userId == null) {
throw new JwtException("JWT에 userId(uid)가 없습니다.");
}

// DB에서 다시 유저를 불러오기 (CustomUserDetailsService 사용)
UserDetails userDetails = customUserDetailsService.loadUserByUsername(userId);

log.debug("인증 객체 생성 완료");
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

UserDetails userDetails = customUserDetailsService.loadUserByUsername(claims.getSubject());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}

// Claims 파싱
private Claims parseClaims(String token) {
try{
try {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(token)
.getBody();
} catch(ExpiredJwtException e) {
} catch (ExpiredJwtException e) {
return e.getClaims();
}
}
}

public UUID getUserIdFromToken(String token) {
Claims claims = parseClaims(token);
String userIdStr = claims.get("uid", String.class);

// uid 클레임이 없을 경우 subject로 대체 (RefreshToken 호환)
if (userIdStr == null || userIdStr.isBlank()) {
userIdStr = claims.getSubject();
}

// 여전히 없거나 비어 있으면 명시적 예외 처리
if (userIdStr == null || userIdStr.isBlank()) {
throw new JwtException("JWT에 userId(uid/subject)가 없습니다.");
}

try {
return UUID.fromString(userIdStr);
} catch (IllegalArgumentException | NullPointerException e) {
throw new JwtException("잘못된 userId 형식의 JWT입니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ public void init() {
}

// JWT 토큰 생성
public String createToken(UUID userId, Role role) {
public String createToken(UUID userId, Role role, String email) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + accessTokenValidityInMillis);

return Jwts.builder()
.setSubject(userId.toString())
.setSubject(email)
.claim("uid", userId.toString())
.claim("role", role.name())
.setIssuedAt(now)
.setExpiration(expiryDate)
Expand Down Expand Up @@ -83,6 +84,8 @@ public String getRoleFromToken(String token) {
return claims.get("role", String.class);
}



// 토큰 유효성 검증
public boolean validationToken(String token) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role.name()));
}

@Override
public String getPassword() {
return password;
}

@Override
public String getUsername() {
return this.email;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,14 @@ public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User findUser = userRepository.findById(UUID.fromString(email)).orElseThrow(
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException {
UUID uuid;
try {
uuid = UUID.fromString(userId);
} catch (IllegalArgumentException e) {
throw new CustomException(ErrorCode.INVALID_ACCESS_TOKEN);
}
User findUser = userRepository.findById(uuid).orElseThrow(
() -> new CustomException(ErrorCode.USER_NOT_FOUND)
);

Expand Down
Loading
Loading