Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ 기능 추가 :GET /admin/status 요청을 통해 로그인 상태를 확인하고 이름과 권한 담는 Controller 기능추가 #259

Merged
merged 9 commits into from
Nov 26, 2024
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
@@ -1,20 +1,27 @@
package intbyte4.learnsmate.admin.controller;

import intbyte4.learnsmate.admin.domain.dto.AdminDTO;
import intbyte4.learnsmate.admin.domain.entity.CustomUserDetails;
import intbyte4.learnsmate.admin.domain.vo.request.RequestEditAdminVO;
import intbyte4.learnsmate.admin.domain.vo.response.ResponseEditAdminVO;
import intbyte4.learnsmate.admin.domain.vo.response.ResponseFindAdminVO;
import intbyte4.learnsmate.admin.mapper.AdminMapper;
import intbyte4.learnsmate.admin.service.AdminService;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("/admin")
@RequiredArgsConstructor
@Slf4j
public class AdminController {

private final AdminService adminService;
Expand All @@ -34,4 +41,20 @@ public ResponseEntity<ResponseEditAdminVO> updateAdmin(@PathVariable Long adminC
AdminDTO updatedAdmin = adminService.updateAdmin(adminCode, adminMapper.fromVoToDto(editAdminVO));
return ResponseEntity.status(HttpStatus.OK).body(adminMapper.fromDtoToEditResponseVO(updatedAdmin));
}

// @AuthenticationPrincipal을 활용 -> CustomUserDetails에서 사용자 정보를 추출
// 인증 성공 시 사용자 이름과 권한을 상태에 저장 -> Pinia 로 이름과 권한 정보 넘어감 (loginState.js 롹인 바람)
@GetMapping("/status")
public ResponseEntity<Map<String, Object>> checkAuthStatus(@AuthenticationPrincipal CustomUserDetails userDetails) {
log.info("GET /admin/status 요청 도착");
if (userDetails == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}

Map<String, Object> response = new HashMap<>();
response.put("name", userDetails.getUserDTO().getAdminName()); // 관리자 이름
response.put("roles", userDetails.getAuthorities()); // 권한 정보

return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import intbyte4.learnsmate.admin.domain.dto.JwtTokenDTO;
import intbyte4.learnsmate.admin.domain.entity.Admin;
import intbyte4.learnsmate.admin.domain.entity.CustomUserDetails;
import intbyte4.learnsmate.admin.domain.vo.request.RequestLoginVO;
import intbyte4.learnsmate.admin.service.AdminService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

Expand Down Expand Up @@ -98,10 +98,18 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
JwtTokenDTO tokenDTO = new JwtTokenDTO(userCode, userEmail, userName);
String token = jwtUtil.generateToken(tokenDTO, roles, null); // JWT 생성 (roles와 추가적인 데이터를 페이로드에 담음)

// 생성된 JWT 토큰을 Authorization 헤더에 추가하여 응답 -> 생성된 JWT 토큰을 Authorization 헤더에 담아 응답으로 전송
response.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token);
// 쿠키 생성
Cookie jwtCookie = new Cookie("token", token);
jwtCookie.setHttpOnly(true); // HTTP Only 속성으로 설정 (JavaScript에서 접근 불가)
jwtCookie.setSecure(false); // HTTPS 연결에서만 전송 (테스트 환경에서는 false 설정 가능)
// https://learnsmate.site -> 배포 환경시 true로 전환
jwtCookie.setPath("/"); // 쿠키의 유효 경로 설정 (애플리케이션 전체에 사용 가능)
jwtCookie.setMaxAge(7 * 24 * 60 * 60); // 쿠키 만료 시간 설정 (7일)

log.info("JWT 생성 완료. Authorization 헤더에 추가됨.");
// 응답에 쿠키 추가
response.addCookie(jwtCookie);

log.info("JWT 생성 완료. JWT 토큰을 쿠키로 응답에 포함시킴");
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import intbyte4.learnsmate.admin.service.AdminService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -39,40 +40,46 @@ protected boolean shouldNotFilter(HttpServletRequest request) throws ServletExce
}

// JWT 토큰을 검사하고 인증을 처리하는 실제 필터 로직. 만약 유효한 JWT 토큰이 있다면 인증을 처리하고, 없으면 다음 필터로 진행.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

log.info("UsernamePasswordAuthenticationFilter보다 먼저 동작하는 필터");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// Authorization 헤더에서 JWT 토큰을 추출
String authorizationHeader = request.getHeader("Authorization");
log.info("jwtFilter의 getHeader('Authorization'): {}", authorizationHeader);
log.info("JwtFilter 실행 - 쿠키에서 토큰 추출 시도");

// 쿠키에서 토큰 가져오기
String token = null;

// Authorization 헤더에 "Bearer "로 시작하는 토큰이 있으면 해당 토큰을 추출
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7); // "Bearer " 이후의 토큰 값만 추출
log.info("Bearer 토큰 추출 완료: {}", token);
if (request.getCookies() != null) {
for (Cookie cookie : request.getCookies()) {
log.info("쿠키 이름: {}, 쿠키 값: {}", cookie.getName(), cookie.getValue());
if ("token".equals(cookie.getName())) {
token = cookie.getValue();
log.info("추출된 토큰: {}", token);
break;
}
}
} else {
// 헤더에 토큰이 없으면 쿼리 파라미터에서 token 값을 찾음
token = request.getParameter("token");
log.info("OAuth 로그인: 쿼리 파라미터에서 토큰 추출. 토큰 : {}", token);
log.warn("요청에 쿠키가 없습니다.");
}

// 토큰이 있을 경우에만 유효성 검사 및 인증 처리 ->
// 토큰이 유효하면 인증 정보를 SecurityContextHolder에 저장하고,
// 이후의 필터가 인증 정보를 사용 가능하게 함.

// 토큰 유효성 검사 및 인증 객체 설정
if (token != null && jwtUtil.validateToken(token)) {
Authentication authentication = jwtUtil.getAuthentication(token); // 토큰으로부터 인증 객체 추출
log.info("JwtFilter를 통과한 유효한 토큰을 통해 security가 관리할 principal 객체: {}", authentication);
SecurityContextHolder.getContext().setAuthentication(authentication); // 인증된 객체를 SecurityContext에 설정하여 이후의 요청에서 인증 정보를 사용할 수 있도록 함
log.info("유효한 토큰입니다. SecurityContext 설정 시작");

// 토큰으로부터 인증 객체 생성
Authentication authentication = jwtUtil.getAuthentication(token);

log.info("생성된 인증 객체: {}", authentication);

// SecurityContext에 인증 객체 설정
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
log.warn("유효하지 않은 토큰이거나 토큰이 없습니다.");
log.warn("유효하지 않은 토큰이거나 토큰이 없습니다.!!!!!!!");
}

// 인증 처리가 끝난 후, 다음 필터로 요청을 전달 -> 실행될 다음 필터는 UsernamePasswordAuthenticationFilter가 처리
// 다음 필터 실행
filterChain.doFilter(request, response);
}

}

Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ protected SecurityFilterChain configure(HttpSecurity http) throws Exception {
.requestMatchers(new AntPathRequestMatcher("/users/verification-email/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/users/verify-code")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/users/send-sms")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/admin/**","GET")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/users/**", "POST")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/users/**", "OPTIONS")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/users/nickname/check", "GET")).permitAll()
Expand Down Expand Up @@ -122,7 +123,6 @@ public CorsConfigurationSource corsConfigurationSource() {
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")); // 허용할 HTTP 메서드 설정
configuration.setAllowCredentials(true); // 인증 정보 허용 (쿠키 등)
configuration.setAllowedHeaders(Collections.singletonList("*")); // 모든 헤더 허용
configuration.setExposedHeaders(Arrays.asList("Authorization")); // 노출할 헤더 설정
configuration.setMaxAge(3600L); // 1시간 동안 캐시

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
Expand Down