SocalLogin 연동 구현
public interface ProviderUser {
public String getId();
public String getUsername();
public String getPassword();
public String getEmail();
public String getProvider();
public List<? extends GrantedAuthority> getAuthorities();
public Map<String, Object> getAttributes();
}
public abstract class OAuth2ProviderUser implements ProviderUser {
private Map<String, Object> attributes;
private OAuth2User oAuth2User;
private ClientRegistration clientRegistration;
public OAuth2ProviderUser(Map<String, Object> attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration){
this.attributes = attributes;
this.oAuth2User = oAuth2User;
this.clientRegistration = clientRegistration;
}
@Override
public String getPassword() {
return UUID.randomUUID().toString();
}
@Override
public String getEmail() {
return (String)attributes.get("email");
}
@Override
public String getProvider() {
return clientRegistration.getRegistrationId();
}
@Override
public List<? extends GrantedAuthority> getAuthorities() {
return oAuth2User.getAuthorities().stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
}
}
각 provider마다 userinfo endpoint에서 들고오는 OAuth2User 정보가 다르기 때문에 공통적인 부분을 정의하고 세세한것으로 나뉘도록 한다.
public class NaverUser extends OAuth2ProviderUser {
public NaverUser(OAuth2User oAuth2User, ClientRegistration clientRegistration){
super((Map<String, Object>)oAuth2User.getAttributes().get("response"), oAuth2User, clientRegistration);
}
@Override
public String getId() {
return (String)getAttributes().get("id");
}
@Override
public String getUsername() {
return (String)getAttributes().get("email");
}
}
여기에 나와있듯이 id 나 email 이 response/id 이런식으로 되있는것을 알 수 있다. response를 key로 값을 가져온다음에 원하는 값을 추가로 가져와야한다.
@Bean
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests((requests) -> requests
.antMatchers("/api/user")
.access("hasAnyRole('SCOPE_profile','SCOPE_email')")
// .access("hasAuthority('SCOPE_profile')")
.antMatchers("/api/oidc")
.access("hasRole('SCOPE_openid')")
//.access("hasAuthority('SCOPE_openid')")
.antMatchers("/")
.permitAll()
.anyRequest().authenticated());
http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(
userInfoEndpointConfig -> userInfoEndpointConfig
.userService(customOAuth2UserService)
.oidcUserService(customOidcUserService)));
http.logout().logoutSuccessUrl("/");
return http.build();
}
기본적으로 제공되는것 말고 customOAuth2UserService를 등록을 해줘야한다. open id service도 마찬가지이다.
@Service
public class CustomOAuth2UserService extends AbstractOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
ClientRegistration clientRegistration = userRequest.getClientRegistration();
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
ProviderUser providerUser = super.providerUser(clientRegistration,oAuth2User);
// 회원가입하기
super.register(providerUser, userRequest);
return oAuth2User;
}
}
oAuth2UserService#loadUser를 통해서 user endpoint를 접속하여 OAuth2User를 가져온후에 각 provider에 맞게 super.providerUser를 통해 다시 상세한 providerUser로 변경한다.
이후 register를 통해 db에 등록하게 된다.
@Service
@Getter
public abstract class AbstractOAuth2UserService {
@Autowired
private UserService userService;
@Autowired
private UserRepository userRepository;
public void register(ProviderUser providerUser, OAuth2UserRequest userRequest){
User user = userRepository.findByUsername(providerUser.getUsername());
if(user == null){
ClientRegistration clientRegistration = userRequest.getClientRegistration();
userService.register(clientRegistration.getRegistrationId(),providerUser);
}else{
System.out.println("userRequest = " + userRequest);
}
}
public ProviderUser providerUser(ClientRegistration clientRegistration, OAuth2User oAuth2User){
String registrationId = clientRegistration.getRegistrationId();
if(registrationId.equals("keycloak")){
return new KeycloakUser(oAuth2User,clientRegistration);
}else if(registrationId.equals("google")){
return new GoogleUser(oAuth2User,clientRegistration);
}
else if(registrationId.equals("naver")){
return new NaverUser(oAuth2User,clientRegistration);
}
return null;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void register(String registrationId, ProviderUser providerUser) {
User user = User.builder().registrationId(registrationId)
.id(providerUser.getId())
.username(providerUser.getUsername())
.password(providerUser.getPassword())
.authorities(providerUser.getAuthorities())
.provider(providerUser.getProvider())
.email(providerUser.getEmail())
.build();
userRepository.register(user);
}
}
repo에 저장하게 되는데 실제 db가 아닌 Map에 저장한다. User는 Entity역할을 한다.
google의 경우에는 open id를 지원함으로 application.yaml 에서 scope에 openid를 설정을 해놓으면 OidcAuthorizationCodeAuthenticationProvider 에서 인증처리를 하게 된다.
@GetMapping("/")
public String index(Model model, Authentication authentication, @AuthenticationPrincipal OAuth2User oAuth2User){
OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken)authentication;
if(authenticationToken != null){
Map<String, Object> attributes = oAuth2User.getAttributes();
String userName = (String)attributes.get("name");
if(authenticationToken.getAuthorizedClientRegistrationId().equals("naver")){
Map<String, Object> response = (Map)attributes.get("response");
userName = (String)response.get("name");
}
model.addAttribute("user", userName);
}
return "index";
}
네이버의 경우에 response로 한번 꺼낸후에 name, email, id 등에 접근이 가능하다.
또한 open id를 지원하지 않기에 OAuth2AuthorizationCodeAuthenticationProvider에서 해결한다.
Form 인증 + OAuth2.0 Soical Login
@Component
public final class DelegatingProviderUserConverter implements ProviderUserConverter<ProviderUserRequest,ProviderUser> {
private final List<ProviderUserConverter<ProviderUserRequest, ProviderUser>> converters;
public DelegatingProviderUserConverter() {
List<ProviderUserConverter<ProviderUserRequest,ProviderUser>> providerUserConverters = Arrays.asList(
new UserDetailsProviderUserConverter(),
new OAuth2GoogleProviderUserConverter(),
new OAuth2NaverProviderUserConverter(),
new OAuth2KakaoProviderUserConverter(),
new OAuth2KakaoOidcProviderUserConverter());
this.converters = Collections.unmodifiableList(new LinkedList<>(providerUserConverters));
}
@Nullable
@Override
public ProviderUser convert(ProviderUserRequest providerUserRequest) {
Assert.notNull(providerUserRequest, "providerUserRequest cannot be null");
for (ProviderUserConverter<ProviderUserRequest,ProviderUser> converter : this.converters) {
ProviderUser providerUser = converter.convert(providerUserRequest);
if (providerUser != null) {
return providerUser;
}
}
return null;
}
}
각 Provider에 맞게 User를 Converting하는 레이어를 만든것이다. converters를 돌면서 registrationid가 매칭되는 경우만 변환하고 매칭이 되지않으면 null을 return한다.
public record ProviderUserRequest (ClientRegistration clientRegistration, OAuth2User oAuth2User, User user){
public ProviderUserRequest(ClientRegistration clientRegistration, OAuth2User oAuth2User){
this(clientRegistration,oAuth2User,null);
};
public ProviderUserRequest(User user){
this(null,null,user);
};
}
인자로 ProviderUserRequest record가 주어지는데 ClientRegistration , OAuth2User , User를 필드로가지고 있는 불변클래스이다. DTO 개념으로 활용이된다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService extends AbstractOAuth2UserService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if(user == null){
user = User.builder()
.id("1")
.username("onjsdnjs")
.password("{noop}1234")
.authorities(AuthorityUtils.createAuthorityList("ROLE_USER"))
.email("onjsdnjs@gmail.com")
.build();
}
ProviderUserRequest providerUserRequest = new ProviderUserRequest(user);
ProviderUser providerUser = providerUser(providerUserRequest);
selfCertificate(providerUser);
return new PrincipalUser(providerUser);
}
}
form login은 UserDetailService#loadUserByUserName 메소드를 통해 repository에서 username으로 User를 찾아서 UserDetails으로 return 해야한다.
하지만 providerUser메소드에서 Provider별로 (google, naver , kakao) OAuth2User가 나오게 됨으로 UserDetail과 OAuth2User를 통일 해줘야한다.
public record PrincipalUser(ProviderUser providerUser) implements UserDetails, OidcUser, OAuth2User {
@Override
public String getName() {
return providerUser.getUsername();
}
@Override
public Map<String, Object> getAttributes() {
return providerUser.getAttributes();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return providerUser.getAuthorities();
}
@Override
public String getPassword() {
return providerUser.getPassword();
}
@Override
public String getUsername() {
return providerUser.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public Map<String, Object> getClaims() {
return null;
}
@Override
public OidcUserInfo getUserInfo() {
return null;
}
@Override
public OidcIdToken getIdToken() {
return null;
}
}
principleUser는 record 로 dto역할을 하게 되고 OAuth2User, UserDetails , OidcUser 3개의 타입을 동시에 처리할 수 있게된다.
'WEB > Security' 카테고리의 다른 글
OAuth 2.0 Resource Server API - jwt() (0) | 2022.12.22 |
---|---|
OAuth 2.0 Resource Server (0) | 2022.12.19 |
OAuth 2.0 Client - oauth2Client() (1) | 2022.12.14 |
OAuth2.0 Client - oauth2Login() + API 커스텀 구현 (0) | 2022.11.23 |
OAuth 2.0 Client - oauth2Login() (1) | 2022.11.22 |