구글 로그인 구현은 아래의 블로그를 굉장히 많이 참고했습니다!!! 너무너무너무너무 도움이 많이 됐어요 감사합니다...
https://antdev.tistory.com/71

도메인 계층 구조 나타내려고 드디어 tree도 설치하였다! 이번 소셜 로그인을 구현할 때 영향을 받은 도메인은 다음과 같다.
└── com
└── example
└── fit_friends
├── FitFriendsApplication.java
├── config
│ └── auth
│ ├── GoogleOAuth.java
│ ├── SecurityConfig.java
│ ├── SocialOAuth.java
│ └── dto
│ ├── GoogleRequestAccessTokenDto.java
│ ├── OAuthAttributes.java
│ └── SessionUser.java
├── controller
│ ├── LoginController.java
│ └── PostApiController.java
SocialOAuth.java
public interface SocialOAuth {
String getOAuthRedirectUrl();
String requestAccessToken(String code);
}
getOAuthRedirectUrl()requestAccessToken(String code)GoogleOAuth.javaSocialOAuth 의 구현체@Component
@RequiredArgsConstructor
public class GoogleOAuth implements SocialOAuth{
@Value("${google.client.id}")
private String clientId;
@Value("${google.redirect.uri}")
private String redirectUri;
@Value("${google.base.uri}")
private String googleBaseUri;
@Value("${google.client.secret}")
private String clientSecret;
@Value("${google.token_uri}")
private String tokenUri;
@Override
public String getOAuthRedirectUrl() {
Map<String,Object> params = new HashMap<>();
params.put("client_id",clientId);
params.put("response_type","code");
params.put("redirect_uri",redirectUri);
params.put("scope","profile");
String paramsString = params.entrySet().stream()
.map(x->x.getKey()+"="+x.getValue())
.collect(Collectors.joining("&"));
return googleBaseUri+"?"+paramsString;
}
@Override
public String requestAccessToken(String code) {
RestTemplate restTemplate = new RestTemplate();
Map<String,Object> params = new HashMap<>();
params.put("code",code);
params.put("client_id",clientId);
params.put("grant_type","authorization_code");
params.put("response_type","code");
params.put("redirect_uri",redirectUri);
params.put("scope","profile");
params.put("client_secret",clientSecret);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(tokenUri, params, String.class);
if(responseEntity.getStatusCode()== HttpStatus.OK){
return responseEntity.getBody();
}
return "구글 로그인 요청 처리 실패";
}
}
@Value() 로 가져온 application.properties 의 변수값이 null 로 뜬다면,@Value() 가 properties의 값을 가져오는건 클래스가 스프링 빈으로 등록되는 순간인데, 다른 클래스에서 new 연산자를 통해 이 클래스를 생성해버리면 스프링 빈으로 등록되지 않기 때문에 @Value() 를 통해 변수에 값이 저장되지도 않는 것이다. ➡️ 이런 실수를 할 때면 분명 스프링으로 개발하고 있는데 스프링의 기능을 제대로 쓰지 않고 있다는게 느껴진다..getOAuthRedirectUrl()에서 필요한 request params은 4가지이다.
client_id, response_type, redirect_uri,scopeapplication.properties 등에 따로 빼두고 꺼내 쓰기!!String paramsString = params.entrySet().stream()
.map(x->x.getKey()+"="+x.getValue())
.collect(Collectors.joining("&"));
Map.entrySet() :Map의 Key-Value 쌍의 모음
Stream() : 데이터 처리 연산을 지원하도록 저장되어 있는 요소들을 추출하여 반복적인 처리를 가능하게 하는 기능
requestAccessToken(String code)에서 필요한 request params은 6가지이다.
code, client_id, grant_type, response_type, redirect_uri, scope, client_secretredirect_uri 는 모두 구글 클라우드에서 미리 입력해준 uri과 일치해야 한다.url : 요청을 보낼 서버의 urlrequest : POST 요청 시 함께 보낼 객체responseType : 서버의 응답을 어떤 형태로 받을지를 지정하는 Class 객체HttpEntity 를 생성하여 원래 보내야하는 객체와, HttpHeaders 객체를 함께 담는다.PostforObject 나 GET 요청을 수행하는 getForEntity 등 다양하게 존재한다.LoginController.javapublic class LoginController{
@Autowired
private final SocialOAuth socialOAuth;
private final HttpServletResponse response;
@GetMapping("/api/login")
public void loginGoogle() throws Exception{
response.sendRedirect(socialOAuth.getOAuthRedirectUrl());
}
@GetMapping("/login/oauth2/code/google")
public String requestToken(@RequestParam(name="code") String code) throws Exception{
return socialOAuth.requestAccessToken(code);
}
GetMapping 하고 그 다음엔 어떻게 해야하지..? 라는 생각에서 막혀서 진짜 한참을 헤맸다. 계속 controller의 한 메서드 내에서 code 받은걸 다시 accessToken 을 받기 위해 주소를 리디렉트하고.. 복잡하게 생각했는데 그냥 메서드를 각각 다 나누면 생각하기도 훨씬 쉽다!끝인줄 알았는데 생각해보니 accessToken 을 이용해서 유저 정보까지 로드해야했다.! 킵고잉.
token을 얻는 것을 끝으로 하는 LoginController 의 requestToken 을 수정하여 유저 정보까지 로드할 수 있도록 한다.
@GetMapping("/login/oauth2/code/google")
public String requestUserInfo(@RequestParam(name="code") String code) throws Exception{
final String accessToken = socialOAuth.requestAccessToken(code);
return socialOAuth.getUserInfo(accessToken);
}
메서드 이름도 requestUserInfo 로 변경하였다.
socialOAuth.requestAccessToken(code) 값을 리턴하지 않고 SocialOAuth 의 getUserInfo 에 담아 유저 정보 로드를 할 수 있도록
한다.
@Override
public String getUserInfo(String authorizationCode) {
RestTemplate restTemplate = new RestTemplate();
Map<String,Object> params = new HashMap<>();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization","Bearer "+ authorizationCode);
HttpEntity<String> request = new HttpEntity<>(headers);
ResponseEntity<String> responseEntity = restTemplate.exchange(userInfo_uri, HttpMethod.GET,request,String.class);
if(responseEntity.getStatusCode() == HttpStatus.OK){
return responseEntity.getBody();
}
return "구글 사용자 정보 로드 실패";
}
이전과 마찬가지로 RestTemplate 을 이용한다. 한가지 차이는 유저 정보 로드 시에는 헤더 값에 Authorization 필드가 필요하므로 HttpHeaders 클래스에 헤더를 추가하고 이걸 HttpEntity 에 담아서 RestTemplate 로 전송하는 방식을 취한다.
그래서 RestTemplate 의 getforEntity 메서드를 사용할 수 없고 exchange 메서드를 이용한다.
➡️ exchange : 모든 메서드로 사용 가능하고 (ex. GET, POST, PUT...) Http 헤더를 새로 만들 수 있다.