들어가는 글
항상 백앤드와 프론트가 한 서버에 존재하는 개발만 해보았고 구글로그인도 그 때 해봤지만 이 번에 하는 프로젝트는 프론트와 백앤드의 서버가 각자 존재한다. 이렇게 프론트와 백앤드서버가 따로 존재할 때는 구글로그인을 프론트에서 다 해주거나 프론트에서 인가코드를 받고 그 코드를 백앤드에 넘겨 이어서 진행하는 방법으로 해야한다.
소셜로그인 흐름
- 리액트에서 유저가 구글로그인을 시도하여 인가코드를 요청한다.
- 구글에서 응답으로 구글 로그인을 시도한 유저의 인가코드를 같이 준다.
- 리액트에서는 받은 인가코드를 서버에 엑세스토큰과 리플레쉬토큰을 응답받기위해 요청헤더에 담아 보낸다.
- 인가코드를 받은 서버에서는 구글에게 구글플랫폼에 등록한 redirectUrl, clientId, clientSecret, 인가코드, 설정을 Body에 담아 구글에 엑세스토큰을 요청한다.
- 구글해서는 리액트와 동일한 redirectUrl, clientId, clientSecret 그리고 인가코드를 가지고 있는지 확인 후 유저의 토큰을 보내준다.
- 구글에서 준 토큰을 구글에게 요청하고 구글은 그 토큰에 맞는 유저를 보내준다.
- 서버는 구글에게 받은 유저정보를 받는다.
- 서버는 그 유저정보를 db에 저장하고 커스텀 토큰을 리액트에게 준다.
구글 플래폼 설정하는법
리액트에는 정확하게 어떻게하는지는 모르지만 아래 래퍼런스에서 자바스크립트를 좀 알아보는 걸 추천한다.
아래 링크는 메모용으로 가져가는 링크 아래 링크는 인가코드를 받는 로직은 없지만 인가코드를 건너 띄고 엑세스토큰부터 가능하다.
토큰을 받을 때 사용한 로직
- GoogleLoginType 는 구글과 통신할 때 사용하는 상수를 enum으로 정리했다.
public enum GoogleLoginType {
GOOGLE_ACCESS_TOKEN_URL("googleAccessTokenUrl", "accounts.google.com"),
GOOGLE_USER_INFO_URL("googleUserInfoUrl", "www.googleapis.com"),
GOOGLE_ACCESS_PATH("path", "/o/oauth2/token"),
GOOGLE_USER_INFO_PATH("path", "/oauth2/v3/tokeninfo"),
ACCESS_TOKEN("access_token"),
PROTOCOL("protocol", "https"),
CLIENT_ID("client_id"),
CLIENT_SECRET("client_secret"),
CODE("code"),
GRANT_TYPE("grant_type", "authorization_code"),
REDIRECT_URI("redirect_uri", "http://localhost:3000");
private String name;
private String value;
GoogleLoginType(String name, String value) {
this.name = name;
this.value = value;
}
GoogleLoginType(String name) {
this.name = name;
}
}
- 구글에 엑세스토큰을 받기위해 사용한 로직
code는 리액트에서 받은 인가코드
clientId와 clientSecret는 구글플랫폼에서 받은 값을 사용하면된다.
public SocialLoginRequestDto findAccessTokenByCode(String code) {
HttpHeaders headers = new HttpHeaders();
ResponseEntity<SocialLoginRequestDto> responseEntity;
try {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
UriComponents googleUrl = UriComponentsBuilder.newInstance()
.scheme(PROTOCOL.getValue())
.host(GOOGLE_ACCESS_TOKEN_URL.getValue())
.path(GOOGLE_ACCESS_PATH.getValue())
.build(true);
UriComponents body = UriComponentsBuilder.newInstance()
.queryParam(CODE.getName(), code)
.queryParam(CLIENT_ID.getName(), clientId)
.queryParam(CLIENT_SECRET.getName(), clientSecret)
.queryParam(GRANT_TYPE.getName(), GRANT_TYPE.getValue())
.queryParam(REDIRECT_URI.getName(), REDIRECT_URI.getValue())
.build(true);
responseEntity = restTemplate.exchange(
googleUrl.toString(),
HttpMethod.POST,
new HttpEntity<String>(body.toString().substring(1), headers),
SocialLoginRequestDto.class
);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
return statusCheck(responseEntity);
}
- 엑세스토큰을 가지고 유저정보를 구글에게 요청할 때 사용한 로직
public SocialLoginRequestDto googleUserInfoByAccessToken(String accessToken) {
ResponseEntity<SocialLoginRequestDto> responseEntity;
try {
UriComponents googleUrl = UriComponentsBuilder.newInstance()
.scheme(PROTOCOL.getValue())
.host(GOOGLE_USER_INFO_URL.getValue())
.path(GOOGLE_USER_INFO_PATH.getValue())
.queryParam(ACCESS_TOKEN.getName(), accessToken)
.build(true);
responseEntity = restTemplate.exchange(
googleUrl.toString(),
HttpMethod.GET,
new HttpEntity<String>("", new HttpHeaders()),
SocialLoginRequestDto.class
);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
return statusCheck(responseEntity);
}
전체로직
- controller
@PostMapping("/user/social")
public TokenResponseDto socialLogin(@RequestHeader(SOCIAL_HEADER_KEY) String code) {
return userService.findAccessTokenByCode(code);
}
- service
@Transactional
public TokenResponseDto findAccessTokenByCode(String code) {
SocialLoginRequestDto response = socialLoginRestTemplate.findAccessTokenByCode(code);
SocialLoginRequestDto socialLoginRequestDto = googleUserInfoByAccessToken(response.getAccess_token());
Users user = userRepository.findByUsername(socialLoginRequestDto.getEmail())
.orElseGet(() ->
userRepository.save(
new Users(
socialLoginRequestDto,
userRepository.findAllCount()
)
)
);
return createTokens2(user.getUsername());
}
- RestTemplate로 통신하는 API
@Component
public class SocialLoginRestTemplate {
private final RestTemplate restTemplate;
private final String clientId;
private final String clientSecret;
public SocialLoginRestTemplate(RestTemplate restTemplate,
@Value("${client_id}") String clientId,
@Value("${secret_key}") String clientSecret) {
this.restTemplate = restTemplate;
this.clientId = clientId;
this.clientSecret = clientSecret;
}
public SocialLoginRequestDto findAccessTokenByCode(String code) {
HttpHeaders headers = new HttpHeaders();
ResponseEntity<SocialLoginRequestDto> responseEntity;
try {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
UriComponents googleUrl = UriComponentsBuilder.newInstance()
.scheme(PROTOCOL.getValue())
.host(GOOGLE_ACCESS_TOKEN_URL.getValue())
.path(GOOGLE_ACCESS_PATH.getValue())
.build(true);
UriComponents body = UriComponentsBuilder.newInstance()
.queryParam(CODE.getName(), code)
.queryParam(CLIENT_ID.getName(), clientId)
.queryParam(CLIENT_SECRET.getName(), clientSecret)
.queryParam(GRANT_TYPE.getName(), GRANT_TYPE.getValue())
.queryParam(REDIRECT_URI.getName(), REDIRECT_URI.getValue())
.build(true);
responseEntity = restTemplate.exchange(
googleUrl.toString(),
HttpMethod.POST,
new HttpEntity<String>(body.toString().substring(1), headers),
SocialLoginRequestDto.class
);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
return statusCheck(responseEntity);
}
public SocialLoginRequestDto googleUserInfoByAccessToken(String accessToken) {
ResponseEntity<SocialLoginRequestDto> responseEntity;
try {
UriComponents googleUrl = UriComponentsBuilder.newInstance()
.scheme(PROTOCOL.getValue())
.host(GOOGLE_USER_INFO_URL.getValue())
.path(GOOGLE_USER_INFO_PATH.getValue())
.queryParam(ACCESS_TOKEN.getName(), accessToken)
.build(true);
responseEntity = restTemplate.exchange(
googleUrl.toString(),
HttpMethod.GET,
new HttpEntity<String>("", new HttpHeaders()),
SocialLoginRequestDto.class
);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
return statusCheck(responseEntity);
}
private SocialLoginRequestDto statusCheck(ResponseEntity<SocialLoginRequestDto> socialLoginRequestDtoResponseEntity){
int statusCode = socialLoginRequestDtoResponseEntity.getStatusCode().value();
if (statusCode != 200) {
throw new RuntimeException("statusCode = " + statusCode);
}
return socialLoginRequestDtoResponseEntity.getBody();
}
}
마치는 글
RestTemplate를 한번 이용해보고 싶었지만 이렇게나마 사용 할 수 있어서 만족했고 최대한 깔끔한 코드를 작성하고 싶었는데 그래도 나름 만족을 하고있다. 시큐리티를 사용해 Oauth2를 이용하는 소셜로그인보다 더 깔끔하고 쉬워서 놀랍다.
'몰아 넣기' 카테고리의 다른 글
[자바/spring] 이미지 업로드 예제 (0) | 2022.07.10 |
---|---|
[java/spring] UUID를 사용해서 파일이름 랜덤으로 만들기 또는 이미지 확장자 체크하기 (0) | 2022.07.10 |
[그 외] 프로젝트 폴더 구조 (0) | 2022.06.29 |
[그 외] REST API 란? (0) | 2022.06.29 |
[그 외] 클라이언트에서 서버 흐름 (0) | 2022.06.29 |