인프런 커뮤니티 질문&답변

buo642g님의 프로필 이미지
buo642g

작성한 질문수

스프링 시큐리티

Remember Me

작성

·

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 를 커스터마이징하여 아래와 같이 구현하였습니다.

SecurityConfig.java

...
@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);
  }


}
buo642g님의 프로필 이미지
buo642g

작성한 질문수

질문하기