연결 과정 목차
- yml 파일에 OAuth 클라이언트의 설정 정보 작성
- SecurityConfig 수정
- OAuthService 구현
- OAuthAttributes 생성
- build.gradle 의존성 추가
- UserProfile 생성
- OAuth2UserService를 구현한 CustomOAuth2UserService 구현
Spring Boot는 어떻게 yml 파일에 작성한 정보로 OAuth 설정을 할까?
Spring Security OAuth2 - KAKAO, Google, Naver
연결하고싶은 서비스에 OAuth 클라이언트 등록
👍 application.yml 작성
spring:
security:
oauth2:
client:
registration:
kakao:
client-id: 앱 설정 -> 앱 키 -> REST API 키
client-secret: 제품 설정 -> 카카오 로그인 -> 보안 -> Client Secret
redirect-uri: 앱 설정 -> 플랫폼 -> Web // http://localhost:8080/login/oauth2/code/kakao
authorization-grant-type: authorization_code
client-authentication-method: POST
client-name: Kakao
scope:
- 제품 설정 -> 카카오 로그인 -> 동의 항목
- profile_nickname
- profile_image
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
- client
- 카카오 아이디로 로그인하기 위해 만든 애플리케이션의 정보 입력
- Spring Security OAuth2의 redirect uri 템플릿은 {baseUrl}/login/oauth2/code/{registrationId} 형식이므로 이에 맞춰 작성
- scope에 동의 항목 설정 한 내용 추가
- provider
- Spring Security OAuth2의 경우 provider에 대한 정보를 구글과 페이스북, 트위터 등을 가지고 있음
- 우리나라에서만 한정적으로 사용하는 네이버나, 카카오 같은 서비스에 대한 정보들은 직접 등록 필요
✌️ SecurityConfig 수정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizeRequests ->
authorizeRequests
.anyRequest()
.permitAll()
);
// 추가
http.oauth2Login() // OAuth2 로그인 구성을 위한 설정 가져옴
.userInfoEndpoint() // 사용자 정보 엔드포인트에 대한 설정 추가
.userService(customOAuth2UserService); // 사용자 서비스 구성(로그인 성공 후 사용자 정보 처리)
return http.build();
}
}
@Configuration 어노테이션을 사용하여 Spring Security 설정을 정의하고, @EnableWebSecurity 어노테이션을 통해 웹 보안을 활성화한다.
🤟 OAuthService 구현
1️⃣ OAuthAttributes
OAuthAttributes enum 클래스는 여러 OAuth 공급자(ex. google, kakao, naver 등)의 속성을 처리하는 역할을 한다.
서비스에 따라 얻어온 유저 정보의 key 값이 다르기 때문에 각각 관리해주어야 한다.
public enum OAuthAttributes {
GOOGLE("google", attributes -> new UserProfile(
String.valueOf(attributes.get("sub")),
(String) attributes.get("name"),
(String) attributes.get("email"),
(String) attributes.get("picture")
)),
NAVER("naver", attributes -> {
Map<String, Object> response = (Map<String, Object>) attributes.get("response");
return new UserProfile(
(String) response.get("id"),
(String) response.get("name"),
(String) response.get("email"),
(String) response.get("profile_image")
);
}),
KAKAO("kakao", attributes -> {
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
return new UserProfile(
(String) kakaoAccount.get("id"),
(String) profile.get("nickname"),
(String) kakaoAccount.get("email"),
(String) profile.get("profile_image_url")
);
});
private final String registrationId;
private final Function<Map<String, Object>, UserProfile> userProfileFactory;
OAuthAttributes(String registrationId,
Function<Map<String, Object>, UserProfile> userProfileFactory) {
this.registrationId = registrationId;
this.userProfileFactory = userProfileFactory;
}
public static UserProfile extract(String registrationId, Map<String, Object> attributes) {
return Arrays.stream(values())
.filter(provider -> registrationId.equals(provider.registrationId))
.findFirst()
.orElseThrow(IllegalArgumentException::new)
.userProfileFactory.apply(attributes);
}
}
extract 메서드를 사용하여 OAuth 서비스의 이름과 얻어온 유저 정보를 통해 UserProfile 객체를 생성한다.
2️⃣ build.gradle 의존성 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // 추가
}
3️⃣ UserProfile 생성
@Getter
public class UserProfile {
private final String oauthId;
private final String userId;
private final String nickname;
private final String profileImageUrl;
public UserProfile(String oauthId, String nickname, String userId, String profileImageUrl) {
this.oauthId = oauthId;
this.nickname = nickname;
this.userId = userId;
this.profileImageUrl = profileImageUrl;
}
public User toUser() {
return new User(oauthId, nickname, userId, profileImageUrl, Role.ASSOCIATE);
}
}
나는 OAuth에서 자체 ID, userID, nickname, image url을 가져오기 때문에 UserProfile을 위와 같이 만들어주었다.
4️⃣ OAuth2UserService 구현
아래 코드는 OAuth2UserService<OAuth2UserRequest, OAuth2User> 인터페이스를 구현한 커스텀 OAuth2 사용자 서비스이다.
해당 클래스는 OAuth2 공급자(Google, Naver 등)에서 가져온 사용자 정보를 처리해 UserProfile 객체를 생성한 후, 사용자를 저장 혹은 업데이트한 후 최종적으로 DefaultOAuth2User 객체를 반환해 인증 및 권한 정보를 제공한다.
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// OAuth 서비스(github, google, naver)에서 가져온 유저 정보를 담고있음
OAuth2User oAuth2User = super.loadUser(userRequest);
// OAuth 서비스 이름(ex. github, naver, google)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// OAuth 로그인 시 키 값. 구글, 네이버, 카카오 등 각 다르기 때문에 변수로 받아서 넣음
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
// OAuth2 서비스의 유저 정보들
Map<String, Object> attributes = oAuth2User.getAttributes();
// registrationId에 따라 유저 정보를 통해 공통된 UserProfile 객체로 만들어 줌
UserProfile userProfile = OAuthAttributes.extract(registrationId, attributes);
User user = saveOrUpdateUserProfile(userProfile);
return createDefaultOAuth2User(user, attributes, userNameAttributeName);
}
private User saveOrUpdateUserProfile(UserProfile userProfile) {
// OAuth에서의 유저 정보 변경이 있을 수 있기 때문에 DB에 update
User user = userRepository.findByOauthId(userProfile.getOauthId());
if (user != null) {
user = user.update(userProfile.getUserId(), userProfile.getNickname(),
userProfile.getProfileImageUrl());
} else {
user = userRepository.save(userProfile.toUser());
}
return user;
}
private DefaultOAuth2User createDefaultOAuth2User(User user, Map<String, Object> attributes,
String userNameAttributeName) {
return new DefaultOAuth2User(
Collections.singletonList(new SimpleGrantedAuthority(user.getRole().getKey())),
attributes,
userNameAttributeName
);
}
}
- loadUser
OAuth2 사용자 정보를 가져오는 메서드로, userRequest를 기반으로 OAuth2 공급자에서 사용자 정보를 가져온다. registrationId를 추출하여 공급자 이름을 확인하고, 사용자 정보를 attributes 맵으로 받는다. - saveOrUpdateUserProfile
UserProfile 객체를 기반으로 사용자를 검색하여 존재하는 경우 업데이트하고, 그렇지 않은 경우 새로운 사용자로 저장한다. - createDefaultOAuth2User
User 객체와 attributes, userNameAttributeName을 사용하여 DefaultOAuth2User를 생성한다
User 객체의 역할을 기반으로 권한을 설정한다.
https://iseunghan.tistory.com/300
https://velog.io/@max9106/OAuth2#%EC%82%AC%EC%A0%84-%EC%9E%91%EC%97%85
https://velog.io/@yoon_s_whan/Springboot-Oauth2-jwt-Kakao-Android
'Back-end' 카테고리의 다른 글
[CI/CD] GitHub Actions를 이용한 빌드 및 배포 자동화 (0) | 2023.08.10 |
---|---|
[Java] 가비지 컬렉션(Garbage Collection: GC) (0) | 2023.06.04 |
[Spring Boot] yml 파일에 작성한 정보로 어떻게 OAuth 설정을 할까? (0) | 2023.05.22 |
[Java] HashTable VS HashMap(Linked List / Red-Black Tree) (0) | 2023.05.04 |
[Spring Boot] @Transactional(rollbackFor=Exception.class) (0) | 2023.03.21 |