💻 버전 관리
Spring Boot : 3.2.0
JDK : 17
Build : Maven
대부분의 서비스는 자체적인 로그인 / 회원가입도 있으나, 외부 플랫폼의 유저 정보를 활용하여 서비스에 도입한다. 보통 소셜 로그인이라고 하는데 이에 대해 공부한 내용과 코드를 정리하고자 한다.
우리 서비스에서 타 플랫폼에 저장되어 있는 유저의 정보를 가져와 별도의 회원가입 절차 없이 플랫폼 유저 정보를 활용하여 우리 서비스를 이용하는 것을 말한다.
처음엔 단순히 이런식의 구조를 생각하였다. 공부를 하면서 알게 된 거지만, 이 구조는 미친 짓이라고 한다.
그 이유는
타 플랫폼 입장에서 사용자의 민감한 정보를 탈취당한 입장이기에, 우리 서비스에 소송을 걸어올 수도 있는 상황이 야기된다.
이런 문제를 해결하기 위해서 OAuth 가 등장하기 이전에는 구글에서 AuthSub, 야후의 BBAuth 등 각자 회사가 개발한 통신 규약을 사용하도록 했다고 한다.
이렇게 되면 HTTP 마냥 표준화가 되지 않기 때문에 구글 로그인을 구현할 땐 AuthSub, 야후를 구현할 땐 BBAut에 맞춰 개발하고 유지보수를 해야 하는데 굉장히 머리아플것이다.
이를 위해서 처음으로 나온게 OAuth1.0 이다.
지금 현재까지 자주 사용되는 OAuth2.0은 OAuth1.0를 조금 더 단순화 하고 모바일에서도 안전하게 사용될 수 있도록 업그레이드 하여 릴리즈 된 버전이라고 한다.
OAuth 통신을 정확하게 이해하려면 3가지 용어를 꼭 이해해야 한다.
플로우는 생각보다 간단하다!
동작 매커니즘
Resource Owner
(김멋사)가 우리 서비스의 카카오로 로그인하기 버튼을 클릭즉, 카카오 로그인 페이지
호출!
이때, 플랫폼마다 변수 값은 조금 다르긴 하지만, response_type, client_id, redirect_uri, scope 등을 매개로 요청한다! (Front 에서 하는 일)
Authorization Serve
는 Redirect URI
(우리 서버) 로 리다이렉트 시킨다. 이때, Authorization Code
를 같이 넘겨 준다! 이때의 Authorization Code
는 Resource
를 얻기 위한 Access Token
을 획득하기 위해 잠시 사용하는 임시 코드!(Authorization → Access Token 라는 것이 핵심)
Authorization Code
를 Authorization Server
로 전달해 Access Token
을 발급.Access Token
으로 Resource Server
에서 Resource
에 접근하고 Resource Owner
에게 로그인 완료 전송.💡 왜 Authorization Code 가 필요할까?
Authentication Server 에서 AccessToken 을 바로 넘겨주면 되지 않나? 라는 생각을 해봤다.
그 이유는 Authorization Server에서 Access Token 을 발급 하고 다시 Redirect URI 를 넘어와야 하는데 그 과정에서 노출이 되면 OAuth2.0 통신을 하는 이유가 사라진다고 한다. (앞서 얘기한 내용과 동일한 내용)
때문에 때문에 AccessToken 은 프론트단에서 Authorization Code 를 백엔드단에 넘겨주고 백엔드 단에서 AccessToken 을 저장하는 것이 가장 안전하다!
카카오 공식 문서 에서 가져온 내용이다.
상단 메뉴에 [내 애플리케이션] → [생성]
공식문서를 읽어보면 인가코드를 가져오기 위한 사전 준비는 다음과 같다.
사전설정 |
---|
플랫폼 등록 |
카카오 로그인 활성화 |
RedirectURI 등록 |
동의항목 |
OpenID Connect 활성화 (선택) |
간편가입 (선택) |
카카오 API는 카카오디벨로퍼스에 플랫폼 정보가 등록된 서비스에서만 사용 가능하다.
[내 애플리케이션] > [플랫폼]에서 서비스의 각 플랫폼 정보를 등록할 수 있다.
카카오 ID/PW 입력 후 Authorization Code(인가코드) 를 redirect 할 서버 URI 를 지정하는 단계
왼쪽 탭 [카카오 로그인] → Redirect URI 등록
Resource Server 에서 어떤 정보들을 가져올 수 있는지 선택하는 단계
권한을 늘리고 싶으면 비즈니스 앱으로 인증을 받아야 한다.
여기까지 사전준비 하면 사용하기만 하면 된다.
resources/templates/login.html
로그인 버튼을 누른다는 시나리오로 타 플랫폼 로그인 화면을 불러오기 위해 간단한 html 과 authorizationCode를 callback 할 redirect controller 를 작성하였다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<a href="https://kauth.kakao.com/oauth/authorize?response_type=code&client_id=${REST_API_KEY}&redirect_uri=${REDIRECT_URI}"><img src="/kakao_login_large_wide.png"></a>
</body>
</html>
controller/AuthController.java
@RestController
@RequestMapping("/api/v1/auth")
@RequiredArgsConstructor
@Slf4j
public class AuthController {
private final AuthService authService;
@GetMapping("/kakao/callback")
public ResponseEntity<?> getKaKaoAuthorizeCode(@RequestParam("code") String authorizeCode, String type){
type = "kakao";
log.info("[google login] authorizeCode : {}", authorizeCode);
return authService.signIn(authorizeCode, type);
}
의존성 주입을 위한 서비스 인터페이스를 생성하였다.
service/AuthService
public interface AuthService {
ResponseEntity<?> signIn(String authorizeCode, String type);
}
이제 AccessToken 을 발급 받아야 한다.
💡 코드에 상수값이 있는 것은 보기 좋지 않기에 설정 파일에서 관리!
application.properties
spring.application.name=6W
kakao.client.id = ${CLIENT_ID}
kakao.redirect.url = ${REDIRECT_URL}
kakao.accesstoken.url = https://kauth.kakao.com/oauth/token
kakao.userinfo.url = https://kapi.kakao.com/v2/user/me
service/impl/AuthServiceImpl
구현체
@Service
@Slf4j
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final AuthDAO authDAO;
@Value("${kakao.client.id}")
private String kakaoClientKey;
@Value("${kakao.redirect.url}")
private String kakaoRedirectUrl;
@Value("{$kakao.accesstoken.url}")
private String kakaoAccessTokenUrl;
@Value("${kakao.userinfo.url}")
private String kakaoUserInfoUrl;
@Override
public ResponseEntity<?> signIn(String authorizeCode, String type) {
switch (type){
case "kakao":
log.info("[kakao login] issue a authorizecode");
ObjectMapper objectMapper = new ObjectMapper();
RestTemplate restTemplate = new RestTemplate(); // http 통신을 위한 객체
HttpHeaders headers = new HttpHeaders();
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); // 해당 명세에 맡게 파라미터 지정
params.add("grant_type", "authorization_code");
params.add("client_id", kakaoClientKey);
params.add("redirect_uri", kakaoRedirectUrl);
params.add("code", authorizeCode);
HttpEntity<MultiValueMap<String, String>> kakaoTokenRequest = new HttpEntity<>(params, headers);
try{
ResponseEntity<String> response = restTemplate.exchange(
kakaoAccessTokenUrl,
HttpMethod.POST,
kakaoTokenRequest,
String.class
);
log.info("[kakao login] authorizecode issued successfully");
Map<String, Object> responseMap = objectMapper.readValue(response.getBody(), new TypeReference<Map<String, Object>>() {});
String accessToken = (String) responseMap.get("access_token");
RequestAuthDto requestSignUpDto = getKakaoUserInfo(accessToken);
return authDAO.login(requestSignUpDto);
}catch (Exception e){
log.warn("[kakao login] fail authorizecode issued");
return ResponseEntity.status(ResultCode.PASSWORD_NOT_MATCH.getCode())
.body(CommonResponse.fail(ResultCode.PASSWORD_NOT_MATCH));
}
}
return null;
}
이제 사용자 정보를 가져와야 한다.
해당 API 명세는 다음과 같다.
service/impl/AuthServiceImpl
private RequestAuthDto getKakaoUserInfo(String accessToken){
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
ObjectMapper mapper = new ObjectMapper();
headers.add("Authorization", "Bearer "+accessToken);
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
MultiValueMap<String, String> requestBody = new LinkedMultiValueMap<>();
requestBody.add("secure_resource", "true");
HttpEntity<?> entity = new HttpEntity<>(requestBody,headers);
ResponseEntity<String> response = restTemplate.postForEntity(kakaoUserInfoUrl,entity,String.class);
try{
Map<String, Object> responseMap = mapper.readValue(response.getBody(), new TypeReference<Map<String, Object>>() {});
Map<String, Object> kakaoAccount = (Map<String, Object>) responseMap.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
RequestAuthDto requestSignUpDto = RequestAuthDto.builder()
.name((String) kakaoAccount.get("name"))
.nickName((String) kakaoAccount.get("nickname"))
.password(getRandomPassword())
.phoneNumber((String) kakaoAccount.get("phone_number"))
.email((String)kakaoAccount.get("email"))
.profileUrl((String) profile.get("profile_image_url"))
.loginType(LoginType.KAKAO.toString())
.useAble(true)
.build();
return requestSignUpDto;
}catch (Exception e){
e.printStackTrace();
return null;
}
}
마지막으로 우리 서비스 DB에 회원가입이 이미 되어 있는지 확인 후 회원가입 or 로그인을 DAO 단에서 해결하면 된다.
dao/AuthDAO
public interface AuthDAO {
ResponseEntity<?> login(RequestAuthDto requestAuthDto);
}
dao/impl/AuthDAOImpl
@Component
@RequiredArgsConstructor
@Slf4j
public class AuthDAOImpl implements AuthDAO {
private final UserRepository userRepository;
private final JwtTokenProvider jwtTokenProvider;
@Override
public ResponseEntity<?> login(RequestAuthDto requestAuthDto) {
if (checkUserExist(requestAuthDto.getEmail(), requestAuthDto.getLoginType())) {
User user = userRepository.findByEmailAndLoginType(requestAuthDto.getEmail(), requestAuthDto.getLoginType());
log.info("user name : {}",user.getUsername());
if (user.isEnabled()) {
return ResponseEntity.status(ResultCode.OK.getCode())
.body(ResponseAuthDto.builder()
.accessToken(jwtTokenProvider.createAccessToken(user.getEmail(), user.getRoles()))
.refreshToken(jwtTokenProvider.createRefreshToken(user.getEmail()))
.name(user.getUsername())
.status(CommonResponse.success())
.build());
} else {
return ResponseEntity.status(ResultCode.DELETED_USER.getCode())
.body(ResultCode.DELETED_USER);
}
} else {
log.info("[sign up] no user");
CommonResponse commonResponse = signUp(requestAuthDto);
if (commonResponse.getCode() == 200) {
return login(requestAuthDto);
}
}
return null;
}
private CommonResponse signUp(RequestAuthDto requestAuthDto){
User user = User.builder()
.name(requestAuthDto.getName())
.nickName(requestAuthDto.getNickName())
.password(requestAuthDto.getPassword())
.phoneNumber(requestAuthDto.getPhoneNumber())
.email(requestAuthDto.getEmail())
.profileUrl(requestAuthDto.getProfileUrl())
.loginType(requestAuthDto.getLoginType())
.useAble(requestAuthDto.isUseAble())
.roles(Collections.singletonList("ROLE_USER"))
.build();
userRepository.save(user);
return CommonResponse.success();
}
private boolean checkUserExist(String email, String loginType){
return userRepository.existsByEmailAndLoginType(email, loginType);
}
}
안녕하세요 카카오로그인관련 질문이 있는데요! 백엔드 측에서 저렇게 마지막 사진처럼 로그인이 성공한게 html에 보여지는거 이후에 로그인 성공시 어떠한 페이지로 이동하는것도 혹시 백엔드측에서 처리를 해야하는건가요?