작성
·
407
1
안녕하세요.
remember-me 기능을 처리하는 과정에서 문제가 발생하여 질문 남깁니다.
먼저 강의를 따라 구현한 AuthenticationProvider 구현체와 rememberMe 관련 설정입니다.
remeber-me input을 체크하고 로그인을 시도하였습니다.
인증을 마치고 RemeberMeService 를 거쳐 TokenBasedRememberMeServices 에서 토큰을 만드는 과정에서 username 과 password 를 조회하는데
인증 객체가 UserPasswordAuthenticationToken 인스턴스이기 때문에 아래 조건문에 따라 toString() 을 반환합니다.
결과적으로 아래와 같이 다른 username 을 반환받았습니다.
그리고 비밀번호를 조회하여 null 을 반환받고
그로 인해 password 를 찾기위해 아래 조건식에 따라 loadUserByUsername 을 통해 user 를 조회하게 됩니다.
이 과정에서 UsernameNotFoundException 예외가 발생합니다.
단순히 toString() 을 username 을 반환하도록 구현하여 해결했습니다만
잘못 구현된 부분이나 잘못 이해한 부분 혹은 다른 해결방법이 있는지에 대해 질문 드립니다.
--추가--
아래와 같이 수정하는 방법으로도 해결됨을 확인하였습니다.
토큰에 Account, null 을 준 방식과 어떠한 차이점이 있는지 알고 싶습니다.
답변 2
2
네
토큰에 들어가는 유저객체는 타입으로 생성된 객체를 저장합니다.
Account 는 JPA 엔터티로서 UserDetails 타입으로 구현되기에는 적절치 않습니다.
그래서 일반 POJO 객체로 AccountContext 를 생성하고 여기에 Account 저장하는 식으로 구현한거라 보시면 됩니다.
다만 AccountContext 를 저장하느냐 아니면 Account 를 저장하느냐 문제는 특별한 이유가 있는 것은 아닙니다.
나중에 토큰에서 유저 객체를 참조할 때 더 효율적인 방식대로 저장하면 됩니다.
그리고 credential 에 null 을 준것은 보안상 그렇게 처리한 거라 보는데 이것 또한 토큰에서 패스워드를 반드시 참조해야 할 상황이 생긴다면 저장하는 것이 맞겠지만 보통 Account 에도 패스워드가 저장되어 있기 때문에 토큰에는 null 을 주어도 크게 문제 없을 것 같습니다.
0
저는 TokenBasedRememberMeServices 를 커스터마이징하여 아래와 같이 구현하였습니다.
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http..authorizeRequests()
....
.and()
.rememberMe()
.rememberMeServices(tokenBasedRememberMeServices());
}
@Bean
public CustomTokenBasedRememberMeServices tokenBasedRememberMeServices() {
CustomTokenBasedRememberMeServices rememberMeServices = new CustomTokenBasedRememberMeServices("rememberMeKey", customUserDetailsService);
rememberMeServices.setParameter("rememberMe");
rememberMeServices.setCookieName("REMEMBER_ME");
rememberMeServices.setTokenValiditySeconds(36000);
return rememberMeServices;
}
...
CustomTokenBasedRememberMeService.java
public class CustomTokenBasedRememberMeServices extends AbstractRememberMeServices {
public CustomTokenBasedRememberMeServices(String key, UserDetailsService userDetailsService) {
super(key, userDetailsService);
}
...
// 커스터마이징
@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication successfulAuthentication) {
AccountDto accountDto = (AccountDto)successfulAuthentication.getPrincipal();
String username = retrieveUserName(accountDto);
String password = retrievePassword(accountDto);
// If unable to find a username and password, just abort as
// TokenBasedRememberMeServices is
// unable to construct a valid token in this case.
if (!StringUtils.hasLength(username)) {
this.logger.debug("Unable to retrieve username");
return;
}
if (!StringUtils.hasLength(password)) {
UserDetails user = getUserDetailsService().loadUserByUsername(username);
password = user.getPassword();
if (!StringUtils.hasLength(password)) {
this.logger.debug("Unable to obtain password for user: " + username);
return;
}
}
int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
long expiryTime = System.currentTimeMillis();
// SEC-949
expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
String signatureValue = makeTokenSignature(expiryTime, username, password);
setCookie(new String[] { username, Long.toString(expiryTime), signatureValue }, tokenLifetime, request,
response);
if (this.logger.isDebugEnabled()) {
this.logger.debug(
"Added remember-me cookie for user '" + username + "', expiry: '" + new Date(expiryTime) + "'");
}
}
...
// 커스터마이징
protected String retrieveUserName(AccountDto accountDto) {
if (isInstanceOfUserDetails(accountDto)) {
return accountDto.getUserId();
}
return accountDto.getUserId().toString();
}
// 커스터마이징
protected String retrievePassword(AccountDto accountDto) {
if (isInstanceOfUserDetails(accountDto)) {
return accountDto.getUserPw();
}
if (accountDto.getUserPw() != null) {
return accountDto.getUserPw().toString();
}
return null;
}
// 커스터마이징
private boolean isInstanceOfUserDetails(AccountDto accountDto) {
return accountDto instanceof AccountDto;
}
...
}
FormAuthenticationProvider.java
public class FormAuthenticationProvider implements AuthenticationProvider {
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 사용자 입력 로그인 정보
String userId = authentication.getName();
String userPw = (String) authentication.getCredentials();
// DB에 저장된 로그인 정보
AccountContext accountContext = (AccountContext) customUserDetailsService.loadUserByUsername(userId);
// 패스워드 검증
if (!passwordEncoder.matches(userPw, accountContext.getAccountDto().getUserPw())) {
throw new BadCredentialsException("BadCredentialsException");
}
// 추가 검증
FormWebAuthenticationDetails formWebAuthenticationDetails = (FormWebAuthenticationDetails) authentication.getDetails();
String secretKey = formWebAuthenticationDetails.getSecretKey();
if (secretKey == null || !"secret".equals(secretKey)) {
throw new InsufficientAuthenticationException("Invalid SecretKey");
}
// 인증에 성공한 인증객체 리턴
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(accountContext.getAccountDto(), accountContext.getPassword(), accountContext.getAuthorities());
return authenticationToken;
}
@Override
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
}