OAuth 2.0(Open Authorization 2.0, OAuth2)은 인증을 위한 개방형 표준 프로토콜입니다.
이 프로토콜에서는 Third-Party 프로그램에게 리소스 소유자를 대신하여 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식을 제공합니다.
구글, 페이스북, 카카오, 네이버 등에서 제공하는 간편 로그인 기능도 OAuth2 프로토콜 기반의 사용자 인증 기능을 제공하고 있습니다.
Client
Resource Owner
Resource Server
Authorization Server
Authorization endpoint와 Token endpoint를 가집니다.Authorization Code Grant│ 권한 부여 승인 코드 방식
권한 부여 승인을 위해 자체 생성한 Authorization Code를 전달하는 방식으로 많이 쓰이고 기본이 되는 방식입니다.
Implicit Grant │ 암묵적 승인 방식
자격증명을 안전하게 저장하기 힘든 클라이언트(ex: JavaScript등의 스크립트 언어를 사용한 브라우저)에게 최적화된 방식입니다.
암시적 승인 방식에서는 권한 부여 승인 코드 없이 바로 Access Token이 발급 됩니다. Access Token이 바로 전달되므로 만료기간을 짧게 설정하여 누출의 위험을 줄일 필요가 있습니다.
Refresh Token 사용이 불가능한 방식이며, 이 방식에서 권한 서버는 client_secret를 사용해 클라이언트를 인증하지 않습니다. Access Token을 획득하기 위한 절차가 간소화되기에 응답성과 효율성은 높아지지만 Access Token이 URL로 전달된다는 단점이 있습니다.
Resource Owner Password Credentials Grant │ 자원 소유자 자격증명 승인 방식
간단하게 username, password로 Access Token을 받는 방식입니다.
클라이언트가 타사의 외부 프로그램일 경우에는 이 방식을 적용하면 안됩니다. 자신의 서비스에서 제공하는 어플리케이션일 경우에만 사용되는 인증 방식입니다. Refresh Token의 사용도 가능합니다.
Client Credentials Grant │클라이언트 자격증명 승인 방식
클라이언트의 자격증명만으로 Access Token을 획득하는 방식입니다.
OAuth2의 권한 부여 방식 중 가장 간단한 방식으로 클라이언트 자신이 관리하는 리소스 혹은 권한 서버에 해당 클라이언트를 위한 제한된 리소스 접근 권한이 설정되어 있는 경우 사용됩니다.
이 방식은 자격증명을 안전하게 보관할 수 있는 클라이언트에서만 사용되어야 하며, Refresh Token은 사용할 수 없습니다.

카카오 디벨롭스에서 발급 받은 여러 key값들을 아래와 같이 새로 추가합니다.
security:
oauth2:
client:
registration:
kakao:
client-name: kakao
client-id: f66ad78db368781970e4086debb56661
client-secret: y4Rv3gbKYIJdcyLZbtY6VGVnLdlhnkY7
client-authentication-method: POST
redirect-uri: "http://localhost:8080/api/v1/oauth2/code/kakao"
authorization-grant-type: authorization_code
scope: account_email, profile_nickname, profile_image, openid
provider : kakao
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeHttpRequests()
.requestMatchers("/api/v1/auth/**").permitAll()
...
.oauth2Login(login -> login
.clientRegistrationRepository(clientRegistrationRepository())
.authorizedClientRepository(authorizedClientRepository())
.userInfoEndpoint()
.userService(customOAuth2UserService));
return http.build();
}
...
@Bean
public ClientRegistrationRepository clientRegistrationRepository(){
return new InMemoryClientRegistrationRepository(this.kakaoClientRegistration());
}
@Bean
public OAuth2AuthorizedClientRepository authorizedClientRepository() {
return new HttpSessionOAuth2AuthorizedClientRepository();
}
private ClientRegistration kakaoClientRegistration(){
return ClientRegistration.withRegistrationId("kakao")
.clientId("f66ad78db368781970e4086debb56661")
.clientSecret("y4Rv3gbKYIJdcyLZbtY6VGVnLdlhnkY7")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/api/v1/oauth2/code/{registrationId}")
.scope("account_email", "profile_nickname", "profile_image", "openid")
.authorizationUri("https://kauth.kakao.com/oauth/authorize")
.tokenUri("https://kauth.kakao.com/oauth/token")
.userInfoUri("https://kapi.kakao.com/v2/user/me")
.userNameAttributeName("id")
.clientName("kakao")
.build();
}
...
해당 클래스에 kakaoClient에서 부여 받은 값들을 설정합니다.
@Service
@Slf4j
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final HttpSession httpSession;
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String kakaoClientId;
@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
private String kakaoClientSecret;
private String codeVerifier = "YOUR_CODE_VERIFIER";
private String codeChallenge = "YOUR_CODE_CHALLENGE";
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
throw new UnsupportedOperationException("Unimplemented method 'loadUser'");
}
public OAuth2UserInfoResponse kakaoCallback(String code) throws OAuth2AuthenticationException {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = requestHeaders(new HttpHeaders());
MultiValueMap parameters = requestParameters(code, new LinkedMultiValueMap());
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(parameters, headers);
ResponseEntity<String> response = restTemplate.postForEntity("https://kauth.kakao.com/oauth/token", request, String.class);
if (response.getStatusCode() == HttpStatus.OK) {
Gson gson = new Gson();
Map<String, Object> responseMap = gson.fromJson(response.getBody(), Map.class);
for(Object obj : responseMap.keySet()){
log.info(" obj : {} ", obj + " " + responseMap.get(obj));
}
String accessToken = (String) responseMap.get("access_token");
log.info("Access Token: {}", accessToken);
OAuth2UserInfoResponse userInfoResponse = kakaoCall(responseMap);
return userInfoResponse;
} else {
log.error("Error occurred while fetching access token: {}", response.getStatusCode());
throw new UnsupportedOperationException("Unimplemented method 'loadUser'");
}
}
public HttpHeaders requestHeaders(HttpHeaders headers){
String credentials = kakaoClientId + ":" + kakaoClientSecret;
String encodedCredentials = new String(Base64.getEncoder().encode(credentials.getBytes()));
headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8");
headers.add("Authorization", "Basic " + encodedCredentials);
return headers;
}
public MultiValueMap<String, String> requestParameters(String code, LinkedMultiValueMap linkedMultiValueMap){
MultiValueMap<String,String> parameters = linkedMultiValueMap;
parameters.add("grant_type", "authorization_code");
parameters.add("client_id", kakaoClientId);
parameters.add("client_secret", kakaoClientSecret); // Add client_secret
parameters.add("redirect_uri", "http://localhost:8080/api/v1/oauth2/code/kakao");
parameters.add("code", code);
parameters.add("code_verifier", codeVerifier);
// parameters.add("code_challenge", codeChallenge);
return parameters;
}
public OAuth2UserInfoResponse kakaoCall(Map<String, Object> parameters) {
return OAuth2UserInfoResponse.builder()
.accessToken((String)parameters.get("access_token"))
.refreshToken((String)parameters.get("refresh_token"))
.idToken((String)parameters.get("id_token"))
.build();
}
}
@RestController
@RequestMapping("api/v1/oauth2")
@Slf4j
public class OAuth2Controller {
@Autowired
private CustomOAuth2UserService oAuth2UserService;
@GetMapping("/code/kakao")
public ResponseEntity<OAuth2UserInfoResponse> kakaoCallback(@RequestParam String code) {
log.info("code {} =", code);
return ResponseEntity.ok(oAuth2UserService.kakaoCallback(code));
}
}
Postman이 아닌, 웹 브라우저로 KakaoCallback의 URL를 요청하면 카카오 로그인으로 리다이렉트 됩니다. 카카오에 가입된 회원정보를 입력하고 로그인 합니다.
일반적으로는 OAuth2.0을 통해 외부 코드를 받아와서 클라이언트 내에서 자체적으로 처리하지만, 제 경우에는 직접 DTO로 받아서 처리했습니다.
OAuth2User.loadUser() 를 통해서 리소스 서버와 연결해야 할텐데, 일단은 서버에서 토큰을 발급 받았다는 것만으로 의의를 두고자 합니다. 추후에 반드시 수정해야겠죠.비문, 오탈자와 코드 오류 및 잘못된 지식에 대한 지적 및 질문은 언제나 환영합니다.