OAuth
는 인증 및 권한 부여 프로토콜로서, 웹, 모바일 어플리케이션에서 다른 어플리케이션의 API를 안전하게 사용하기 위한 방법을 제공한다. OAuth는 기본적으로 사용자가 자신의 인증 정보를 다른 어플리케이션에 직접 제공하는 것이 아닌, 인증 서버를 통해 인증을 처리한다. 이 때, 사용자는 인증 서버에 로그인하여 인증을 완료하고, 인증 서버는 어플리케이션에게 인증된 사용자 정보를 제공한다.
OAuth 1.0에서 복잡하게 얽혀있던 인증과 권한 처리에 대해 인증은 인증 서버에서, 권한은 자원 서버에서 처리하도록 설계됨. OAuth 2.0 에서는 Access Token의 유효 기간을 설정하여 보안성을 높였으며, Refresh Token을 제공하여 Access Token의 갱신을 용이하게 한다.
OAuth2.0을 사용하면 로그인 및 회원가입과 관련된 보안, 비밀번호 찾기, 회원인증 등의 보안과 관련된 기능은 Resource Server(네이버, 구글 등)에 맡기고 다른 서비스적인 기능에 집중할 수 있다.
OAuth2UserService
에서 loadUser
메서드 실행loadUser
메서드에서는 받은 사용자 정보를 통해 기존 회원을 매핑해주거나 해당 정보로 새로운 회원을 만들어줄 수 있다.User
객체 반환User
객체 + 권한 정보를 담은 Authentication
객체가 SecurityContext
에 저장된다.SecurityContext
가 필요하며, 이는 인증 정보를 스레드 로컬에 저장하는 방식으로 동작한다.UserOAuth2UserService
OAuth
를 통해 로그인을 시도한 사용자의 정보를 가공하여 User
객체를 만들어 반환한다. KakaoOAuth2UserInfo
Kakao OAuth2
를 통해 받은 json
형식의 데이터를 내가 원하는 형식으로 반환한다.CustomOAuth2User
UserOAuth2UserService
에서 반환할 User
객체를 커스터마이징
#oauth2 client for kakao
spring.security.oauth2.client.registration.kakao.client-id= Kakao Developer로 부터 받은 고유 Id
spring.security.oauth2.client.registration.kakao.scope= account_email, gender
spring.security.oauth2.client.registration.kakao.client-name= Kakao
spring.security.oauth2.client.registration.kakao.authorization-grant-type= authorization_code
spring.security.oauth2.client.registration.kakao.redirect-uri=http://localhost:8080/login/oauth2/code/kakao
spring.security.oauth2.client.registration.kakao.client-authentication-method= POST
#oauth2 provider for kakao
spring.security.oauth2.client.provider.kakao.authorization-uri= https://kauth.kakao.com/oauth/authorize
spring.security.oauth2.client.provider.kakao.token-uri= https://kauth.kakao.com/oauth/token
spring.security.oauth2.client.provider.kakao.user-info-uri= https://kapi.kakao.com/v2/user/me
spring.security.oauth2.client.provider.kakao.user-name-attribute= id
kakao_account
라는 map
형식의 필드안에 있는 구조를 파악할 수 있다.{
"id":123456789,
"connected_at": "2023-04-12T00:00:28Z",
"kakao_account": {
//프로필
"profile_nickname_needs_agreement": false,
"profile_image_needs_agreement ": false,
"profile": {
"nickname": "홍길동",
"thumbnail_image_url": "http://yyy.kakao.com/.../img_110x110.jpg",
"profile_image_url": "http://yyy.kakao.com/dn/.../img_640x640.jpg",
"is_default_image":false
},
//이름
"name_needs_agreement":false,
"name":"홍길동",
//이메일
"email_needs_agreement":false,
"is_email_valid": true,
"is_email_verified": true,
"email": "sample@sample.com",
//나이
"age_range_needs_agreement":false,
"age_range":"20~29",
//생일
"birthyear_needs_agreement": false,
"birthyear": "2002",
"birthday_needs_agreement":false,
"birthday":"1130",
"birthday_type":"SOLAR",
//성별
"gender_needs_agreement":false,
"gender":"female",
//휴대번호
"phone_number_needs_agreement": false,
"phone_number": "+82 010-1234-5678",
"ci_needs_agreement": false,
"ci": "${CI}",
"ci_authenticated_at": "2019-03-11T11:25:22Z",
},
"properties":{
"${CUSTOM_PROPERTY_KEY}": "${CUSTOM_PROPERTY_VALUE}",
...
}
}
attributes
(위의 OAuth2 데이터 중 kakao_account
부분) 정보를 받아 필드 값에 주입한다.getEmail
을 통해 kakao_account
의 이메일 정보를 반환받는다.public class KakaoOAuth2UserInfo {
public KakaoOAuth2UserInfo(Map<String, Object> attributes) {
this.attributes = attributes;
}
private Map<String, Object> attributes;
public String getEmail() {
return (String) attributes.get("email");
}
}
UserOAuth2UserService
에서 반환할 User
클래스를 커스터마이즈한 클래스@Getter
public class CustomOAuth2User extends User implements OAuth2User {
public CustomOAuth2User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
@Override
public Map<String, Object> getAttributes() {
return null;
}
@Override
public String getName() {
return null;
}
}
providerTypeCode
와 attribute
의 id
를 조합해 만든 username
을 통해 기존에 존재하는 회원인지 찾고, 존재하면 기존 객체 반환, 존재하지 않다면 create
한다.@Service
@RequiredArgsConstructor
@Transactional
public class UserOAuth2UserService extends DefaultOAuth2UserService {
private final UserService userService;
private final UserRepository userRepository;
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);
KakaoOAuth2UserInfo kakaoOAuth2UserInfo = new KakaoOAuth2UserInfo((Map<String, Object>) oAuth2User.getAttribute("kakao_account"));
String name = oAuth2User.getName();
String providerTypeCode = userRequest.getClientRegistration().getRegistrationId().toUpperCase();
String username = providerTypeCode + name;
SiteUser siteUser;
Optional<SiteUser> optionalSiteUser = userRepository.findByUsername(username);
if (optionalSiteUser.isEmpty()) {
siteUser = userService.create(username, "", kakaoOAuth2UserInfo.getEmail());
}
else {
siteUser = optionalSiteUser.get();
}
String role = siteUser.getUserRole().getValue();
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role);
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(grantedAuthority);
return new CustomOAuth2User(siteUser.getUsername(), siteUser.getPassword(), authorities);
}
}
userRequest
에는 위의 3개의 파라미터 정보를 가지고 있다.
clientRegistration
: client id, secret 등의 정보 저장accessToken
: Authorization Server로 부터 받은 accessToken 저장additionalParameters
: 추가 요청한 사용자의 정보가 저장super.loadUser(userRequest)
이러한 정보를 담고있는 userRequest
를 DefaultOAuth2UserService
의 loadUser
메서드를 통해 OAuth2User
객체로 반환한다.
GrantedAuthority
인터페이스를 구현한 클래스이며, 인증된 사용자의 권한을 나타내는 클래스이다.
생성자 매개변수로 String
타입의 role
(ex : USER)을 넣어주어 생성하면 권한 정보를 나타내게 된다.
또한, 스프링 시큐리티에서는 인증된 사용자의 권한 정보를 Collection<? extends GrantedAuthority>
형태로 저장하기 때문에, 생성한 SimpleGrantedAuthority
를 따로 만든 컬렉션에 저장해 인증된 사용자에게 하나 이상의 권한을 부여할 수 있다.
전체적인 흐름을 먼저 이해하려 노력했고 간단하게 구현을 해봤다.
하지만, 여기서 더 나아가 구체적인 동작 과정과 흐름에 대해 이해하려면 더 많은 시간과 노력을 들여 공부를 해야겠다는 생각이 자동으로 들어버렸다...