Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -38,19 +38,49 @@ public class AuthController {
private final AuthService authService;

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody Map<String, String> 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("이메일 또는 비밀번호가 틀렸습니다.");

}
}

Expand Down Expand Up @@ -80,6 +110,7 @@ public ResponseEntity<?> signup(@RequestBody MemberDTO request) {
return ResponseEntity.ok("회원가입이 완료되었습니다.");
}


@PostMapping("/reissue")
public ResponseEntity<?> reissue(@RequestBody Map<String, String> request) {
String refreshToken = request.get("refreshToken");
Expand All @@ -90,17 +121,21 @@ public ResponseEntity<?> reissue(@RequestBody Map<String, String> 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)) {
return ResponseEntity.status(401).body("토큰 정보가 일치하지 않습니다.");
}

// 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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ public CustomUserDetails(Member member) {

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// "ROLE_USER"와 같은 형식으로 권한을 반환합니다.
// member.getRole().name()이 "USER"라면 "ROLE_USER"로 변환이 필요할 수 있습니다.
return Collections.singletonList(
new SimpleGrantedAuthority("ROLE_" + member.getRole().name())
);
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/aibe/team2/domain/auth/dto/LoginRequest.java
Original file line number Diff line number Diff line change
@@ -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;
}
14 changes: 14 additions & 0 deletions src/main/java/com/aibe/team2/domain/auth/dto/LoginResponse.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
34 changes: 23 additions & 11 deletions src/main/java/com/aibe/team2/domain/auth/util/JwtTokenProvider.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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();
}

// 토큰 유효성 검사
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/aibe/team2/global/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
22 changes: 4 additions & 18 deletions src/main/java/com/aibe/team2/global/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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()
Expand All @@ -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;
}
}