inflearn logo
강의

강의

N
챌린지

챌린지

멘토링

멘토링

N
클립

클립

로드맵

로드맵

지식공유

스프링 시큐리티

Remember Me

508

감채

작성한 질문수 18

1

안녕하세요.
remember-me 기능을 처리하는 과정에서 문제가 발생하여 질문 남깁니다.

 

먼저 강의를 따라 구현한 AuthenticationProvider 구현체와 rememberMe 관련 설정입니다.

 

remeber-me input을 체크하고 로그인을 시도하였습니다.


인증을 마치고 RemeberMeService 를 거쳐 TokenBasedRememberMeServices 에서 토큰을 만드는 과정에서 username 과 password 를 조회하는데

 

인증 객체가 UserPasswordAuthenticationToken 인스턴스이기 때문에 아래 조건문에 따라 toString() 을 반환합니다.

결과적으로 아래와 같이 다른 username 을 반환받았습니다.

 

그리고 비밀번호를 조회하여 null 을 반환받고
그로 인해 password 를 찾기위해 아래 조건식에 따라 loadUserByUsername 을 통해 user 를 조회하게 됩니다.

 

이 과정에서 UsernameNotFoundException 예외가 발생합니다.

단순히 toString() 을 username 을 반환하도록 구현하여 해결했습니다만
잘못 구현된 부분이나 잘못 이해한 부분 혹은 다른 해결방법이 있는지에 대해 질문 드립니다.

 

--추가--

아래와 같이 수정하는 방법으로도 해결됨을 확인하였습니다.

토큰에 Account, null 을 준 방식과 어떠한 차이점이 있는지 알고 싶습니다.

 

Spring Security spring-boot java

답변 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);
  }


}

시큐리티 공부 버전 질문

0

177

1

[해결 방법] MethodSecurityConfig.customMethodSecurityMetadataSource() 호출하지 않는 이슈

0

187

1

AbstractSecurityInterceptor.class.beforeInvocation()를 2번 실행하는 경우

0

179

1

강의 코드가 왜이렇게 뒤죽박죽인가요...

0

254

1

메인 페이지로 접속해도 login url로 리다이렉트가 되지 않습니다..

0

238

1

파라미터값이 넘어가지 않습니다 ....

0

376

1

security filterChain 설정 질문이 있습니다.

0

332

1

소스 부분 질문 드립니다.

0

210

2

섹션4 7번 강의 문제가 있는거 같네요.

0

345

2

파일이 수시로 이름이 바껴있네요 ㄷㄷ

0

306

1

HttpSessionSecurityContextRepository를 사용안하는 문제

0

557

2

error , exception 이 잘 안됩니다.

0

284

2

thymeleaf tag 질문합니다.

0

198

2

버전업하면서 deprecated된 것들이 너무많아요

0

478

1

spring security 패치 관련

0

438

1

모바일을 사용할때 토큰말고 세션

0

852

2

DB 연동한 인가 부분에 대한 질문입니다!

0

265

1

Ajax방식도 똑같이 Session방식을 사용하는건가요?

0

308

1

Config 파일 생성 시 질문이 있습니다.

0

228

1

강사님 몇일동안 구글 검색만 100개 했는데도 이유를 모르겠습니다..

1

433

2

403 에러 뜹니다.

0

813

2

login_proc의 존재에 대한 간략한 설명입니다

0

277

1

top.html에 로그인 링크를 만들어서 로그인을 해봤습니다

0

288

2

안녕하세요. DB에 저장될 때 이해 안 가는 값이 있어서 질문드립니다!

0

191

1