본문 바로가기
공부/프로그래밍

[springboot] refresh_token 호출 시 에러날 때(Authorization Server)

by demonic_ 2020. 11. 20.
반응형

AuthenticationProvider 를 구현해서 로그인을 인증하고 있다면 실패한다.

유저정보를 호출할때 에러가 발생하는데, 결론만 이야기하면 AuthorizationServerEndpointsConfigurer에 UserDetailsService를 삽입해야 refresh_token으로 갱신할때 유저정보를 꺼내올 수 있다.

 

이전글인 [springboot, oauth] Authorization Server(인증서버) 구축하기 에서는 AuthenticationProvider로 구현했었는데, 그러다보니 refresh_token을 호출할 때 다음의 에러가 발생했다.

{
    "error": "server_error",
    "error_description": "Internal Server Error"
}

관련글:

lemontia.tistory.com/927

 

[springboot, oauth] Authorization Server(인증서버) 구축하기

OAuth2 역할은 크게 4가지로 분류된다 - Resource Owner - Authorization Server - Resource Server - Client :: Resource Owner는 유저를 뜻한다. 내가 만든 서비스를 이용하고자 하는 고객을 의미한다 :: Author..

lemontia.tistory.com

 

에러를 역추적해서 올라가보니 PreAuthenticatedAuthenticationProvider 에서

authenticate(Authentication authentication) 메서드에서 유저정보를 가져와야 하는데, 실패한다.(UserDetailService 가 없다는 에러)

아래는 로그인데, UserDetailsService is required. 의 문구가 보인다.

.m.m.a.ExceptionHandlerExceptionResolver : Using @ExceptionHandler org.springframework.security.oauth2.provider.endpoint.TokenEndpoint#handleException(Exception)
o.s.s.o.provider.endpoint.TokenEndpoint  : Handling error: IllegalStateException, UserDetailsService is required.

 

문제가 되는 코드를 살펴보면 다음 메서드를 호출하는데

UserDetails ud = preAuthenticatedUserDetailsService
				.loadUserDetails((PreAuthenticatedAuthenticationToken) authentication);

 

이때 유저정보를 호출할 수 없어 발생한 에러다. 메서드를 따라가보면 아래 인터페이스가 나온다.

package org.springframework.security.core.userdetails;

import org.springframework.security.core.Authentication;

/**
 * Interface that allows for retrieving a UserDetails object based on an
 * <tt>Authentication</tt> object.
 *
 * @author Ruud Senden
 * @since 2.0
 */
public interface AuthenticationUserDetailsService<T extends Authentication> {

	/**
	 *
	 * @param token The pre-authenticated authentication token
	 * @return UserDetails for the given authentication token, never null.
	 * @throws UsernameNotFoundException if no user details can be found for the given
	 * authentication token
	 */
	UserDetails loadUserDetails(T token) throws UsernameNotFoundException;
}

 

loadUserDetails 를 호출하는 것을 살펴보면 아래 2가지가 있다. 여기에 온게 preAuthenticatedUserDetailsService 에서의 호출이니, UserDetailsByNameServiceWrapper 로 들어가보았다.

...
    /**
	 * Get the UserDetails object from the wrapped UserDetailsService implementation
	 */
	public UserDetails loadUserDetails(T authentication) throws UsernameNotFoundException {
		return this.userDetailsService.loadUserByUsername(authentication.getName());
	}
...

 

자 그럼 어떻게 해결해야 할까? 다음의 절차를 따르면 된다.

1. UserDetails를 구현하는 클래스를 만든다

2. AuthenticationManagerBuilder 에 UserDetailsService를 등록한다.

3. AuthorizationServerEndpointsConfigurer에 UserDetailsService를 등록한다

 

사실 2번의 경우를 대신한게 AuthenticationProvider 였는데, 로그인 절차를 하나로 통일하는게 더 낫기에 같이 등록해주었다.

 

그럼 1번부터 살펴보자

CustomUserDetailsService (UserDetailsService를 구현한 클래스)

@RequiredArgsConstructor
@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserAuthRepository userAuthRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        KaUser findKaUser = userAuthRepository.getKaUserByUsername(username);
        if(findKaUser == null) {
            throw new BadCredentialsException("아이디 또는 패스워드가 틀립니다");
        }

        Collection<? extends GrantedAuthority> authorities = getAuthorities();
        findKaUser.setAuthorities(authorities);

        return findKaUser;
    }

    /**
     * 권한 추가
     */
    private Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
        grantedAuthorityList.add(new SimpleGrantedAuthority("ROLE_USER"));

        return grantedAuthorityList;
    }
}

 

 

보면 UserAuthRepository 를 주입받는데, 저기서 DB와 연결하여 해당 유저의 정보(username, password 등)를 조회한다.

 

실제로 실행되는 메서드는 오버라이드 된 loadUserByUsername인데, 파라미터를 보면 username 밖에 없다. 때문에 여기서는 비밀번호 체크 같은 것을 시도할 순 없으며 조회하여 리턴만 한다.

 

UserAuthRepository 는 아래의 코드로 되어있는데, 아직 DB연결을 하지않고 임의로 생성한 유저로 비교했다.

@Repository
@RequiredArgsConstructor
public class UserAuthRepository {
    private final PasswordEncoder passwordEncoder;

    public KaUser getKaUserByUsername(String username) {
        if(username.equals("user")){
            return new KaUser("user", passwordEncoder.encode("123"));
        }
        return null;
    }
}

KaUser 클래스는 UserDetail 인터페이스를 상속해야 한다.

@Getter
public class KaUser implements UserDetails {
    private static final long serialVersionUID = 1L;

    private String username;

    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public KaUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
        this.username = username;
        this.password = password;
        this.authorities = authorities;
    }

    public KaUser(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

아래 설정은 하지 않아도 된다.

그럼 이제 AuthenticationManagerBuilder에 CustomUserDetailsService를 등록한다.

# 아래 설정은 하지 않아도 된다. (틀린 설정)
@RequiredArgsConstructor
@EnableWebSecurity
public class AuthorizatinoSecurityConfig extends WebSecurityConfigurerAdapter {
    private final CustomUserDetailsService customUserDetailsService;
    private final PasswordEncoder passwordEncoder;

...
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder);
    }
...
}

 

마지막으로 AuthorizationServerEndpointsConfigurer에 CustomUserDetailsService를 등록한다.

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    private final PasswordEncoder passwordEncoder;

    private final CustomUserDetailsService customUserDetailsService;

    private AuthenticationManager authenticationManager;

    private DataSource dataSource;

    public AuthorizationServerConfig(AuthenticationConfiguration authenticationConfiguration
            , DataSource dataSource, PasswordEncoder passwordEncoder, CustomUserDetailsService customUserDetailsService) throws Exception {
        this.authenticationManager = authenticationConfiguration.getAuthenticationManager();
        this.dataSource = dataSource;
        this.passwordEncoder = passwordEncoder;
        this.customUserDetailsService = customUserDetailsService;
    }

...
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
        endpoints.tokenStore(jdbcTokenStore());
        endpoints.approvalStore(approvalStore());
        endpoints.userDetailsService(customUserDetailsService);
    }
    

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // grantTypes에 refresh_token이 포함되어 있어야한다
        clients.inMemory()
                .withClient(oauth2ClientId)
                .secret(passwordEncoder.encode(oauth2Secret))
                .autoApprove(true)
                .redirectUris("/oauth2/callback")
                .scopes("read", "write")
                .authorizedGrantTypes("password", "refresh_token")
        ;
    }
...
}

 

 

이제 accessToken을 호출하고 받은 값으로 refresh_token을 호출해보자.

 

accessToken 호출(headers 에는 Authorization에 clientId와 secret 값을 함께 전달)

 

refresh_token 으로 재호출(headers 에는 Authorization에 clientId와 secret 값을 함께 전달)

 

 

끝.

반응형

댓글