diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 13292823..e89c5aa9 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,6 +13,7 @@ on: jobs: deploy: runs-on: ubuntu-latest + steps: - name: Checkout code uses: actions/checkout@v3 @@ -25,21 +26,22 @@ jobs: aws-region: ap-northeast-2 - name: Login to Amazon ECR - id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - name: Build and push + - name: Build and push Docker image (single arch) uses: docker/build-push-action@v4 with: context: . - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 # ⚡ 멀티 플랫폼 제거 → 빌드 속도 2~3배 향상 push: true tags: 867344478016.dkr.ecr.ap-northeast-2.amazonaws.com/enjoy-app:latest + cache-from: type=gha + cache-to: type=gha,mode=max - - name: SSH into EC2 server and deploy + - name: SSH into EC2 and deploy uses: appleboy/ssh-action@master with: host: ${{ secrets.EC2_HOST }} @@ -50,5 +52,4 @@ jobs: cd /home/ec2-user/back docker-compose down docker pull 867344478016.dkr.ecr.ap-northeast-2.amazonaws.com/enjoy-app:latest - docker system prune -f - docker-compose up -d \ No newline at end of file + docker-compose up -d diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a868768e..56cbdbb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,6 @@ on: branches: [ main ] env: - JWT_SECRET: ${{ secrets.JWT_SECRET }} - KAKAO_CLIENT_ID: ${{ secrets.KAKAO_CLIENT_ID }} - KAKAO_CLIENT_SECRET: ${{ secrets.KAKAO_CLIENT_SECRET }} - KAKAO_REDIRECT_URI: ${{ secrets.KAKAO_REDIRECT_URI }} RDS_ENDPOINT: ${{ secrets.RDS_ENDPOINT }} RDS_PORT: ${{ secrets.RDS_PORT }} RDS_USERNAME: ${{ secrets.MYSQL_DB_USERNAME }} @@ -61,10 +57,13 @@ jobs: - name: Build and Test with Gradle env: SPRING_PROFILES_ACTIVE: ci + MYSQL_DB_USERNAME: ${{ secrets.MYSQL_DB_USERNAME }} + MYSQL_DB_PASSWORD: ${{ secrets.MYSQL_DB_PASSWORD }} + MYSQL_DB_NAME: ${{ secrets.MYSQL_DB_NAME }} + MYSQL_DB_PORT: ${{ secrets.RDS_PORT }} + MYSQL_DB_HOST: ${{ secrets.RDS_ENDPOINT }} run: | - ./gradlew clean - ./gradlew compileJava --stacktrace - ./gradlew build --info + ./gradlew clean build --info - name: Upload Test Report Artifact if: failure() diff --git a/src/main/java/com/example/smartair/config/SecurityConfig.java b/src/main/java/com/example/smartair/config/SecurityConfig.java deleted file mode 100644 index 857004ad..00000000 --- a/src/main/java/com/example/smartair/config/SecurityConfig.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.smartair.config; - -import com.example.smartair.jwt.*; - -import com.example.smartair.repository.userRepository.RefreshRepository; -import com.example.smartair.repository.userRepository.UserRepository; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.web.cors.CorsConfiguration; - -import java.util.Arrays; -import java.util.Collections; -@Configuration -@EnableWebSecurity -public class SecurityConfig { - - private final AuthenticationConfiguration authenticationConfiguration; - private final JWTUtil jwtUtil; - private final UserRepository userRepository; - private final RefreshRepository refreshRepository; - - public SecurityConfig(AuthenticationConfiguration authenticationConfiguration, JWTUtil jwtUtil, - RefreshRepository refreshRepository, UserRepository userRepository) { - this.authenticationConfiguration = authenticationConfiguration; - this.jwtUtil = jwtUtil; - this.refreshRepository = refreshRepository; - this.userRepository = userRepository; - } - - @Bean - public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { - return configuration.getAuthenticationManager(); - } - - @Bean - public BCryptPasswordEncoder bCryptPasswordEncoder() { - return new BCryptPasswordEncoder(); - } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - - http - .cors(corsCustomizer -> corsCustomizer.configurationSource(request -> { - CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOriginPatterns(Arrays.asList( - "https://smartair.site", - "https://smartair-frontend.vercel.app", - "http://localhost:3000" - )); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH")); // 허용할 HTTP 메서드 - config.setAllowCredentials(true); - config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type")); // 허용할 헤더 - config.setExposedHeaders(Collections.singletonList("Authorization")); // 노출할 헤더 - config.setMaxAge(3600L); // 캐싱 시간 - return config; - })) - .csrf(csrf -> csrf.disable()) - .formLogin(form -> form.disable()) - .httpBasic(httpBasic -> httpBasic.disable()) - .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/login", "/join", "/reissue", "/oauth2/**", - "/swagger-ui/**", "/v3/api-docs/**", "/swagger-resources/**", "/webjars/**" - - ).permitAll() - .requestMatchers(HttpMethod.GET, "/sensorMappingWithRoom").permitAll() - .requestMatchers(HttpMethod.POST, "/api/reports/anomaly").permitAll() - .requestMatchers(HttpMethod.POST, "/predictedAirQuality").permitAll() - .anyRequest().permitAll() - ); - - // JWT 필터 & 커스텀 필터 설정 - http.addFilterBefore(new CustomLogoutFilter(jwtUtil, refreshRepository), LogoutFilter.class); - http.addFilterBefore(new JWTFilter(jwtUtil, userRepository), LoginFilter.class); - http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration), jwtUtil, refreshRepository), - UsernamePasswordAuthenticationFilter.class); - - return http.build(); - } -} - - diff --git a/src/main/java/com/example/smartair/controller/userController/JoinController.java b/src/main/java/com/example/smartair/controller/userController/JoinController.java deleted file mode 100644 index 0c695ae1..00000000 --- a/src/main/java/com/example/smartair/controller/userController/JoinController.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.example.smartair.controller.userController; - -import com.example.smartair.dto.userDto.JoinDTO; -import com.example.smartair.exception.CustomException; -import com.example.smartair.exception.ErrorCode; -import com.example.smartair.service.userService.JoinService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Collections; - -@Slf4j -@RestController -public class JoinController implements JoinControllerDocs { - private final JoinService joinService; - - public JoinController(JoinService joinService){ - this.joinService = joinService; - } - - @PostMapping("/join") - public ResponseEntity joinProcess(@RequestBody JoinDTO joinDTO){ - boolean success = joinService.joinProcess(joinDTO); - if (success) { - return ResponseEntity.ok(Collections.singletonMap("message", "success")); - } else { - throw new CustomException(ErrorCode.USER_ALREADY_EXISTS); // 기존 방식 대신 예외 던지기 - } - } - -} diff --git a/src/main/java/com/example/smartair/controller/userController/JoinControllerDocs.java b/src/main/java/com/example/smartair/controller/userController/JoinControllerDocs.java deleted file mode 100644 index 0bf9dbcf..00000000 --- a/src/main/java/com/example/smartair/controller/userController/JoinControllerDocs.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.example.smartair.controller.userController; - -import com.example.smartair.dto.userDto.JoinDTO; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.parameters.RequestBody; -import org.springframework.http.ResponseEntity; - -public interface JoinControllerDocs { - - @Operation( - summary = "사용자 회원가입", - description = """ - ## 사용자 회원가입 요청 - - 사용자가 이메일, 닉네임, 비밀번호, 역할(ROLE)을 입력하여 회원가입을 시도합니다. - - --- - - **요청 형식 (RequestBody)** - ```json - { - "email": "example@example.com", - "nickname": "홍길동", - "username" : "이름", - "password": "password123", - "role": "USER" - } - ``` - - **Role 값 예시** - - USER - - ADMIN - - MANAGER - - --- - - **응답** - - `200 OK`: 회원가입 성공 - `{ "message": "success" }` - - `400 Bad Request`: 이미 존재하는 사용자 - `{ "errorCode": "USER_ALREADY_EXISTS" }` - """, - requestBody = @RequestBody( - description = "회원가입에 필요한 사용자 정보", - required = true, - content = @Content( - schema = @Schema(implementation = JoinDTO.class), - examples = @ExampleObject( - name = "Join Request Example", - value = "{\n \"email\": \"test@example.com\",\n \"nickname\": \"tester\", \n \"username\": \"이름\",\n \"password\": \"securePass123\",\n \"role\": \"USER\"\n}" - ) - ) - ) - ) - ResponseEntity joinProcess(JoinDTO joinDTO); -} diff --git a/src/main/java/com/example/smartair/controller/userController/KakaoLoginPageController.java b/src/main/java/com/example/smartair/controller/userController/KakaoLoginPageController.java deleted file mode 100644 index 3036d814..00000000 --- a/src/main/java/com/example/smartair/controller/userController/KakaoLoginPageController.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.example.smartair.controller.userController; - -import org.checkerframework.checker.units.qual.K; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping("/login") -public class KakaoLoginPageController implements KakaoLoginPageControllerDocs { - - @Value("${spring.security.oauth2.client.registration.kakao.client-id}") - private String client_id; - - @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") - private String redirect_uri; - - @GetMapping("/page") - public String loginPage(Model model) { - String location = "https://kauth.kakao.com/oauth/authorize?response_type=code&client_id="+client_id+"&redirect_uri="+redirect_uri; - model.addAttribute("location", location); - - return "login"; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/controller/userController/KakaoLoginPageControllerDocs.java b/src/main/java/com/example/smartair/controller/userController/KakaoLoginPageControllerDocs.java deleted file mode 100644 index 0265a5ab..00000000 --- a/src/main/java/com/example/smartair/controller/userController/KakaoLoginPageControllerDocs.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.example.smartair.controller.userController; - - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -@Tag(name = "카카오 로그인 페이지", description = "카카오 OAuth2 로그인 페이지 렌더링 API") -@RequestMapping("/login") -public interface KakaoLoginPageControllerDocs { - - @Operation( - summary = "카카오 로그인 페이지 링크 제공", - description = """ - ## 카카오 로그인 링크 렌더링 - - 클라이언트에서 카카오 로그인을 요청할 수 있도록 OAuth2 인증 URL을 포함한 login 페이지를 반환합니다. - - --- - **응답 (`Model`)** - - `location`: 카카오 로그인 redirect URL (String) - - 해당 location 값을 통해 사용자는 Kakao OAuth2 인증을 시작할 수 있습니다. - - --- - **예시 URL 형식** - ```text - https://kauth.kakao.com/oauth/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_uri} - ``` - """ - ) - @GetMapping("/page") - String loginPage(Model model); -} - diff --git a/src/main/java/com/example/smartair/controller/userController/LoginController.java b/src/main/java/com/example/smartair/controller/userController/LoginController.java index c27ea2c1..70f1181c 100644 --- a/src/main/java/com/example/smartair/controller/userController/LoginController.java +++ b/src/main/java/com/example/smartair/controller/userController/LoginController.java @@ -1,22 +1,8 @@ package com.example.smartair.controller.userController; -import com.example.smartair.dto.userDto.LoginDTO; -import com.example.smartair.dto.userDto.TokenDto; -import com.example.smartair.entity.login.CustomUserDetails; -import com.example.smartair.service.userService.LoginService; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @Tag(name = "로그인", description = "로그인 관련 API") @@ -25,7 +11,7 @@ @AllArgsConstructor public class LoginController { - private final LoginService loginService; + diff --git a/src/main/java/com/example/smartair/controller/userController/ReissueController.java b/src/main/java/com/example/smartair/controller/userController/ReissueController.java deleted file mode 100644 index 090f80bc..00000000 --- a/src/main/java/com/example/smartair/controller/userController/ReissueController.java +++ /dev/null @@ -1,139 +0,0 @@ -package com.example.smartair.controller.userController; - -import com.example.smartair.entity.login.RefreshEntity; -import com.example.smartair.jwt.JWTUtil; -import com.example.smartair.repository.userRepository.RefreshRepository; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.JwtException; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.Date; -import java.util.Map; - -@RestController -@RequestMapping("/reissue") -@Slf4j -public class ReissueController implements ReissueControllerDocs { - - private final JWTUtil jwtUtil; - private final RefreshRepository refreshRepository; - - private final long ACCESS_TOKEN_EXP = 10000L * 60 * 30; // 30분 - private final long REFRESH_TOKEN_EXP = 1000L * 60 * 60 * 24 * 7; // 7일 - - public ReissueController(JWTUtil jwtUtil, RefreshRepository refreshRepository) { - this.jwtUtil = jwtUtil; - this.refreshRepository = refreshRepository; - } - - /** - * Refresh 토큰을 이용한 Access 토큰 재발급 - */ - @PostMapping - public ResponseEntity reissue(HttpServletRequest request, HttpServletResponse response) { - - // 1. 쿠키에서 refresh token 추출 - String refresh = extractRefreshTokenFromCookie(request); - if (refresh == null) { - return buildErrorResponse("E001", "Refresh token not found in cookies", HttpStatus.BAD_REQUEST); - } - - // 2. 토큰 유효성 검사 - try { - jwtUtil.isExpired(refresh); - } catch (ExpiredJwtException e) { - log.warn("Refresh token expired: {}", refresh); - return buildErrorResponse("E002", "Refresh token expired", HttpStatus.UNAUTHORIZED); - } catch (JwtException | IllegalArgumentException e) { - log.warn("Invalid refresh token format: {}", e.getMessage()); - return buildErrorResponse("E003", "Invalid refresh token", HttpStatus.UNAUTHORIZED); - } - - // 3. 토큰 유형 확인 (access or refresh) - if (!"refresh".equals(jwtUtil.getCategory(refresh))) { - return buildErrorResponse("E004", "Token is not a refresh token", HttpStatus.UNAUTHORIZED); - } - - // 4. DB에서 refresh 토큰 존재 확인 - if (!refreshRepository.existsByRefresh(refresh)) { - return buildErrorResponse("E005", "Refresh token not found in DB", HttpStatus.UNAUTHORIZED); - } - - // 5. 사용자 정보 추출 - String username = jwtUtil.getUsername(refresh); - String role = jwtUtil.getRole(refresh); - String email = jwtUtil.getEmail(refresh); - - // 6. 새 토큰 생성 - String newAccess = jwtUtil.createJwt("access", username, role, email, ACCESS_TOKEN_EXP); - String newRefresh = jwtUtil.createJwt("refresh", username, role, email, REFRESH_TOKEN_EXP); - - // 7. 기존 refresh 삭제 후 새 refresh 저장 - refreshRepository.deleteByRefresh(refresh); - saveNewRefreshToken(username, newRefresh, REFRESH_TOKEN_EXP); - - // 8. 새 토큰 전송 - response.setHeader("access", newAccess); - response.addCookie(createCookie("refresh", newRefresh)); - - log.info("New tokens issued for user '{}'", username); - return ResponseEntity.ok(Map.of( - "code", "S001", - "message", "New tokens issued" - )); - } - - /** - * 쿠키에서 refresh 토큰 추출 - */ - private String extractRefreshTokenFromCookie(HttpServletRequest request) { - Cookie[] cookies = request.getCookies(); - if (cookies == null) return null; - for (Cookie cookie : cookies) { - if ("refresh".equals(cookie.getName())) { - return cookie.getValue(); - } - } - return null; - } - - /** - * 새 Refresh 토큰 저장 - */ - private void saveNewRefreshToken(String username, String refresh, long expireMs) { - Date expiration = new Date(System.currentTimeMillis() + expireMs); - RefreshEntity entity = new RefreshEntity(); - entity.setUsername(username); - entity.setRefresh(refresh); - entity.setExpiration(expiration.toString()); - refreshRepository.save(entity); - } - - /** - * Refresh 토큰을 담는 HttpOnly 쿠키 생성 - */ - private Cookie createCookie(String key, String value) { - Cookie cookie = new Cookie(key, value); - cookie.setMaxAge((int) (REFRESH_TOKEN_EXP / 1000)); - cookie.setSecure(true); - cookie.setHttpOnly(true); - cookie.setPath("/"); - return cookie; - } - - /** - * 에러 응답 생성기 - */ - private ResponseEntity> buildErrorResponse(String code, String message, HttpStatus status) { - return ResponseEntity.status(status).body(Map.of( - "code", code, - "error", message - )); - } -} diff --git a/src/main/java/com/example/smartair/controller/userController/ReissueControllerDocs.java b/src/main/java/com/example/smartair/controller/userController/ReissueControllerDocs.java deleted file mode 100644 index 64d9d115..00000000 --- a/src/main/java/com/example/smartair/controller/userController/ReissueControllerDocs.java +++ /dev/null @@ -1,68 +0,0 @@ -package com.example.smartair.controller.userController; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; - -@Tag(name = "JWT 토큰 재발급 API", description = "Access Token 재발급 및 Refresh Token Rotation 처리") -public interface ReissueControllerDocs { - - @Operation( - summary = "JWT 토큰 재발급", - description = """ - ## 설명 - - 쿠키에 저장된 `refresh` 토큰을 기반으로 Access Token을 재발급합니다. - - 기존 Refresh 토큰은 폐기되고 새 Refresh + Access 토큰이 함께 발급됩니다. - - 새 Access는 `access` 헤더에, Refresh는 `Set-Cookie`로 응답됩니다. - - ## 요청 - - `refresh` 토큰은 **HttpOnly 쿠키**로 전송되어야 합니다. - - 별도의 Request Body는 없습니다. - - ## 응답 - - 성공: 새 access 토큰과 refresh 토큰이 포함됨 (access는 헤더, refresh는 쿠키) - - 실패: 상태 코드와 에러 메시지 포함된 JSON 반환 - - """, - responses = { - @ApiResponse( - responseCode = "200", - description = "Access + Refresh 토큰 재발급 성공", - content = @Content(mediaType = "application/json", examples = @ExampleObject(value = """ - { - "code": "S001", - "message": "New tokens issued" - } - """)) - ), - @ApiResponse( - responseCode = "400", - description = "요청 쿠키에 Refresh 토큰 없음", - content = @Content(mediaType = "application/json", examples = @ExampleObject(value = """ - { - "code": "E001", - "error": "Refresh token not found in cookies" - } - """)) - ), - @ApiResponse( - responseCode = "401", - description = "Refresh 토큰 만료, 위조, DB 없음 등 인증 오류", - content = @Content(mediaType = "application/json", examples = @ExampleObject(value = """ - { - "code": "E002", - "error": "Refresh token expired" - } - """)) - ) - } - ) - ResponseEntity reissue(HttpServletRequest request, HttpServletResponse response); -} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/controller/userController/UserController.java b/src/main/java/com/example/smartair/controller/userController/UserController.java deleted file mode 100644 index fc436475..00000000 --- a/src/main/java/com/example/smartair/controller/userController/UserController.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.example.smartair.controller.userController; - -import com.example.smartair.dto.userDto.KakaoUserInfoResponseDTO; -import com.example.smartair.dto.userDto.UserInfoDTO; -import com.example.smartair.entity.login.CustomUserDetails; -import com.example.smartair.entity.login.RefreshEntity; -import com.example.smartair.entity.user.User; -import com.example.smartair.jwt.JWTUtil; -import com.example.smartair.repository.userRepository.RefreshRepository; -import com.example.smartair.service.userService.KakaoService; -import jakarta.servlet.http.Cookie; -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.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import java.util.Date; - -@Slf4j -@RestController -@RequiredArgsConstructor -public class UserController implements UserControllerDocs{ - - private final KakaoService kakaoService; - private final JWTUtil jwtUtil; - private final RefreshRepository refreshRepository; - - @GetMapping("/login/oauth2/kakao") - public ResponseEntity callback(@RequestParam("code") String code) { - log.info("kakao code : " + code); - String accessToken = kakaoService.getAccessTokenFromKakao(code); - - kakaoService.debugUserInfoResponse(accessToken); - KakaoUserInfoResponseDTO userInfo = kakaoService.getUserInfo(accessToken); - - User user = kakaoService.findUserOrCreateUser(userInfo); - - String access = jwtUtil.createJwt("access", user.getUsername(), String.valueOf(user.getRole()), user.getEmail(), 600000L); - String refresh = jwtUtil.createJwt("refresh", user.getUsername(), String.valueOf(user.getRole()), user.getEmail(),86400000L); - - addRefreshEntity(user.getUsername(), refresh, 86400000L); - return ResponseEntity.ok() - .header("access", access) - .header("Set-Cookie", createCookie("refresh",refresh).toString()) - .build(); - } - private void addRefreshEntity(String username, String refresh, Long expiredMs) { - - Date date = new Date(System.currentTimeMillis() + expiredMs); - - RefreshEntity refreshEntity = new RefreshEntity(); - refreshEntity.setUsername(username); - refreshEntity.setRefresh(refresh); - refreshEntity.setExpiration(date.toString()); - - refreshRepository.save(refreshEntity); - } - private Cookie createCookie(String key, String value) { - - Cookie cookie = new Cookie(key, value); - cookie.setMaxAge(24*60*60); - //cookie.setSecure(true); 쿠키 암호화 전송 https://howisitgo1ng.tistory.com/entry/HTTP-Only%EC%99%80-Secure-Cookie - //cookie.setPath("/"); - cookie.setHttpOnly(true); - - return cookie; - } - - @GetMapping("/userinfo") - public ResponseEntity getUserInfo(@AuthenticationPrincipal CustomUserDetails userDetails){ - if(userDetails == null){ - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid Token"); - } - - User user =userDetails.getUser(); - UserInfoDTO userInfo = new UserInfoDTO(); - userInfo.setId(user.getId()); - userInfo.setUsername(user.getUsername()); - userInfo.setEmail(user.getEmail()); - userInfo.setRole(String.valueOf(user.getRole())); - userInfo.setLoginType(user.getLoginType()); - - return ResponseEntity.ok(userInfo); - } - - -} diff --git a/src/main/java/com/example/smartair/controller/userController/UserControllerDocs.java b/src/main/java/com/example/smartair/controller/userController/UserControllerDocs.java deleted file mode 100644 index 9182a2cc..00000000 --- a/src/main/java/com/example/smartair/controller/userController/UserControllerDocs.java +++ /dev/null @@ -1,62 +0,0 @@ -package com.example.smartair.controller.userController; - -import com.example.smartair.dto.userDto.KakaoUserInfoResponseDTO; -import com.example.smartair.dto.userDto.UserInfoDTO; -import com.example.smartair.entity.login.CustomUserDetails; -import io.swagger.v3.oas.annotations.Operation; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.RequestParam; - -public interface UserControllerDocs { - - @Operation( - summary = "카카오 로그인 콜백 처리", - description = """ - ## 카카오 로그인 콜백 처리 - - 카카오 로그인 후 받은 인가 코드(code)를 이용하여 사용자 인증을 처리하고 - Access Token 및 Refresh Token을 발급합니다. - - --- - - **요청 파라미터** - - `code` (String): 카카오 OAuth2 서버에서 발급받은 인가 코드 - - --- - - **응답 헤더** - - `access`: Access Token - - `Set-Cookie`: Refresh Token을 담은 HttpOnly 쿠키 - - --- - - **응답 본문** - - 성공 시: `200 OK` - - 실패 시: 에러 메시지 포함 - """ - ) - ResponseEntity callback(@RequestParam("code") String code); - - @Operation( - summary = "사용자 정보 조회", - description = """ - ## 사용자 정보 조히 - - 로그인된 사용자의 정보를 반환합니다. - - --- - - **요청 정보** - - 인증 사용자 정보는 `@AuthenticationPrincipal`을 통해 전달됩니다. - - --- - - **응답** - - 성공 시: `UserInfoDTO 객체` (ID, 이름, 이메일, 권한, 로그인 방식) - - 실패 시: 인증 오류 메시지 - """ - ) - ResponseEntity getUserInfo(@AuthenticationPrincipal CustomUserDetails userDetails); -} - diff --git a/src/main/java/com/example/smartair/dto/userDto/CustomResponseDTO.java b/src/main/java/com/example/smartair/dto/userDto/CustomResponseDTO.java deleted file mode 100644 index 6ea3bbfd..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/CustomResponseDTO.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.smartair.dto.userDto; - -import lombok.Getter; -import lombok.Setter; - -@Setter -@Getter -public class CustomResponseDTO { - public Double temperature; - - public Double moisture; -} diff --git a/src/main/java/com/example/smartair/dto/userDto/JoinDTO.java b/src/main/java/com/example/smartair/dto/userDto/JoinDTO.java deleted file mode 100644 index ceff4ed1..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/JoinDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.smartair.dto.userDto; - -import lombok.*; - -@Getter -@Setter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class JoinDTO { - private String username; - private String password; - private String email; - private String role; // 입력 예시 :USER, ADMIN, MANAGER - -} diff --git a/src/main/java/com/example/smartair/dto/userDto/KakaoTokenResponseDTO.java b/src/main/java/com/example/smartair/dto/userDto/KakaoTokenResponseDTO.java deleted file mode 100644 index 39e98aa4..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/KakaoTokenResponseDTO.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.smartair.dto.userDto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor //역직렬화를 위한 기본 생성자 -@JsonIgnoreProperties(ignoreUnknown = true) -public class KakaoTokenResponseDTO { - @JsonProperty("token_type") //변환을 위한 것 - public String tokenType; - @JsonProperty("access_token") - public String accessToken; - @JsonProperty("id_token") - public String idToken; - @JsonProperty("expires_in") - public Integer expiresIn; - @JsonProperty("refresh_token") - public String refreshToken; - @JsonProperty("refresh_token_expires_in") - public Integer refreshTokenExpiresIn; - @JsonProperty("scope") - public String scope; -} diff --git a/src/main/java/com/example/smartair/dto/userDto/KakaoUserInfoResponseDTO.java b/src/main/java/com/example/smartair/dto/userDto/KakaoUserInfoResponseDTO.java deleted file mode 100644 index b58ca8fa..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/KakaoUserInfoResponseDTO.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.example.smartair.dto.userDto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) // 알 수 없는 필드는 무시 -public class KakaoUserInfoResponseDTO { - - @JsonProperty("id") - private Long id; - - @JsonProperty("properties") - private Properties properties; - - @JsonProperty("kakao_account") - private KakaoAccount kakaoAccount; - - @Getter - @NoArgsConstructor - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Properties { - @JsonProperty("nickname") - private String nickname; - - @JsonProperty("profile_image") - private String profileImage; - - @JsonProperty("thumbnail_image") - private String thumbnailImage; - } - - @Getter - @NoArgsConstructor - @JsonIgnoreProperties(ignoreUnknown = true) - public static class KakaoAccount { - @JsonProperty("email") - private String email; - } - public String getNickname() { - return properties != null ? properties.getNickname() : null; - } - - public String getEmail() { - return kakaoAccount != null ? kakaoAccount.getEmail() : null; - } -} diff --git a/src/main/java/com/example/smartair/dto/userDto/LoginDTO.java b/src/main/java/com/example/smartair/dto/userDto/LoginDTO.java deleted file mode 100644 index 4f859b16..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/LoginDTO.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.smartair.dto.userDto; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class LoginDTO { - private String email; - private String password; -} diff --git a/src/main/java/com/example/smartair/dto/userDto/TokenDto.java b/src/main/java/com/example/smartair/dto/userDto/TokenDto.java deleted file mode 100644 index 22489dda..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/TokenDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.example.smartair.dto.userDto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -// TokenDto.java -@Getter -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class TokenDto { - private String grantType; - private String accessToken; - private String refreshToken; -} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/dto/userDto/UserDetailResponseDto.java b/src/main/java/com/example/smartair/dto/userDto/UserDetailResponseDto.java deleted file mode 100644 index 91bc5ec4..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/UserDetailResponseDto.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.example.smartair.dto.userDto; - -import com.example.smartair.entity.user.Role; -import com.example.smartair.entity.user.User; -import lombok.Builder; -import lombok.Getter; -import java.time.LocalDateTime; - -@Getter -@Builder -public class UserDetailResponseDto { - private Long id; - private String username; - private String email; - private Role role; - private String loginType; - private LocalDateTime createdAt; - private LocalDateTime modifiedAt; - private int participatedRoomCount; - - public static UserDetailResponseDto from(User user) { - return UserDetailResponseDto.builder() - .id(user.getId()) - .username(user.getUsername()) - .email(user.getEmail()) - .loginType(user.getLoginType()) - .createdAt(user.getCreateDate()) - .modifiedAt(user.getModifiedDate()) - .build(); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/dto/userDto/UserInfoDTO.java b/src/main/java/com/example/smartair/dto/userDto/UserInfoDTO.java deleted file mode 100644 index 61920472..00000000 --- a/src/main/java/com/example/smartair/dto/userDto/UserInfoDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.smartair.dto.userDto; - -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -public class UserInfoDTO { - private Long id; - private String username; - private String email; - private String role; - private String loginType; -} diff --git a/src/main/java/com/example/smartair/jwt/CustomLogoutFilter.java b/src/main/java/com/example/smartair/jwt/CustomLogoutFilter.java deleted file mode 100644 index bfeaaec1..00000000 --- a/src/main/java/com/example/smartair/jwt/CustomLogoutFilter.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.example.smartair.jwt; - -import com.example.smartair.repository.userRepository.RefreshRepository; -import io.jsonwebtoken.ExpiredJwtException; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.web.filter.GenericFilterBean; - -import java.io.IOException; - -public class CustomLogoutFilter extends GenericFilterBean { - - private final JWTUtil jwtUtil; - private final RefreshRepository refreshRepository; - - public CustomLogoutFilter(JWTUtil jwtUtil, RefreshRepository refreshRepository) { - - this.jwtUtil = jwtUtil; - this.refreshRepository = refreshRepository; - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - - doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); - } - - private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { - - //path and method verify - String requestUri = request.getRequestURI(); - if (!requestUri.matches("^\\/logout$")) { - filterChain.doFilter(request, response); - return; - } - String requestMethod = request.getMethod(); - if (!requestMethod.equals("POST")) { - - filterChain.doFilter(request, response); - return; - } - - //get refresh token - String refresh = null; - Cookie[] cookies = request.getCookies(); - for (Cookie cookie : cookies) { - - if (cookie.getName().equals("refresh")) { - refresh = cookie.getValue(); - break; - } - } - - //refresh null check - if (refresh == null) { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return; - } - - //expired check - try { - jwtUtil.isExpired(refresh); - } catch (ExpiredJwtException e) { - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return; - } - - // 토큰이 refresh인지 확인 (발급시 페이로드에 명시) - String category = jwtUtil.getCategory(refresh); - if (!category.equals("refresh")) { - - //response status code - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return; - } - - //DB에 저장되어 있는지 확인 - Boolean isExist = refreshRepository.existsByRefresh(refresh); - if (!isExist) { - //response status code - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - return; - } - - //로그아웃 진행 - //Refresh 토큰 DB에서 제거 - refreshRepository.deleteByRefresh(refresh); - - //Refresh 토큰 Cookie 값 0 - Cookie cookie = new Cookie("refresh", null); - cookie.setMaxAge(0); - cookie.setPath("/"); - - response.addCookie(cookie); - response.setStatus(HttpServletResponse.SC_OK); - } -} \ No newline at end of file diff --git a/src/main/java/com/example/smartair/jwt/JWTFilter.java b/src/main/java/com/example/smartair/jwt/JWTFilter.java deleted file mode 100644 index 314af75a..00000000 --- a/src/main/java/com/example/smartair/jwt/JWTFilter.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.example.smartair.jwt; - -import com.example.smartair.entity.login.CustomUserDetails; - - -import com.example.smartair.entity.user.User; - -import com.example.smartair.repository.userRepository.UserRepository; - -import io.jsonwebtoken.ExpiredJwtException; -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.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Optional; - -public class JWTFilter extends OncePerRequestFilter { - private final JWTUtil jwtUtil; - private final UserRepository userRepository; - - public JWTFilter(JWTUtil jwtUtil, UserRepository userRepository){ - this.jwtUtil = jwtUtil; - this.userRepository = userRepository; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - String authorizationHeader = request.getHeader("Authorization"); - - if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) { - filterChain.doFilter(request, response); - return; - } - - String accessToken = authorizationHeader.substring(7); // "Bearer " 이후만 추출 - - - if (accessToken == null) { - filterChain.doFilter(request, response);// 토큰이 없다면 다음 필터로 넘김 - return; - } - - // 토큰 만료 여부 확인, 만료시 다음 필터로 넘기지 않음 - try { - jwtUtil.isExpired(accessToken); - } catch (ExpiredJwtException e) { - - //response body - PrintWriter writer = response.getWriter(); - writer.print("access token expired"); - - //response status code - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - - // 토큰이 access인지 확인 (발급시 페이로드에 명시) - String category = jwtUtil.getCategory(accessToken); - - if (!category.equals("access")) { - - //response body - PrintWriter writer = response.getWriter(); - writer.print("invalid access token"); - - //response status code - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - return; - } - - // username, role 값을 획득 - String username = jwtUtil.getUsername(accessToken); - String role = jwtUtil.getRole(accessToken); - String email = jwtUtil.getEmail(accessToken); - - Optional optionalUser = userRepository.findByEmail(email); - User user = optionalUser.get(); - CustomUserDetails customUserDetails = new CustomUserDetails(user); - - Authentication authToken = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities()); - // 로그인된 사용자 정보, 비밀번호는 필요 없음 (JWT는 이미 인증된 상태), 권한 정보 - - SecurityContextHolder.getContext().setAuthentication(authToken); - - filterChain.doFilter(request, response); - } - -} diff --git a/src/main/java/com/example/smartair/jwt/JWTUtil.java b/src/main/java/com/example/smartair/jwt/JWTUtil.java deleted file mode 100644 index 201669da..00000000 --- a/src/main/java/com/example/smartair/jwt/JWTUtil.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.example.smartair.jwt; - - -import com.example.smartair.entity.user.Role; - -import io.jsonwebtoken.Jwts; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.util.Date; - -@Component -public class JWTUtil { - private SecretKey secretKey; - - public JWTUtil(@Value("${spring.jwt.secret}") String secret) { - secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), - Jwts.SIG.HS256.key().build().getAlgorithm());//SHA256 알고리즘을 통해 SecurityKey 생성 - } - - public String getUsername(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("username", String.class); - } - //Jwts.parser() : JWT를 해석하고 검증할 수 있도록 도와줌 - //.verifyWith(secretKey) : JWT 서명을 검증하는 단계, secretKey로 토큰의 서명이 올바른지 확 - //.parseSingedClaims(token) : JWT 토큰을 해석하여 전체 JWT 객체 반환(Header +payload) - public String getRole(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("role", String.class); - } - public String getEmail(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("email", String.class); - } - public String getCategory(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().get("category", String.class); - } - - public Boolean isExpired(String token) { - return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload().getExpiration().before(new Date()); // 현재시간보다 - } - - - public String createJwt(String category, String username, String role,String email, Long expiredMs) { - return Jwts.builder() - .claim("category", category) - .claim("username", username) - .claim("role", role) - .claim("email", email) - .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + expiredMs)) - .signWith(secretKey) - .compact(); //JWT 문자열 반환 - } - -} - diff --git a/src/main/java/com/example/smartair/jwt/LoginFilter.java b/src/main/java/com/example/smartair/jwt/LoginFilter.java deleted file mode 100644 index 6c2c5d47..00000000 --- a/src/main/java/com/example/smartair/jwt/LoginFilter.java +++ /dev/null @@ -1,136 +0,0 @@ -package com.example.smartair.jwt; - - -import com.example.smartair.dto.userDto.LoginDTO; -import com.example.smartair.dto.userDto.TokenDto; -import com.example.smartair.entity.login.CustomUserDetails; -import com.example.smartair.entity.login.RefreshEntity; - - -import com.example.smartair.repository.userRepository.RefreshRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletInputStream; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.AuthenticationServiceException; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.util.StreamUtils; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Collection; -import java.util.Date; -import java.util.Iterator; - -public class LoginFilter extends UsernamePasswordAuthenticationFilter { - private final AuthenticationManager authenticationManager; - //authenticationManager의 역할 : 로그인 정보 추출, 인증 토큰 생성 - private final JWTUtil jwtUtil; - - private final RefreshRepository refreshRepository; - - public LoginFilter(AuthenticationManager authenticationManager, JWTUtil jwtUtil, RefreshRepository refreshRepository) { - this.authenticationManager = authenticationManager; - this.jwtUtil = jwtUtil; - this.refreshRepository = refreshRepository; - } - - @Override - public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException{ - LoginDTO loginDTO; - - try { - ObjectMapper objectMapper = new ObjectMapper(); - ServletInputStream inputStream = request.getInputStream(); - String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8); - loginDTO = objectMapper.readValue(messageBody, LoginDTO.class); - } catch (IOException e) { - throw new AuthenticationServiceException("로그인 요청의 형식을 읽을 수 없습니다.", e); - } - - String email = loginDTO.getEmail(); - String password = loginDTO.getPassword(); - System.out.println(email); - - // 토큰은 authenticationManager이 username, password를 검증하기 위해서 발급하는 것 - UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null); - //검증이 잘 되면 Authentication 반환, 안되면 exception 반환 - return authenticationManager.authenticate(authToken); - } - @Override - protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) throws IOException { - System.out.println("인증 성공, 토큰 발급할 예정"); - CustomUserDetails customUserDetails = (CustomUserDetails) authentication.getPrincipal(); - - String username = customUserDetails.getUsername(); - - Collection authorities = authentication.getAuthorities(); //Collection 로 하면 GrantedAuthority 타입만 받을 수 있음 (하위 클래스 SimpleGrantedAuthority 허용 X) - Iterator iterator = authorities.iterator(); // SimpleGrantedAuthority에는 ROLE_USER, ROLE_ADMIN이 있어서 ? extends GrantedAuthoroty로 해야됨 - GrantedAuthority auth = iterator.next(); //권한 부여 - String role = auth.getAuthority(); - System.out.println("role 이름: " + role); - - String email = customUserDetails.getEmail(); - String access = jwtUtil.createJwt("access", username, role, email, 86400000L); - String refresh = jwtUtil.createJwt("refresh", username, role, email,864000000L); - - addRefreshEntity(username, refresh, 864000000L); - -// // TokenDto 생성 및 JSON 응답 -// TokenDto tokenDto = TokenDto.builder() -// .grantType("Bearer") -// .accessToken(access) -// .refreshToken(refresh) -// .build(); -// -// // JSON 응답 설정 -// response.setContentType("application/json"); -// response.setCharacterEncoding("UTF-8"); -// -// // TokenDto를 JSON으로 변환하여 응답 본문에 작성 -// ObjectMapper objectMapper = new ObjectMapper(); -// objectMapper.writeValue(response.getWriter(), tokenDto); -// - //응답 설정 - response.setHeader("access", access); - response.addCookie(createCookie("refresh", refresh)); - response.setStatus(HttpStatus.OK.value()); - - System.out.println("첫 토큰 나옴"); - } - - private void addRefreshEntity(String username, String refresh, Long expiredMs) { - - Date date = new Date(System.currentTimeMillis() + expiredMs); - - RefreshEntity refreshEntity = new RefreshEntity(); - refreshEntity.setUsername(username); - refreshEntity.setRefresh(refresh); - refreshEntity.setExpiration(date.toString()); - - refreshRepository.save(refreshEntity); - } - - private Cookie createCookie(String key, String value) { - - Cookie cookie = new Cookie(key, value); - cookie.setMaxAge(24*60*60); - cookie.setSecure(true); //쿠키 암호화 전송 https://howisitgo1ng.tistory.com/entry/HTTP-Only%EC%99%80-Secure-Cookie - cookie.setPath("/"); - cookie.setHttpOnly(true); - - return cookie; - } - @Override - protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) { - response.setStatus(401); - } -} diff --git a/src/main/java/com/example/smartair/repository/userRepository/RefreshRepository.java b/src/main/java/com/example/smartair/repository/userRepository/RefreshRepository.java deleted file mode 100644 index 74bd1e55..00000000 --- a/src/main/java/com/example/smartair/repository/userRepository/RefreshRepository.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.smartair.repository.userRepository; - -import com.example.smartair.entity.login.RefreshEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.transaction.annotation.Transactional; - -public interface RefreshRepository extends JpaRepository { - Boolean existsByRefresh(String refresh); - - @Transactional - void deleteByRefresh(String refresh); -} diff --git a/src/main/java/com/example/smartair/repository/userRepository/UserRepository.java b/src/main/java/com/example/smartair/repository/userRepository/UserRepository.java deleted file mode 100644 index a57a15c0..00000000 --- a/src/main/java/com/example/smartair/repository/userRepository/UserRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.smartair.repository.userRepository; - -import com.example.smartair.entity.user.User; -import jakarta.persistence.LockModeType; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Lock; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.util.Optional; - -public interface UserRepository extends JpaRepository { - Boolean existsByEmail(String email); - - Optional findByEmail(String email); -} diff --git a/src/main/java/com/example/smartair/service/userService/CustomUserDetailsService.java b/src/main/java/com/example/smartair/service/userService/CustomUserDetailsService.java deleted file mode 100644 index 1652bfe3..00000000 --- a/src/main/java/com/example/smartair/service/userService/CustomUserDetailsService.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.example.smartair.service.userService; - -import com.example.smartair.entity.login.CustomUserDetails; -import com.example.smartair.entity.user.User; - -import com.example.smartair.repository.userRepository.UserRepository; - -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.springframework.stereotype.Service; - -import java.util.Optional; - -@Service -public class CustomUserDetailsService implements UserDetailsService { - private final UserRepository userRepository; - - public CustomUserDetailsService(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @Override // 사용자 정보를 불러와서 UserDetails로 반환, loadUserByusername은 시큐리티 식별자로 email로 변경 안한 것 - public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { - System.out.println("사용자 정보 확인" + email); - //DB에서 조회 - Optional userData = userRepository.findByEmail(email); - - if (userData.isEmpty()) { - throw new UsernameNotFoundException("사용자를 찾을 수 없습니다"); - } - User user = userData.get(); - //UserDetails에 담아서 return하면 AutneticationManager가 검증 함 - return new CustomUserDetails(user); - } -} diff --git a/src/main/java/com/example/smartair/service/userService/JoinService.java b/src/main/java/com/example/smartair/service/userService/JoinService.java deleted file mode 100644 index c9ca26b1..00000000 --- a/src/main/java/com/example/smartair/service/userService/JoinService.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.example.smartair.service.userService; - -import com.example.smartair.dto.userDto.JoinDTO; -import com.example.smartair.entity.user.Role; -import com.example.smartair.entity.user.User; - -import com.example.smartair.exception.CustomException; -import com.example.smartair.exception.ErrorCode; -import com.example.smartair.repository.userRepository.UserRepository; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.stereotype.Service; - -@Slf4j -@Service -public class JoinService { - private final UserRepository userRepository; - private final BCryptPasswordEncoder bCryptPasswordEncoder; - - public JoinService(UserRepository userRepository, BCryptPasswordEncoder bCryptPasswordEncoder) { - - this.userRepository = userRepository; - this.bCryptPasswordEncoder = bCryptPasswordEncoder; - } - - public Boolean joinProcess(JoinDTO joinDTO) { - log.info("joinProcess 시작"); - String username = joinDTO.getUsername(); - String password = joinDTO.getPassword(); - String email = joinDTO.getEmail(); - String role = joinDTO.getRole(); - if(username == null || password == null || email == null || role == null) { - log.info("joinProcess 실패"); - throw new CustomException(ErrorCode.USER_JOIN_INFO_BAD_REQUEST, "회원가입에 필요한 정보가 부족합니다."); - - } - Boolean isExist = userRepository.existsByEmail(email); - log.info("user 정보의 db 존재 확인"); - if (isExist) { - throw new CustomException(ErrorCode.USER_ALREADY_EXISTS, "이미 존재하는 이메일입니다."); - } - - User data = new User(); - - data.setUsername(username); - data.setPassword(bCryptPasswordEncoder.encode(password)); - data.setEmail(email); - data.setRole(Role.valueOf(role)); - data.setLoginType("local"); - - userRepository.save(data); - return true; - } -} diff --git a/src/main/java/com/example/smartair/service/userService/KakaoService.java b/src/main/java/com/example/smartair/service/userService/KakaoService.java deleted file mode 100644 index 22a8cc89..00000000 --- a/src/main/java/com/example/smartair/service/userService/KakaoService.java +++ /dev/null @@ -1,120 +0,0 @@ -package com.example.smartair.service.userService; - -import com.example.smartair.dto.userDto.KakaoTokenResponseDTO; -import com.example.smartair.dto.userDto.KakaoUserInfoResponseDTO; -import com.example.smartair.entity.user.Role; -import com.example.smartair.entity.user.User; - -import com.example.smartair.repository.userRepository.UserRepository; - -import io.netty.handler.codec.http.HttpHeaderValues; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Service; -import org.springframework.web.reactive.function.client.WebClient; - -import java.util.Optional; - -@Slf4j -@Service -public class KakaoService { - private String clientId; - private String clientSecret; - private final String KAUTH_TOKEN_URL_HOST; - private final String KAUTH_USER_URL_HOST; - - private final UserRepository userRepository; - - @Autowired - public KakaoService(@Value("${spring.security.oauth2.client.registration.kakao.client-id}") String clientId, - @Value("${spring.security.oauth2.client.registration.kakao.client-secret}") String clientSecret, - UserRepository userRepository) { - this.clientId = clientId; - this.clientSecret = clientSecret; - this.KAUTH_TOKEN_URL_HOST = "https://kauth.kakao.com"; - this.KAUTH_USER_URL_HOST = "https://kapi.kakao.com"; - this.userRepository = userRepository; - } - - public String getAccessTokenFromKakao(String code) { - log.info("getAccessTokenFromKakao 실행"); - KakaoTokenResponseDTO kakaoTokenResponseDto = WebClient.create(KAUTH_TOKEN_URL_HOST).post() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .path("/oauth/token") - .queryParam("grant_type", "authorization_code") - .queryParam("client_id", clientId) - .queryParam("code", code) - .queryParam("client_secret", clientSecret) - .build(true)) - .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) - .retrieve() - //TODO : Custom Exception - .bodyToMono(KakaoTokenResponseDTO.class) - .block(); - log.info("카카오토큰DTO 생성"); - - log.info(" [Kakao Service] Access Token ------> {}", kakaoTokenResponseDto.getAccessToken()); - log.info(" [Kakao Service] Refresh Token ------> {}", kakaoTokenResponseDto.getRefreshToken()); - //제공 조건: OpenID Connect가 활성화 된 앱의 토큰 발급 요청인 경우 또는 scope에 openid를 포함한 추가 항목 동의 받기 요청을 거친 토큰 발급 요청인 경우 - log.info(" [Kakao Service] Id Token ------> {}", kakaoTokenResponseDto.getIdToken()); - log.info(" [Kakao Service] Scope ------> {}", kakaoTokenResponseDto.getScope()); - - return kakaoTokenResponseDto.getAccessToken(); - } - - public void debugUserInfoResponse(String accessToken) { - String response = WebClient.create(KAUTH_USER_URL_HOST) - .get() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .path("/v2/user/me") - .build(true)) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) - .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) - .retrieve() - .bodyToMono(String.class) - .block(); - - log.info("[Kakao Service] Raw user info response: {}", response); - } - - public KakaoUserInfoResponseDTO getUserInfo(String accessToken){ - KakaoUserInfoResponseDTO userInfo = WebClient.create(KAUTH_USER_URL_HOST) - .get() - .uri(uriBuilder -> uriBuilder - .scheme("https") - .path("/v2/user/me") - .build(true)) - .header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken) // access token 인가 - .header(HttpHeaders.CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString()) - .retrieve() - //TODO : Custom Exception - .bodyToMono(KakaoUserInfoResponseDTO.class) - .block(); - - - log.info("[ Kakao Service ] NickName ---> {} ", userInfo.getNickname()); - log.info("[ Kakao Service ] email ---> {} ", userInfo.getEmail()); - return userInfo; - } - - public User findUserOrCreateUser(KakaoUserInfoResponseDTO userInfo) { - Optional finduser = userRepository.findByEmail(userInfo.getEmail()); - User user; - if(finduser.isEmpty()){ - user = new User(); - user.setRole(Role.valueOf("USER")); - user.setUsername(userInfo.getNickname()); - user.setEmail(userInfo.getEmail()); - user.setLoginType("kakao"); - userRepository.save(user); - }else{ - user = finduser.get(); - } - - return user; - } -} diff --git a/src/main/java/com/example/smartair/service/userService/LoginService.java b/src/main/java/com/example/smartair/service/userService/LoginService.java deleted file mode 100644 index f74ce0ba..00000000 --- a/src/main/java/com/example/smartair/service/userService/LoginService.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.example.smartair.service.userService; - -import com.example.smartair.dto.userDto.LoginDTO; -import com.example.smartair.dto.userDto.TokenDto; -import com.example.smartair.entity.login.RefreshEntity; -import com.example.smartair.entity.user.User; -import com.example.smartair.exception.CustomException; -import com.example.smartair.exception.ErrorCode; -import com.example.smartair.jwt.JWTUtil; -import com.example.smartair.repository.userRepository.RefreshRepository; -import com.example.smartair.repository.userRepository.UserRepository; -import com.nimbusds.oauth2.sdk.token.RefreshToken; -import lombok.AllArgsConstructor; -import org.springframework.security.core.token.TokenService; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Date; - -@Service -@AllArgsConstructor -public class LoginService { - private final UserRepository userRepository; - private final PasswordEncoder passwordEncoder; - private final JWTUtil jwtUtil; - private final RefreshRepository refreshRepository; - - // Access Token 만료 시간: 30분 - private static final long ACCESS_TOKEN_EXPIRE_TIME = 30 * 60 * 1000L; - // Refresh Token 만료 시간: 7일 - private static final long REFRESH_TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L; - - - @Transactional - public TokenDto login(LoginDTO loginRequestDto) { - User user = userRepository.findByEmail(loginRequestDto.getEmail()) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - if (!passwordEncoder.matches(loginRequestDto.getPassword(), user.getPassword())) { - throw new CustomException(ErrorCode.USER_PASSWORD_NOT_MATCH); - } - - String accessToken = jwtUtil.createJwt( - "access", - user.getUsername(), - user.getRole().toString(), - user.getEmail(), - ACCESS_TOKEN_EXPIRE_TIME - ); - - String refreshToken = jwtUtil.createJwt( - "refresh", - user.getUsername(), - user.getRole().toString(), - user.getEmail(), - REFRESH_TOKEN_EXPIRE_TIME - ); - - // RefreshEntity 저장 - RefreshEntity refreshEntity = new RefreshEntity(); - refreshEntity.setUsername(user.getUsername()); - refreshEntity.setRefresh(refreshToken); - refreshEntity.setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRE_TIME).toString()); - refreshRepository.save(refreshEntity); - - return TokenDto.builder() - .grantType("Bearer") - .accessToken(accessToken) - .refreshToken(refreshToken) - .build(); - - } -} diff --git a/src/main/resources/application-ci.yml b/src/main/resources/application-ci.yml index 3c1c7617..0e9fe438 100644 --- a/src/main/resources/application-ci.yml +++ b/src/main/resources/application-ci.yml @@ -17,31 +17,6 @@ spring: swagger-ui: enabled: true - jwt: - secret: ${JWT_SECRET} - - security: - oauth2: - client: - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id - registration: - kakao: - client-id: ${KAKAO_CLIENT_ID} - client-secret: ${KAKAO_CLIENT_SECRET} - redirect-uri: ${KAKAO_REDIRECT_URI} - authorization-grant-type: authorization_code - client-authentication-method: POST - client-name: Kakao - scope: - - name - - profile_nickname - - account_email - logging: level: root: warn diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 36454fe1..be01c10c 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -17,33 +17,6 @@ spring: swagger-ui: enabled: true # 필요에 따라 false로 변경 가능 - jwt: - secret: ${JWT_SECRET} # EC2에 설정된 환경 변수 - - security: - oauth2: - client: - # provider 설정은 보통 동일하게 유지 - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id - registration: - kakao: - client-id: ${KAKAO_CLIENT_ID} # EC2에 설정된 환경 변수 - client-secret: ${KAKAO_CLIENT_SECRET} # EC2에 설정된 환경 변수 - # 중요! EC2의 실제 접속 가능한 리디렉션 URI로 변경 - redirect-uri: ${KAKAO_REDIRECT_URI_EC2_PROD} - authorization-grant-type: authorization_code - client-authentication-method: POST - client-name: Kakao - scope: - - name - - profile_nickname - - account_email - logging: level: root: info # 필요에 따라 warn 등으로 변경 가능 \ No newline at end of file diff --git a/src/test/java/com/example/smartair/service/userService/JoinServiceTest.java b/src/test/java/com/example/smartair/service/userService/JoinServiceTest.java deleted file mode 100644 index 6b86dcd6..00000000 --- a/src/test/java/com/example/smartair/service/userService/JoinServiceTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.example.smartair.service.userService; - -import com.example.smartair.dto.userDto.JoinDTO; - -import com.example.smartair.repository.userRepository.UserRepository; - -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.UUID; // UUID 추가 - -@SpringBootTest -@Transactional // 각 테스트 메서드 트랜잭션, 테스트 종료시 롤백 -public class JoinServiceTest { - - @Autowired - private UserRepository userRepository; - - @Autowired - private JoinService joinService; - - @Test - void 회원가입_성공(){ - // 각 테스트 실행 시 고유한 username과 email을 생성 - String uniqueSuffix = UUID.randomUUID().toString().substring(0, 8); - String username = "user_" + uniqueSuffix; - String password = "password"; - String email = "email_" + uniqueSuffix + "@example.com"; - - - JoinDTO joinDTO = JoinDTO.builder().username(username).password(password) - .email(email).role("USER").build(); - - Boolean success = joinService.joinProcess(joinDTO); - - Assertions.assertTrue(success, "회원가입은 성공해야 합니다."); // Assertions.assertTrue 사용 및 메시지 추가 - - // 선택적: 실제로 DB에 저장되었는지, 원하는 값으로 저장되었는지 확인 (userRepository 사용) - // com.example.smartair.entity.user.UserEntity savedUser = userRepository.findByUsername(username).orElse(null); - // Assertions.assertNotNull(savedUser, "저장된 사용자를 찾을 수 없습니다."); - // Assertions.assertEquals(email, savedUser.getEmail(), "이메일이 일치해야 합니다."); - // Assertions.assertEquals(Role.USER, savedUser.getRole(), "역할이 USER여야 합니다."); - } - -} diff --git a/src/test/java/com/example/smartair/service/userService/LoginTest.java b/src/test/java/com/example/smartair/service/userService/LoginTest.java deleted file mode 100644 index abd0beb9..00000000 --- a/src/test/java/com/example/smartair/service/userService/LoginTest.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.example.smartair.service.userService; - -import com.example.smartair.entity.user.User; - -import com.example.smartair.repository.userRepository.UserRepository; -import org.hamcrest.Matchers; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.transaction.annotation.Transactional; - -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@SpringBootTest -@AutoConfigureMockMvc -@Transactional -public class LoginTest { - @Autowired - private MockMvc mockMvc; - - @Autowired - private UserRepository userRepository; - - @Autowired - private PasswordEncoder passwordEncoder; - - @BeforeEach - void setUp(){ - User user = new User(); - - user.setUsername("test"); - user.setPassword(passwordEncoder.encode("password")); - user.setEmail("email"); - userRepository.save(user); - } - - @Test - void 로그인_성공_후_JWT_토큰_발급() throws Exception{ - mockMvc.perform(post("/login") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - - "email":"email", - "password" : "password" - } - """)) - .andExpect(status().isOk()) - .andExpect(header().exists("access")) - .andExpect(header().stringValues("Set-Cookie", Matchers.hasItem(Matchers.containsString("refresh=")))); - } - - @Test - void 로그인_실패_잘못된비밀번호() throws Exception{ - mockMvc.perform(post("/login") - .contentType(MediaType.APPLICATION_JSON) - .content(""" - { - "email": "testuser", - "password": "wrongpassword" - } - """)) - .andExpect(status().isUnauthorized()); - } -}