diff --git a/build.gradle b/build.gradle index d4c224f..bdae515 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + implementation 'mysql:mysql-connector-java:8.0.32' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' diff --git a/src/main/java/codeview/main/auth/controller/OAuth2Controller.java b/src/main/java/codeview/main/auth/controller/OAuth2Controller.java index e95acdb..9175c81 100644 --- a/src/main/java/codeview/main/auth/controller/OAuth2Controller.java +++ b/src/main/java/codeview/main/auth/controller/OAuth2Controller.java @@ -9,21 +9,22 @@ @Controller public class OAuth2Controller { - @GetMapping("/oauth2/callback/google") + @GetMapping("/login/oauth2/code/google") public String googleCallback(@AuthenticationPrincipal OAuth2User principal, Model model) { model.addAttribute("name", principal.getAttribute("name")); return "home"; } - @GetMapping("/oauth2/callback/github") + @GetMapping("/login/oauth2/code/github") public String githubCallback(@AuthenticationPrincipal OAuth2User principal, Model model) { model.addAttribute("name", principal.getAttribute("name")); return "home"; } - @GetMapping("/oauth2/callback/kakao") - public String kakaoCallback(@AuthenticationPrincipal OAuth2User principal, Model model) { - model.addAttribute("name", principal.getAttribute("nickname")); - return "home"; - } +// @GetMapping("/login/oauth2/code/kakao") +// public String kakaoCallback(@AuthenticationPrincipal OAuth2User principal, Model model) { +// model.addAttribute("name", principal.getAttribute("nickname")); +// return "home"; +// } } + diff --git a/src/main/java/codeview/main/auth/dto/OAuth2UserInfo.java b/src/main/java/codeview/main/auth/dto/OAuth2UserInfo.java index 7b60cc0..fb1a365 100644 --- a/src/main/java/codeview/main/auth/dto/OAuth2UserInfo.java +++ b/src/main/java/codeview/main/auth/dto/OAuth2UserInfo.java @@ -20,8 +20,10 @@ public static OAuth2UserInfo of(String registrationId, Map attri return ofGoogle(attributes); case "kakao": return ofKakao(attributes); + case "github": + return ofGithub(attributes); default: - throw new IllegalArgumentException("Illegal registration ID: " + registrationId); + throw new IllegalArgumentException("Unsupported registration ID: " + registrationId); } } @@ -44,6 +46,14 @@ private static OAuth2UserInfo ofKakao(Map attributes) { .build(); } + private static OAuth2UserInfo ofGithub(Map attributes) { + return OAuth2UserInfo.builder() + .name((String) attributes.get("login")) + .email((String) attributes.get("email")) + .profile((String) attributes.get("avatar_url")) + .build(); + } + public Member toEntity() { return Member.builder() .name(name) diff --git a/src/main/java/codeview/main/auth/dto/model/PrincipalDetails.java b/src/main/java/codeview/main/auth/dto/model/PrincipalDetails.java index d2293ba..fddef57 100644 --- a/src/main/java/codeview/main/auth/dto/model/PrincipalDetails.java +++ b/src/main/java/codeview/main/auth/dto/model/PrincipalDetails.java @@ -23,7 +23,8 @@ public PrincipalDetails(Member member, Map attributes, String at @Override public String getName() { - return attributes.get(attributeKey).toString(); + // If attributeKey is null or not present, fallback to member's email or name + return attributes.getOrDefault(attributeKey, member.getName() != null ? member.getName() : member.getEmail()).toString(); } @Override diff --git a/src/main/java/codeview/main/auth/handler/OAuth2SuccessHandler.java b/src/main/java/codeview/main/auth/handler/OAuth2SuccessHandler.java index 8bca82d..0daa477 100644 --- a/src/main/java/codeview/main/auth/handler/OAuth2SuccessHandler.java +++ b/src/main/java/codeview/main/auth/handler/OAuth2SuccessHandler.java @@ -3,7 +3,6 @@ import codeview.main.auth.jwt.TokenProvider; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; @@ -20,25 +19,22 @@ public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final TokenProvider tokenProvider; + private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - + // JWT 토큰 생성 String accessToken = tokenProvider.generateAccessToken(authentication); String refreshToken = tokenProvider.generateRefreshToken(authentication, accessToken); - - Map responseBody = new HashMap<>(); - responseBody.put("code", 200); - Map result = new HashMap<>(); - result.put("accessToken", accessToken); - result.put("refreshToken", refreshToken); - responseBody.put("result", result); + // JSON 형태로 응답 + Map tokens = new HashMap<>(); + tokens.put("accessToken", accessToken); + tokens.put("refreshToken", refreshToken); response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); - response.getWriter().write(new ObjectMapper().writeValueAsString(responseBody)); - response.setStatus(HttpStatus.OK.value()); + response.getWriter().write(objectMapper.writeValueAsString(tokens)); } } diff --git a/src/main/java/codeview/main/auth/jwt/TokenAuthenticationFilter.java b/src/main/java/codeview/main/auth/jwt/TokenAuthenticationFilter.java index eb5f188..b03c2d3 100644 --- a/src/main/java/codeview/main/auth/jwt/TokenAuthenticationFilter.java +++ b/src/main/java/codeview/main/auth/jwt/TokenAuthenticationFilter.java @@ -4,51 +4,33 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; -@RequiredArgsConstructor @Component public class TokenAuthenticationFilter extends OncePerRequestFilter { private final TokenProvider tokenProvider; - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, - FilterChain filterChain) throws ServletException, IOException { - String accessToken = resolveToken(request); - - if (tokenProvider.validateToken(accessToken)) { - setAuthentication(accessToken); - } else { - String reissueAccessToken = tokenProvider.reissueAccessToken(accessToken); + public TokenAuthenticationFilter(TokenProvider tokenProvider) { + this.tokenProvider = tokenProvider; + } - if (StringUtils.hasText(reissueAccessToken)) { - setAuthentication(reissueAccessToken); - response.setHeader("Authorization", "Bearer " + reissueAccessToken); + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String bearerToken = request.getHeader("Authorization"); + if (bearerToken != null && bearerToken.startsWith("Bearer ")) { + String token = bearerToken.substring(7); + Authentication authentication = tokenProvider.getAuthentication(token); + if (authentication != null) { + SecurityContextHolder.getContext().setAuthentication(authentication); } } - filterChain.doFilter(request, response); } - - private void setAuthentication(String accessToken) { - Authentication authentication = tokenProvider.getAuthentication(accessToken); - SecurityContextHolder.getContext().setAuthentication(authentication); - } - - private String resolveToken(HttpServletRequest request) { - String token = request.getHeader("Authorization"); - if (ObjectUtils.isEmpty(token) || !token.startsWith("Bearer ")) { - return null; - } - return token.substring(7); - } } diff --git a/src/main/java/codeview/main/auth/service/CustomOAuth2UserService.java b/src/main/java/codeview/main/auth/service/CustomOAuth2UserService.java index ffde51a..a963a60 100644 --- a/src/main/java/codeview/main/auth/service/CustomOAuth2UserService.java +++ b/src/main/java/codeview/main/auth/service/CustomOAuth2UserService.java @@ -13,6 +13,7 @@ import org.springframework.stereotype.Service; import java.util.Map; +import java.util.Optional; @RequiredArgsConstructor @Service @@ -23,19 +24,46 @@ public class CustomOAuth2UserService extends DefaultOAuth2UserService { @Transactional @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { - Map oAuth2UserAttributes = super.loadUser(userRequest).getAttributes(); + OAuth2User oAuth2User = super.loadUser(userRequest); + Map oAuth2UserAttributes = oAuth2User.getAttributes(); + String registrationId = userRequest.getClientRegistration().getRegistrationId(); String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails() .getUserInfoEndpoint().getUserNameAttributeName(); OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(registrationId, oAuth2UserAttributes); - Member member = getOrSave(oAuth2UserInfo); + + // 이메일이 없는 경우 처리 + Optional memberOptional = Optional.empty(); + if (oAuth2UserInfo.getEmail() != null) { + memberOptional = memberRepository.findByEmail(oAuth2UserInfo.getEmail()); + } + + Member member; + if (memberOptional.isPresent()) { + member = memberOptional.get(); + } else { + // 이메일이 없거나 기존 회원이 아닌 경우 새로 저장 + member = getOrSave(oAuth2UserInfo); + } return new PrincipalDetails(member, oAuth2UserAttributes, userNameAttributeName); } private Member getOrSave(OAuth2UserInfo oAuth2UserInfo) { - return memberRepository.findByEmail(oAuth2UserInfo.getEmail()) - .orElseGet(() -> memberRepository.save(oAuth2UserInfo.toEntity())); + Member member = new Member(); + member.setName(oAuth2UserInfo.getName()); + member.setProfile(oAuth2UserInfo.getProfile()); + + if (oAuth2UserInfo.getEmail() != null) { + member.setEmail(oAuth2UserInfo.getEmail()); + } else { + // 이메일이 없는 경우 다른 방법으로 식별자를 생성 (예: OAuth 제공자의 고유 ID 사용) + member.setEmail(oAuth2UserInfo.getName() + "@example.com"); + } + + member.setRole(Member.Role.ROLE_USER); + + return memberRepository.save(member); } } diff --git a/src/main/java/codeview/main/config/CustomRequestFilter.java b/src/main/java/codeview/main/config/CustomRequestFilter.java new file mode 100644 index 0000000..8322d1a --- /dev/null +++ b/src/main/java/codeview/main/config/CustomRequestFilter.java @@ -0,0 +1,24 @@ +package codeview.main.config; + +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class CustomRequestFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String uri = request.getRequestURI(); + if (!uri.startsWith("/login") && !uri.startsWith("/oauth2")) { + request.getSession().setAttribute("redirectUri", request.getRequestURL().toString()); + } + filterChain.doFilter(request, response); + } +} diff --git a/src/main/java/codeview/main/config/SecurityConfig.java b/src/main/java/codeview/main/config/SecurityConfig.java index 40ec55f..6038e3d 100644 --- a/src/main/java/codeview/main/config/SecurityConfig.java +++ b/src/main/java/codeview/main/config/SecurityConfig.java @@ -3,7 +3,6 @@ import codeview.main.auth.handler.OAuth2SuccessHandler; import codeview.main.auth.jwt.TokenAuthenticationFilter; import codeview.main.auth.jwt.TokenExceptionFilter; -import codeview.main.auth.jwt.TokenProvider; import codeview.main.auth.service.CustomOAuth2UserService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -19,8 +18,8 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import org.springframework.web.filter.CorsFilter; import java.util.Arrays; @@ -32,7 +31,7 @@ public class SecurityConfig { private final CustomOAuth2UserService customOAuth2UserService; private final TokenAuthenticationFilter tokenAuthenticationFilter; - private final TokenProvider tokenProvider; + private final OAuth2SuccessHandler successHandler; @Bean public WebSecurityCustomizer webSecurityCustomizer() { @@ -49,7 +48,6 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .logout(AbstractHttpConfigurer::disable) .headers(c -> c.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable).disable()) .sessionManagement(c -> c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authorizeHttpRequests(request -> request.requestMatchers( new AntPathRequestMatcher("/"), new AntPathRequestMatcher("/home"), @@ -58,15 +56,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti new AntPathRequestMatcher("/api/oauth2/**") ).permitAll() .anyRequest().authenticated()) - .oauth2Login(oauth -> oauth .loginPage("/login") - .successHandler(new OAuth2SuccessHandler(tokenProvider)) + .successHandler(successHandler) .userInfoEndpoint(userInfo -> userInfo .userService(customOAuth2UserService))) .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new TokenExceptionFilter(), TokenAuthenticationFilter.class) - .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint(new CustomAuthenticationEntryPoint()) .accessDeniedHandler(new CustomAccessDeniedHandler())); @@ -74,19 +70,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti } @Bean - public CorsFilter corsFilter() { - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - CorsConfiguration config = new CorsConfiguration(); - config.setAllowCredentials(true); - config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); - config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE")); - config.setAllowedHeaders(Arrays.asList("*")); - source.registerCorsConfiguration("/**", config); - return new CorsFilter(source); - } - - @Bean - public UrlBasedCorsConfigurationSource corsConfigurationSource() { + public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.setAllowedOrigins(Arrays.asList("http://localhost:3000")); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 470bfe7..eda553b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -2,7 +2,7 @@ spring: profiles: active: local group: - local: local, common, secret-dev + local: local, common, secret blue: blue, common, secret-deploy green: green, common, secret-deploy