diff --git a/src/main/java/com/aibe/team2/domain/auth/controller/AuthController.java b/src/main/java/com/aibe/team2/domain/auth/controller/AuthController.java index 7364f71..c85b4e3 100644 --- a/src/main/java/com/aibe/team2/domain/auth/controller/AuthController.java +++ b/src/main/java/com/aibe/team2/domain/auth/controller/AuthController.java @@ -1,5 +1,6 @@ package com.aibe.team2.domain.auth.controller; +import com.aibe.team2.domain.auth.dto.LoginRequest; import com.aibe.team2.domain.auth.dto.MemberDTO; import com.aibe.team2.domain.auth.repository.RefreshTokenRepository; import com.aibe.team2.domain.auth.service.AuthService; @@ -22,7 +23,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Collections; import java.util.Map; @RestController @@ -38,19 +38,49 @@ public class AuthController { private final AuthService authService; @PostMapping("/login") - public ResponseEntity login(@RequestBody Map user) { + public ResponseEntity login(@RequestBody LoginRequest request) { + try { - // 1. 아이디/비번으로 인증 시도 + + String email = request.getEmail(); + String password = request.getPassword(); + + // 1. 인증 시도 authenticationManager.authenticate( - new UsernamePasswordAuthenticationToken(user.get("username"), user.get("password")) + new UsernamePasswordAuthenticationToken(email, password) + ); + + // 2. 사용자 조회 + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + // 3. 토큰 생성 + String accessToken = jwtTokenProvider.createAccessToken(email, member.getRole().name()); + String refreshToken = jwtTokenProvider.createRefreshToken(email, member.getRole().name()); + + // 4. RefreshToken Redis 저장 + refreshTokenRepository.save( + new RefreshToken(email, refreshToken) ); - // 2. 인증 성공 시 토큰 생성 - String token = jwtTokenProvider.createAccessToken(user.get("username")); - return ResponseEntity.ok(Collections.singletonMap("token", token)); + // 5. 응답 반환 + + return ResponseEntity.ok( + new com.aibe.team2.domain.auth.dto.response.LoginResponse( + accessToken, + refreshToken, + member.getRole().name(), + member.getEmail(), + member.getNickname() + ) + ); } catch (AuthenticationException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("아이디 또는 비밀번호가 틀렸습니다."); + + return ResponseEntity + .status(HttpStatus.UNAUTHORIZED) + .body("이메일 또는 비밀번호가 틀렸습니다."); + } } @@ -80,6 +110,7 @@ public ResponseEntity signup(@RequestBody MemberDTO request) { return ResponseEntity.ok("회원가입이 완료되었습니다."); } + @PostMapping("/reissue") public ResponseEntity reissue(@RequestBody Map request) { String refreshToken = request.get("refreshToken"); @@ -90,8 +121,9 @@ public ResponseEntity reissue(@RequestBody Map request) { } // 2. Redis에서 해당 토큰이 존재하는지 확인 - String username = jwtTokenProvider.getUsername(refreshToken); - RefreshToken savedToken = refreshTokenRepository.findById(username) + String email = jwtTokenProvider.getEmail(refreshToken); + + RefreshToken savedToken = refreshTokenRepository.findById(email) .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); if (!savedToken.getRefreshToken().equals(refreshToken)) { @@ -99,8 +131,11 @@ public ResponseEntity reissue(@RequestBody Map request) { } // 3. 새로운 Access Token 발급 - String newAccessToken = jwtTokenProvider.createAccessToken(username); - return ResponseEntity.ok(Map.of("accessToken", newAccessToken)); + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND)); + + String newAccessToken = jwtTokenProvider.createAccessToken(email, member.getRole().name()); + return ResponseEntity.ok(java.util.Map.of("accessToken", newAccessToken)); } @PostMapping("/logout") diff --git a/src/main/java/com/aibe/team2/domain/auth/dto/CustomUserDetails.java b/src/main/java/com/aibe/team2/domain/auth/dto/CustomUserDetails.java index 63023c4..0659437 100644 --- a/src/main/java/com/aibe/team2/domain/auth/dto/CustomUserDetails.java +++ b/src/main/java/com/aibe/team2/domain/auth/dto/CustomUserDetails.java @@ -20,8 +20,6 @@ public CustomUserDetails(Member member) { @Override public Collection getAuthorities() { - // "ROLE_USER"와 같은 형식으로 권한을 반환합니다. - // member.getRole().name()이 "USER"라면 "ROLE_USER"로 변환이 필요할 수 있습니다. return Collections.singletonList( new SimpleGrantedAuthority("ROLE_" + member.getRole().name()) ); diff --git a/src/main/java/com/aibe/team2/domain/auth/dto/LoginRequest.java b/src/main/java/com/aibe/team2/domain/auth/dto/LoginRequest.java new file mode 100644 index 0000000..59610ad --- /dev/null +++ b/src/main/java/com/aibe/team2/domain/auth/dto/LoginRequest.java @@ -0,0 +1,11 @@ +package com.aibe.team2.domain.auth.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LoginRequest { + private String email; + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/aibe/team2/domain/auth/dto/LoginResponse.java b/src/main/java/com/aibe/team2/domain/auth/dto/LoginResponse.java new file mode 100644 index 0000000..c88a471 --- /dev/null +++ b/src/main/java/com/aibe/team2/domain/auth/dto/LoginResponse.java @@ -0,0 +1,14 @@ +package com.aibe.team2.domain.auth.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class LoginResponse { + private String accessToken; + private String refreshToken; + private String role; + private String email; + private String nickname; +} \ No newline at end of file diff --git a/src/main/java/com/aibe/team2/domain/auth/filter/JwtAuthenticationFilter.java b/src/main/java/com/aibe/team2/domain/auth/filter/JwtAuthenticationFilter.java index f54727e..c61d104 100644 --- a/src/main/java/com/aibe/team2/domain/auth/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/aibe/team2/domain/auth/filter/JwtAuthenticationFilter.java @@ -32,10 +32,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse String token = resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { - String username = jwtTokenProvider.getUsername(token); + String email = jwtTokenProvider.getEmail(token); // 1. DB에서 사용자 정보를 로드 (권한 정보 포함) - UserDetails userDetails = customMemberDetailService.loadUserByUsername(username); + UserDetails userDetails = customMemberDetailService.loadUserByUsername(email); // 2. userDetails.getAuthorities()를 통해 실제 권한을 부여 Authentication auth = new UsernamePasswordAuthenticationToken( diff --git a/src/main/java/com/aibe/team2/domain/auth/service/CustomMemberDetailService.java b/src/main/java/com/aibe/team2/domain/auth/service/CustomMemberDetailService.java index 59eb0fa..592c827 100644 --- a/src/main/java/com/aibe/team2/domain/auth/service/CustomMemberDetailService.java +++ b/src/main/java/com/aibe/team2/domain/auth/service/CustomMemberDetailService.java @@ -16,9 +16,9 @@ public class CustomMemberDetailService implements UserDetailsService { private final MemberRepository memberRepository; @Override - public UserDetails loadUserByUsername(String nickname) throws UsernameNotFoundException { - Member member = memberRepository.findByNickname(nickname) - .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + nickname)); + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + email)); return new CustomUserDetails(member); } diff --git a/src/main/java/com/aibe/team2/domain/auth/util/JwtTokenProvider.java b/src/main/java/com/aibe/team2/domain/auth/util/JwtTokenProvider.java index 28a371e..82af8b7 100644 --- a/src/main/java/com/aibe/team2/domain/auth/util/JwtTokenProvider.java +++ b/src/main/java/com/aibe/team2/domain/auth/util/JwtTokenProvider.java @@ -1,6 +1,7 @@ package com.aibe.team2.domain.auth.util; import com.aibe.team2.domain.auth.repository.RefreshTokenRepository; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; @@ -9,7 +10,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import java.nio.charset.StandardCharsets; import java.security.Key; import java.util.Date; @@ -31,30 +31,42 @@ public void init() { } // Access Token 생성 - public String createAccessToken(String username) { - return createToken(username, accessTokenValidity); + public String createAccessToken(String email, String role) { + return createToken(email, role, accessTokenValidity); } // Refresh Token 생성 및 Redis 저장 - public String createRefreshToken(String username) { - String token = createToken(username, refreshTokenValidity); - refreshTokenRepository.save(new RefreshToken(username, token)); + public String createRefreshToken(String email, String role) { + String token = createToken(email, role, refreshTokenValidity); + refreshTokenRepository.save(new RefreshToken(email, token)); return token; } // 토큰 생성 - public String createToken(String username, long exp) { + public String createToken(String email, String role, long exp) { return Jwts.builder() - .setSubject(username) + .setSubject(email) + .claim("role", role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + exp)) .signWith(key, SignatureAlgorithm.HS256) .compact(); } - // 토큰에서 사용자 아이디 추출 - public String getUsername(String token) { - return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getSubject(); + public String getEmail(String token) { + return getClaims(token).getSubject(); + } + + public String getRole(String token) { + return getClaims(token).get("role", String.class); + } + + public Claims getClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token) + .getBody(); } // 토큰 유효성 검사 diff --git a/src/main/java/com/aibe/team2/domain/auth/util/OAuth2SuccessHandler.java b/src/main/java/com/aibe/team2/domain/auth/util/OAuth2SuccessHandler.java index af8fc7a..acf9a94 100644 --- a/src/main/java/com/aibe/team2/domain/auth/util/OAuth2SuccessHandler.java +++ b/src/main/java/com/aibe/team2/domain/auth/util/OAuth2SuccessHandler.java @@ -1,10 +1,12 @@ package com.aibe.team2.domain.auth.util; +import com.aibe.team2.domain.mypage.entity.Member; +import com.aibe.team2.domain.mypage.entity.enums.Provider; +import com.aibe.team2.domain.mypage.entity.enums.Role; +import com.aibe.team2.domain.mypage.repository.member.MemberRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpHeaders; -import org.springframework.http.ResponseCookie; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; @@ -18,33 +20,41 @@ public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler { private final JwtTokenProvider jwtTokenProvider; + private final MemberRepository memberRepository; @Override - public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, + public void onAuthenticationSuccess(HttpServletRequest request, + HttpServletResponse response, Authentication authentication) throws IOException { + OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal(); - // Google의 경우 "email" 필드에 이메일이 들어있습니다. String email = oAuth2User.getAttribute("email"); + String nickname = oAuth2User.getAttribute("name"); + + Member member = memberRepository.findByEmail(email) + .orElseGet(() -> memberRepository.save( + new Member( + email, + null, + (nickname != null && !nickname.isBlank()) ? nickname : email.split("@")[0], + Role.MEMBER, + Provider.GOOGLE + ) + )); + + String role = member.getRole().name(); + + String accessToken = jwtTokenProvider.createAccessToken(email, role); + String refreshToken = jwtTokenProvider.createRefreshToken(email, role); + + String targetUrl = UriComponentsBuilder + .fromUriString("http://localhost:5173/AIBE4_FinalProject_Team2_FE/oauth/callback") + .queryParam("accessToken", accessToken) + .queryParam("refreshToken", refreshToken) + .build() + .toUriString(); - // 우리 서버의 JWT 발급 - String token = jwtTokenProvider.createAccessToken(email); - - - // 1. 쿠키 생성 - ResponseCookie cookie = ResponseCookie.from("accessToken", token) - .path("/") - .httpOnly(true) // JavaScript에서 접근 불가 (XSS 방어) - .secure(true) // HTTPS 환경에서만 전송 - .sameSite("Lax") // CSRF 방어 - .maxAge(3600) // 유효 기간 설정 - .build(); - - // 2. 응답 헤더에 쿠키 추가 - response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); - - // 3. 리다이렉트 (토큰 제외) - String targetUrl = "http://localhost:5173/oauth/callback"; getRedirectStrategy().sendRedirect(request, response, targetUrl); } } \ No newline at end of file diff --git a/src/main/java/com/aibe/team2/global/config/CorsConfig.java b/src/main/java/com/aibe/team2/global/config/CorsConfig.java new file mode 100644 index 0000000..4e6c94b --- /dev/null +++ b/src/main/java/com/aibe/team2/global/config/CorsConfig.java @@ -0,0 +1,33 @@ +package com.aibe.team2.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +public class CorsConfig { + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(List.of( + "http://localhost:5173" + )); + configuration.setAllowedMethods(List.of( + "GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS" + )); + configuration.setAllowedHeaders(List.of("*")); + configuration.setAllowCredentials(true); + + configuration.setExposedHeaders(List.of("Authorization")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/aibe/team2/global/config/SecurityConfig.java b/src/main/java/com/aibe/team2/global/config/SecurityConfig.java index 764ff2f..8a5f302 100644 --- a/src/main/java/com/aibe/team2/global/config/SecurityConfig.java +++ b/src/main/java/com/aibe/team2/global/config/SecurityConfig.java @@ -19,8 +19,6 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; @@ -74,8 +72,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { "/api/v1/notifications/**", "/api/v1/resumes/**", "/api/v1/job-postings/**", - "/resume.html" - + "/resume.html", + "/css/**", + "/js/**", + "/images/**" ).permitAll() .requestMatchers("/api/files/**", "/api/v1/auth/**").permitAll() .anyRequest().authenticated() @@ -101,18 +101,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { return http.build(); } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(List.of(allowedOrigins)); // CORS 설정 - configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")); - configuration.setAllowedHeaders(List.of("*")); - configuration.setAllowCredentials(true); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - return source; - } }