diff --git a/src/main/java/org/swmaestro/repl/gifthub/auth/controller/GoogleController.java b/src/main/java/org/swmaestro/repl/gifthub/auth/controller/GoogleController.java new file mode 100644 index 00000000..ec5333b7 --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/auth/controller/GoogleController.java @@ -0,0 +1,33 @@ +package org.swmaestro.repl.gifthub.auth.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; +import org.swmaestro.repl.gifthub.auth.dto.GoogleDto; +import org.swmaestro.repl.gifthub.auth.dto.TokenDto; +import org.swmaestro.repl.gifthub.auth.service.GoogleService; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +@Tag(name = "Auth", description = "구글 로그인을 이용한 사용자 인증 관련 API") +public class GoogleController { + private final GoogleService googleService; + + @GetMapping("/google/callback") + @Operation(summary = "구글 로그인 콜백 메서드", description = "구글 로그인 후 리다이렉트 되어 인가 코드를 출력하는 메서드입니다.") + public String callback(@RequestParam String code) { + return code; + } + + @PostMapping("/google/sign-in") + @Operation(summary = "구글 로그인 메서드", description = "구글로부터 사용자 정보를 얻어와 회원가입 및 로그인을 하기 위한 메서드입니다.") + public TokenDto signIn(@RequestHeader("Authorization") String code) { + code = code.substring(7); + TokenDto googleTokenDto = googleService.getToken(code); + GoogleDto googleDto = googleService.getUserInfo(googleTokenDto); + TokenDto tokenDto = googleService.signIn(googleDto); + return tokenDto; + } +} diff --git a/src/main/java/org/swmaestro/repl/gifthub/auth/dto/GoogleDto.java b/src/main/java/org/swmaestro/repl/gifthub/auth/dto/GoogleDto.java new file mode 100644 index 00000000..1e41cf0c --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/auth/dto/GoogleDto.java @@ -0,0 +1,20 @@ +package org.swmaestro.repl.gifthub.auth.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Builder +@Getter +@Setter +public class GoogleDto { + @NotNull + @Size(min = 4, max = 60) + private String username; + + @NotNull + @Size(min = 2, max = 12) + private String nickname; +} diff --git a/src/main/java/org/swmaestro/repl/gifthub/auth/service/GoogleService.java b/src/main/java/org/swmaestro/repl/gifthub/auth/service/GoogleService.java new file mode 100644 index 00000000..c669f349 --- /dev/null +++ b/src/main/java/org/swmaestro/repl/gifthub/auth/service/GoogleService.java @@ -0,0 +1,191 @@ +package org.swmaestro.repl.gifthub.auth.service; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Service; +import org.swmaestro.repl.gifthub.auth.dto.GoogleDto; +import org.swmaestro.repl.gifthub.auth.dto.TokenDto; +import org.swmaestro.repl.gifthub.auth.entity.Member; +import org.swmaestro.repl.gifthub.auth.repository.MemberRepository; +import org.swmaestro.repl.gifthub.exception.BusinessException; +import org.swmaestro.repl.gifthub.exception.ErrorCode; +import org.swmaestro.repl.gifthub.util.JwtProvider; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; + +@Service +@PropertySource("classpath:application.yml") +public class GoogleService { + private final MemberService memberService; + private final MemberRepository memberRepository; + private final RefreshTokenService refreshTokenService; + private final JwtProvider jwtProvider; + private final String clientId; + private final String redirectUri; + private final String clientSecret; + + public GoogleService(MemberService memberService, MemberRepository memberRepository, RefreshTokenService refreshTokenService, JwtProvider jwtProvider, + @Value("${google.client_id}") String clientId, @Value("${google.client_secret}") String clientSecret, @Value("${google.redirect_uri}") String redirectUri) { + this.memberService = memberService; + this.memberRepository = memberRepository; + this.refreshTokenService = refreshTokenService; + this.jwtProvider = jwtProvider; + this.clientId = clientId; + this.redirectUri = redirectUri; + this.clientSecret = clientSecret; + } + + public TokenDto getToken(String code) { + String reqURL = "https://oauth2.googleapis.com/token"; + TokenDto tokenDto = null; + + try { + URL url = new URL(reqURL); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream())); + StringBuilder sb = new StringBuilder(); + + sb.append("grant_type=authorization_code"); + sb.append("&client_id=" + clientId); + sb.append("&client_secret=" + clientSecret); + sb.append("&redirect_uri=" + redirectUri); + sb.append("&code=" + code); + bw.write(sb.toString()); + bw.flush(); + + int responseCode = conn.getResponseCode(); + System.out.println("responseCode : " + responseCode); + + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + + String line = ""; + String result = ""; + + while ((line = br.readLine()) != null) { + result += line; + } + System.out.println("response body : " + result); + + JsonParser parser = new JsonParser(); + JsonElement element = parser.parse(result); + + String accessToken = element.getAsJsonObject().get("access_token").getAsString(); + + br.close(); + bw.close(); + tokenDto = TokenDto.builder() + .accessToken(accessToken) + .build(); + + } catch (ProtocolException e) { + throw new BusinessException("잘못된 프로토콜을 사용하였습니다.", ErrorCode.INVALID_INPUT_VALUE); + } catch (MalformedURLException e) { + throw new BusinessException("잘못된 URL 형식을 사용하였습니다.", ErrorCode.INVALID_INPUT_VALUE); + } catch (IOException e) { + throw new BusinessException("HTTP 연결을 수행하는 동안 입출력 관련 오류가 발생하였습니다.", ErrorCode.INTERNAL_SERVER_ERROR); + } + return tokenDto; + } + + public GoogleDto getUserInfo(TokenDto tokenDto) { + String reqURL = "https://www.googleapis.com/oauth2/v2/userinfo"; + + GoogleDto googleDto = null; + + try { + URL url = new URL(reqURL); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + conn.setRequestMethod("GET"); + conn.setDoOutput(true); + + conn.setRequestProperty("Authorization", "Bearer " + tokenDto.getAccessToken()); + + int responseCode = conn.getResponseCode(); + + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line = ""; + String result = ""; + + while ((line = br.readLine()) != null) { + result += line; + } + + JsonParser parser = new JsonParser(); + JsonElement element = parser.parse(result); + + String nickname = element.getAsJsonObject().get("name").getAsString(); + String email = element.getAsJsonObject().get("email").getAsString(); + + br.close(); + googleDto = GoogleDto.builder() + .nickname(nickname) + .username(email) + .build(); + } catch (ProtocolException e) { + throw new BusinessException("잘못된 프로토콜을 사용하였습니다.", ErrorCode.INVALID_INPUT_VALUE); + } catch (MalformedURLException e) { + throw new BusinessException("잘못된 URL 형식을 사용하였습니다.", ErrorCode.INVALID_INPUT_VALUE); + } catch (IOException e) { + throw new BusinessException("HTTP 연결을 수행하는 동안 입출력 관련 오류가 발생하였습니다.", ErrorCode.INTERNAL_SERVER_ERROR); + } + return googleDto; + } + + public TokenDto signIn(GoogleDto googleDto) { + if (memberService.isDuplicateUsername(googleDto.getUsername())) { + TokenDto tokenDto = signInWithExistingMember(googleDto); + return tokenDto; + } + Member member = convertGoogleDtotoMember(googleDto); + + memberRepository.save(member); + + String accessToken = jwtProvider.generateToken(member.getUsername()); + String refreshToken = jwtProvider.generateRefreshToken(member.getUsername()); + + TokenDto tokenDto = TokenDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + refreshTokenService.storeRefreshToken(tokenDto, member.getUsername()); + + return tokenDto; + } + + public TokenDto signInWithExistingMember(GoogleDto googleDto) { + Member member = memberRepository.findByUsername(googleDto.getUsername()); + if (member == null) { + throw new BusinessException("존재하지 않는 아이디입니다.", ErrorCode.INVALID_INPUT_VALUE); + } + String accessToken = jwtProvider.generateToken(member.getUsername()); + String refreshToken = jwtProvider.generateRefreshToken(member.getUsername()); + + TokenDto tokenDto = TokenDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .build(); + + refreshTokenService.storeRefreshToken(tokenDto, member.getUsername()); + + return tokenDto; + } + + public Member convertGoogleDtotoMember(GoogleDto googleDto) { + return Member.builder() + .nickname(googleDto.getNickname()) + .username(googleDto.getUsername()) + .build(); + } +} diff --git a/src/main/java/org/swmaestro/repl/gifthub/config/SecurityConfig.java b/src/main/java/org/swmaestro/repl/gifthub/config/SecurityConfig.java index 872b536b..12d4936a 100644 --- a/src/main/java/org/swmaestro/repl/gifthub/config/SecurityConfig.java +++ b/src/main/java/org/swmaestro/repl/gifthub/config/SecurityConfig.java @@ -29,33 +29,33 @@ public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(authorizeHttpRequests -> - authorizeHttpRequests.requestMatchers("/auth/sign-up", "/auth/sign-in", "/auth/kakao/**", "/auth/sign-in/**", - "/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/error").permitAll() - .anyRequest().authenticated()) - .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .exceptionHandling(exceptionHandling -> exceptionHandling - .accessDeniedHandler(new AccessDeniedHandler() { - @Override - public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException, IOException { - // 권한 문제가 발생했을 때 이 부분을 호출한다. - response.setStatus(403); - response.setCharacterEncoding("utf-8"); - response.setContentType("text/html; charset=UTF-8"); - response.getWriter().write("권한이 없습니다."); - } - }) - .authenticationEntryPoint(new AuthenticationEntryPoint() { - @Override - public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { - // 인증문제가 발생했을 때 이 부분을 호출한다. - response.setStatus(401); - response.setCharacterEncoding("utf-8"); - response.setContentType("text/html; charset=UTF-8"); - response.getWriter().write("인증되지 않은 사용자입니다."); - } - }) - ); + .authorizeHttpRequests(authorizeHttpRequests -> + authorizeHttpRequests.requestMatchers("/auth/sign-up", "/auth/sign-in", "/auth/kakao/**", "/auth/google/**", "/auth/sign-in/**", + "/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/webjars/**", "/error").permitAll() + .anyRequest().authenticated()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exceptionHandling -> exceptionHandling + .accessDeniedHandler(new AccessDeniedHandler() { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException, IOException { + // 권한 문제가 발생했을 때 이 부분을 호출한다. + response.setStatus(403); + response.setCharacterEncoding("utf-8"); + response.setContentType("text/html; charset=UTF-8"); + response.getWriter().write("권한이 없습니다."); + } + }) + .authenticationEntryPoint(new AuthenticationEntryPoint() { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + // 인증문제가 발생했을 때 이 부분을 호출한다. + response.setStatus(401); + response.setCharacterEncoding("utf-8"); + response.setContentType("text/html; charset=UTF-8"); + response.getWriter().write("인증되지 않은 사용자입니다."); + } + }) + ); return httpSecurity.build(); } diff --git a/src/test/java/org/swmaestro/repl/gifthub/auth/controller/GoogleControllerTest.java b/src/test/java/org/swmaestro/repl/gifthub/auth/controller/GoogleControllerTest.java new file mode 100644 index 00000000..368c85c6 --- /dev/null +++ b/src/test/java/org/swmaestro/repl/gifthub/auth/controller/GoogleControllerTest.java @@ -0,0 +1,60 @@ +package org.swmaestro.repl.gifthub.auth.controller; + +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.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.swmaestro.repl.gifthub.auth.dto.GoogleDto; +import org.swmaestro.repl.gifthub.auth.dto.TokenDto; +import org.swmaestro.repl.gifthub.auth.service.GoogleService; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@AutoConfigureMockMvc +public class GoogleControllerTest { + @Autowired + private MockMvc mockMvc; + @MockBean + private GoogleService googleService; + + @Test + public void callbackTest() throws Exception { + mockMvc.perform(get("/auth/google/callback") + .param("code", "myawesomecode")) + .andExpect(status().isOk()); + } + + @Test + public void signInTest() throws Exception { + String code = "myawesomecode"; + + TokenDto googleTokenDto = TokenDto.builder() + .accessToken("myawesomeKakaojwt") + .refreshToken("myawesomeKakaojwt") + .build(); + + TokenDto tokenDto = TokenDto.builder() + .accessToken("myawesomejwt") + .refreshToken("myawesomejwt") + .build(); + + GoogleDto googleDto = GoogleDto.builder() + .nickname("정인희") + .username("dls@gmail.com") + .build(); + + when(googleService.getToken(code)).thenReturn(googleTokenDto); + when(googleService.getUserInfo(googleTokenDto)).thenReturn(googleDto); + when(googleService.signIn(googleDto)).thenReturn(tokenDto); + + mockMvc.perform(post("/auth/google/sign-in") + .header("Authorization", "Bearer " + code)) + .andExpect(status().isOk()); + } +}