Spring OAuth2 를 사용해서 카카오 로그인 연동해보기
✍️
spring boot 2.7.13 프로젝트에서
security form login 구현 후
회원가입을 구현하지 않고,
OAuth2 를 통한 카카오톡 간편 가입 / 로그인을 진행하기 위해
kakao 로그인 연동을 진행하게 되었다.
spring 코드 관련해서는
security form 로그인 인증 관련 부분은 기록하지 않고
oauth2 관련해서 추가되는 부분만 기록!
앱 아이콘은 등록하지 않아도 된다.
앱 이름을 적당히 입력해주고
사업자명에는 사용자 실명을 작성해주었다.
입력후 저장!
여기서 보이는 REST API 키가
spring 설정시 client-id
속성의 값으로 쓰인다.
web 플랫폼 등록 버튼을 누르고
사이트 도메인 부분에 애플리케이션이 실행될 url 주소를 전부 입력해준다.
이때 주의해야 할 점은 url 마지막에 /
문자가 붙어버리면 등록이 안된다.
REST API로 개발하는 경우 필수로 등록해주어야 한다고 한다.
활성화 설정
을 ON으로 변경해주고,
Redirct URI 등록 버튼을 누르면 나타나는 입력창에
Redirct URI
를 입력해주어야 한다.
Redirct URI를 설계해서 등록해주어야 하는데,
Spring Security Oauth2 문서에 들어가보면 기본 리다이렉트 uri 템플릿이 있다.
참고하면 좋을것 같다!
카카오 로그인 api를 통해 받고싶은 정보들을 설정해주어야 한다.
왼쪽 네비게이션 메뉴에서
카카오 로그인 - 동의항목
클릭
필요한 항목에서 설정
을 누르고 내용을 작성해준다.
이때 필수 동의
값은 단 1개
의 항목만 가능하다.
나머지
는 선택 동의
로만 요청할 수 있다.
rest api를 통한 인증시 해당 키가 필요하다.
카카오 로그인
-> 보안
-> 코드 생성
클릭 후
코드를 확인하고
활성화 상태를 사용함으로 설정해준다.
외부 서비스에서 OAuth 인증을 받아 사용하려는 것이기 때문에
Spring Initializr에서 OAuth2 Client 를 추가해준다.
spring boot 2.7.13 기준
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
spring:
security:
oauth2:
client:
registration:
kakao:
client-id: ${kakao.client.id} # 앱키 -> REST API 키
client-secret: ${kakao.client.secret} # 카카오 로그인 -> 보안 -> Client Secret 코드
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/kakao" # yml 파일에서 {} 가 spring 특수문자로 인식되게 하기 위해 " " 사용
client-authentication-method: POST
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 # 식별자 . 카카오의 경우 "id" 사용
security의 UserDetails
를 상속받아 구현한 파일에서 진행한다.
추가해준 부분
클래스 파일에 OAuth2User
인터페이스를 상속.
Map<String, Object> getAttributes()
메서드와 String getName()
메서드를 재정의.
필드로 Map<String, Object> oAuth2Attributes
추가.
public record BoardPrincipal(
String username,
String password,
Collection<? extends GrantedAuthority> authorities,
String email,
String nickname,
String memo,
Map<String, Object> oAuth2Attributes // 추가
) implements UserDetails, OAuth2User { // OAuth2User 추가
..
// Spring Security 필수 메서드 재정의
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; }
@Override public String getPassword() { return password; }
@Override public String getUsername() { return username; }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
// OAuth2 Client 필수 메서드 재정의
@Override public Map<String, Object> getAttributes() { return oAuth2Attributes; }
@Override public String getName() { return username; }
}
응답 데이터 항목에서 필요한 항목들만 참고하여 작성한다.
public record KakaoOAuth2Response(
Long id,
LocalDateTime connectedAt,
Map<String, Object> properties,
KakaoAccount kakaoAccount
) {
public record KakaoAccount(
Boolean profileNicknameNeedsAgreement,
Profile profile,
Boolean hasEmail,
Boolean emailNeedsAgreement,
Boolean isEmailValid,
Boolean isEmailVerified,
String email
) {
public record Profile(String nickname) {
public static Profile from(Map<String, Object> attributes) {
return new Profile(String.valueOf(attributes.get("nickname")));
}
}
public static KakaoAccount from(Map<String, Object> attributes) {
return new KakaoAccount(
Boolean.valueOf(String.valueOf(attributes.get("profile_nickname_needs_agreement"))),
Profile.from((Map<String, Object>) attributes.get("profile")),
Boolean.valueOf(String.valueOf(attributes.get("has_email"))),
Boolean.valueOf(String.valueOf(attributes.get("email_needs_agreement"))),
Boolean.valueOf(String.valueOf(attributes.get("is_email_valid"))),
Boolean.valueOf(String.valueOf(attributes.get("is_email_verified"))),
String.valueOf(attributes.get("email"))
);
}
public String nickname() { return this.profile().nickname(); }
}
public static KakaoOAuth2Response from(Map<String, Object> attributes) {
return new KakaoOAuth2Response(
Long.valueOf(String.valueOf(attributes.get("id"))),
LocalDateTime.parse(
String.valueOf(attributes.get("connected_at")),
DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.systemDefault())
),
(Map<String, Object>) attributes.get("properties"),
KakaoAccount.from((Map<String, Object>) attributes.get("kakao_account"))
);
}
public String email() { return this.kakaoAccount().email(); }
public String nickname() { return this.kakaoAccount().nickname(); }
}
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(
HttpSecurity http,
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService
) throws Exception {
return http
.authorizeHttpRequests(
auth -> auth
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
.mvcMatchers(
HttpMethod.GET,
"/",
"/articles",
"/articles-hashtag"
).permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.logout(
logout -> logout
.logoutSuccessUrl("/")
)
// oauth2 로그인 추가
.oauth2Login(
oAuth -> oAuth
.userInfoEndpoint(userInfo -> userInfo.userService(oAuth2UserService))
)
.build();
}
// OAuth2 로그인 인증
// security로 form 로그인 구현 시 UserDetailsService.loadUserByUsername() 을 구현하는 것과 같다.
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService(
UserAccountService userAccountService,
PasswordEncoder passwordEncoder
) {
final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
return userRequest -> {
OAuth2User oAuth2User = delegate.loadUser(userRequest);
KakaoOAuth2Response kakaoResponse = KakaoOAuth2Response.from(oAuth2User.getAttributes());
String registrationId = userRequest.getClientRegistration().getRegistrationId(); // kakao
String providerId = String.valueOf(kakaoResponse.id());
String username = registrationId + "_" + providerId;
String dummyPassword = passwordEncoder.encode("{bcrypt}" + UUID.randomUUID());
return userAccountService.searchUser(username)
.map(BoardPrincipal::from)
.orElseGet(() -> BoardPrincipal.from(
userAccountService.saveUser(
username,
dummyPassword,
kakaoResponse.email(),
kakaoResponse.nickname(),
null
)
));
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
애플리케이션 실행 후
localhost:8080/login 으로 진입해보면
이런 형태로 OAuth 로그인 링크가 생성되어있다.
이 페이지는 온전히 시큐리티가 만들어준다.
kakao 라고 써있는 링크를 누르고
카카오 로그인 웹페이지로 이동하면 성공이다 😊