Skip to content

Commit

Permalink
Feat & Refactor : security, jwt 설정 추가 및 카카오 로그인 진행중 / member 엔티티 수정 /…
Browse files Browse the repository at this point in the history
… config 파일 구조변경
  • Loading branch information
Suhun0331 committed Dec 10, 2024
1 parent c3604d0 commit 68661ac
Show file tree
Hide file tree
Showing 49 changed files with 1,905 additions and 607 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package umc.kkijuk.server.auth.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import umc.kkijuk.server.auth.jwt.JwtUtil;
import umc.kkijuk.server.auth.service.KakaoAuthService;
import umc.kkijuk.server.member.domain.Member;
import umc.kkijuk.server.member.service.MemberService;

import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/auth/kakao")
@RequiredArgsConstructor
@Slf4j
public class KakaoAuthController {

private final KakaoAuthService kakaoAuthService;
private final MemberService memberService;
private final JwtUtil jwtUtil;

@PostMapping("/login")
public ResponseEntity<Map<String, Object>> loginWithKakao(@RequestHeader("Authorization") String kakaoAccessToken) {
try {
// Bearer 제거
if (kakaoAccessToken.toLowerCase().startsWith("bearer ")) {
kakaoAccessToken = kakaoAccessToken.substring(7).trim();
}

// 카카오 사용자 정보 가져오기
Map<String, Object> kakaoUserInfo = kakaoAuthService.getKakaoUserInfo(kakaoAccessToken);
log.info("카카오 사용자 정보: {}", kakaoUserInfo);

// 사용자 정보 추출
String email = kakaoAuthService.extractEmail(kakaoUserInfo);
String name = kakaoAuthService.extractName(kakaoUserInfo);
String phoneNumber = kakaoAuthService.extractPhoneNumber(kakaoUserInfo);
LocalDate birthDate = kakaoAuthService.extractBirthDate(kakaoUserInfo);

// // 이메일 검증
// if (email == null || email.isEmpty()) {
// log.error("카카오 사용자 정보에 이메일이 없습니다.");
// return ResponseEntity.badRequest().body(Map.of("error", "카카오 사용자 정보에 이메일이 없습니다."));
// }

// 핸드폰 번호 검증
if (phoneNumber == null || phoneNumber.isEmpty()) {
log.error("카카오 사용자 정보에 핸드폰 번호가 없습니다.");
return ResponseEntity.badRequest().body(Map.of("error", "카카오 사용자 정보에 핸드폰 번호가 없습니다."));
}

Member member = memberService.findByPhoneNumber(phoneNumber);
if (member == null) {
log.info("신규 사용자 생성 - 핸드폰 번호: {}", phoneNumber);
member = memberService.createMember(email, name, phoneNumber, birthDate);
}

// JWT 토큰 생성
String accessToken = jwtUtil.createAccessToken(member.getEmail());
String refreshToken = jwtUtil.createRefreshToken(member.getEmail());

// 응답 데이터 생성
Map<String, Object> response = new HashMap<>();
response.put("accessToken", accessToken);
response.put("refreshToken", refreshToken);

log.info("로그인 성공: 이메일={}, accessToken 생성 완료", email);
return ResponseEntity.ok(response);

} catch (Exception e) {
log.error("카카오 로그인 처리 중 오류 발생: {}", e.getMessage(), e);
return ResponseEntity.internalServerError().body(Map.of("error", "로그인 처리 실패"));
}
}
}
16 changes: 16 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/dto/AuthResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package umc.kkijuk.server.auth.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class AuthResponse {
private String accessToken;
private String refreshToken;

public AuthResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
}
38 changes: 38 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/dto/CustomOAuth2User.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package umc.kkijuk.server.auth.dto;

import java.util.Collection;
import java.util.Map;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.OAuth2User;

public class CustomOAuth2User implements OAuth2User {

private final Collection<? extends GrantedAuthority> authorities;
private final Map<String, Object> attributes;
private final String nameAttributeKey;

public CustomOAuth2User(
Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes,
String nameAttributeKey) {
this.authorities = authorities;
this.attributes = attributes;
this.nameAttributeKey = nameAttributeKey;
}

@Override
public Map<String, Object> getAttributes() {
return attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}

@Override
public String getName() {
return (String) attributes.get(nameAttributeKey);
}
}
38 changes: 38 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/dto/KakaoResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package umc.kkijuk.server.auth.dto;

import java.util.Map;

public class KakaoResponse implements OAuth2Response {

private final Map<String, Object> attributes; // 전체 응답 데이터
private final Map<String, Object> kakaoAccountAttributes; // 카카오 계정 데이터
private final Map<String, Object> profileAttributes; // 프로필 데이터

// 생성자
public KakaoResponse(Map<String, Object> attributes) {
this.attributes = attributes;
this.kakaoAccountAttributes = (Map<String, Object>) attributes.get("kakao_account");
this.profileAttributes =
kakaoAccountAttributes != null
? (Map<String, Object>) kakaoAccountAttributes.get("profile")
: null;
}

@Override
public String getProvider() {
return "kakao"; // 고정 값
}

@Override
public String getProviderId() {
return attributes.get("id").toString(); // 카카오 사용자 고유 ID
}

@Override
public String getEmail() {
return kakaoAccountAttributes != null && kakaoAccountAttributes.containsKey("email")
? kakaoAccountAttributes.get("email").toString()
: null; // 이메일이 없는 경우 null 반환
}

}
10 changes: 10 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/dto/OAuth2Response.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package umc.kkijuk.server.auth.dto;

public interface OAuth2Response {
String getProvider();

String getProviderId();

String getEmail();

}
23 changes: 23 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/dto/OAuth2ResponseImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package umc.kkijuk.server.auth.dto;

import lombok.Getter;
import umc.kkijuk.server.member.domain.Member;

@Getter
public class OAuth2ResponseImpl implements OAuth2Response {
private final String provider = "kakao"; // Provider 고정 (예: Kakao)
private final String providerId;
private final String email;

public OAuth2ResponseImpl(String providerId, String email) {
this.providerId = providerId;
this.email = email;
}

public static OAuth2ResponseImpl fromUser(Member member) {
return new OAuth2ResponseImpl(
member.getId().toString(), // 예시로 user ID를 providerId로 설정
member.getEmail()
);
}
}
11 changes: 11 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/dto/RefreshTokenRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package umc.kkijuk.server.auth.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class RefreshTokenRequest {
private String email;
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package umc.kkijuk.server.auth.handler;

import java.io.IOException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
@Slf4j
public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

@Override
public void onAuthenticationSuccess(
HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
// 기존 리다이렉트 동작 제거
clearAuthenticationAttributes(request);

// JSON 응답
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");

// 인가코드 반환 (테스트용 코드, 프론트엔드에서 처리 가능)
String authorizationCode = request.getParameter("code");
log.info("로그인 성공, 인가코드 반환: {}", authorizationCode);

response.getWriter().write(String.format(
"{\"authorizationCode\": \"%s\"}", authorizationCode));
}
}
34 changes: 34 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/jwt/JwtCustomFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package umc.kkijuk.server.auth.jwt;

import java.io.IOException;

import lombok.RequiredArgsConstructor;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.web.filter.OncePerRequestFilter;

@RequiredArgsConstructor
public class JwtCustomFilter extends OncePerRequestFilter {

private final JwtFilter jwtFilter;

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

// 특정 요청 URI에 대해 필터 제외
if (requestUri.matches("^\\/login(?:\\/.*)?$") || requestUri.matches("^\\/oauth2(?:\\/.*)?$")) {
filterChain.doFilter(request, response);
return;
}

// JWT 필터 실행
jwtFilter.doFilterInternal(request, response, filterChain);
}
}
83 changes: 83 additions & 0 deletions src/main/java/umc/kkijuk/server/auth/jwt/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package umc.kkijuk.server.auth.jwt;

import java.io.IOException;
import java.util.Collections;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import umc.kkijuk.server.member.domain.Member;
import umc.kkijuk.server.member.repository.MemberRepository;

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

private final JwtUtil jwtUtil;
private final MemberRepository memberRepository;

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

String requestUri = request.getRequestURI();

// 카카오 로그인 경로 제외
if (requestUri.startsWith("/api/auth/kakao/login")) {
chain.doFilter(request, response);
return;
}

try {
final String authorizationHeader = request.getHeader("Authorization");
String email = null;
String jwt = null;

if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
email = jwtUtil.extractEmail(jwt);
}

if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
Member member = memberRepository.findByEmail(email).orElse(null);

if (member != null && jwtUtil.validateToken(jwt, member.getEmail())) {
UserDetails userDetails =
new org.springframework.security.core.userdetails.User(
member.getEmail(),
"",
Collections.singletonList(new SimpleGrantedAuthority(member.getRole().name())));

UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
log.warn("Invalid JWT Token for member: {}", email);
}
}
} catch (Exception e) {
log.warn("JWT Authentication Error: {}", e.getMessage());
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
}

chain.doFilter(request, response);
}
}
Loading

0 comments on commit 68661ac

Please sign in to comment.