AuthenticationProvider 를 구현해서 로그인을 인증하고 있다면 실패한다.
유저정보를 호출할때 에러가 발생하는데, 결론만 이야기하면 AuthorizationServerEndpointsConfigurer에 UserDetailsService를 삽입해야 refresh_token으로 갱신할때 유저정보를 꺼내올 수 있다.
이전글인 [springboot, oauth] Authorization Server(인증서버) 구축하기 에서는 AuthenticationProvider로 구현했었는데, 그러다보니 refresh_token을 호출할 때 다음의 에러가 발생했다.
{
"error": "server_error",
"error_description": "Internal Server Error"
}
관련글:
에러를 역추적해서 올라가보니 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 값을 함께 전달)
끝.
'공부 > 프로그래밍' 카테고리의 다른 글
[eslint, react] eslint-disable-next-line 에러 표기 뜰 때 (0) | 2020.11.22 |
---|---|
[webpack] Cannot read property 'tap' of undefined 에러 (0) | 2020.11.22 |
[aws] CodeDeploy(CI/CD) 자동 배포에 실패할때 복구방법 정리 (0) | 2020.11.18 |
[springboot, slf4j] logging 파일분리 application.properties 에 설정하기(RollingFileAppender) (0) | 2020.11.16 |
[springboot, security] Authorization Server 실무에 써먹게 설정 (0) | 2020.11.15 |
댓글