Spring Security

이강희·2022년 8월 28일
0
post-thumbnail

Spring Security

용어 정리

Authentication ( 인증 ) : 제공된 자격 증명을 기반으로 사용자의 시원을 확인합니다. 웹 사이트에 접근할 때 사용되는 아이디, 비밀번호를 말합니다.

Authorization ( 인가 ) : 사용자가 성공적으로 인증되었다는 가정 하에 특정 작업을 수행하거나 특정 데이터를 읽을 수 있는 권한이 있는지 확인합니다.

Spring Security 처리 과정

  1. 사용자가 로그인 정보와 함께 인증 요청을 합니다.
  2. AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken의 인증용 객체를 생성합니다.
  3. AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken객체를 전달합니다.
  4. AuthenticationManager는 등록된 AuthenticationProvider을 조회하여 인증을 요청합니다.
  5. 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨줍니다.
  6. 넘겨받은 사용자 정보를 통해 DB에 찾은 사용자 정보인 UserDeatails 객체를 만들어 줍니다.
  7. AuthenticationProvider은 UserDetails를 넘겨받고 사용자 정보를 비교합니다.
  8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.
  9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.
  10. Authentication 객체를 SecurityContextHolder안에 SecurityContext에 저장합니다.
내용을 정리하자면 로그인 요청이 들어오면 내용으로 인증 조회 절차를 밟은 후
만약 결과가 맞으면 Authentication 객체를 SecurityContext에 저장합니다.

코드리뷰

서비스 로직 (로그인)

@Transactional(readOnly = true)
public TokenResponseDto signIn(String userId, String pw) {

	  // userId 확인
    UserDetails userDetails = myUserDetailsService.loadUserByUsername(userId);

    // pw 확인
    if (!passwordEncoder.matches(pw, userDetails.getPassword())) {
      throw new BadCredentialsException(userDetails.getUsername() + "Invalid password");
		}

    Authentication authentication = new UsernamePasswordAuthenticationToken(
      userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()
		);

   String refreshToken = jwtTokenProvider.createRefreshToken(authentication);
   RefreshRedisToken token = RefreshRedisToken.createToken(userId, refreshToken);

	 // 기존 토큰이 있으면 수정, 없으면 생성
   refreshRedisRepository.save(token);

   // accessToken과 refreshToken 리턴
   return TokenResponseDto.builder()
   accessToken("Bearer-" + jwtTokenProvider.createAccessToken(authentication))
       .accessToken("Bearer-" + refreshToken)
       .build();
}

서비스 로직을 보면 위에 있는 내용을 더 쉽게 이해할 수 있다.

UserDetails userDetails = myUserDetailsService.loadUserByUsername(userId);

첫번째 코드는 스프링에서 지원하는 UserDetailsService Interface를 통해 userId가 유효한지 확인하는 과정이다. 만약 userId가 존재한다면 UserDetails 객체에 담아둔다.

if (!passwordEncoder.matches(pw, userDetails.getPassword())) {
     throw new BadCredentialsException(userDetails.getUsername() + "Invalid password");
}

두번째 코드는 비밀번호가 유효한지 검증하는 단계이다 이 떄 회원가입 시 비밀번호가 암호화 되어있어 passwordEncoder.matches 메서드를 확인하여 비밀번호를 검증한다.

Authentication authentication = new UsernamePasswordAuthenticationToken(
     userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities()
);

세번째 코드는 검증이 마치고 Authentication 객체에 아이디, 패스워드, 인가정보를 넣어 저장한다.

String refreshToken = jwtTokenProvider.createRefreshToken(authentication);
RefreshRedisToken token = RefreshRedisToken.createToken(userId, refreshToken);

// 기존 토큰이 있으면 수정, 없으면 생성
refreshRedisRepository.save(token);

// accessToken과 refreshToken 리턴
return TokenResponseDto.builder()
        .accessToken("Bearer-" + jwtTokenProvider.createAccessToken(authentication))
        .accessToken("Bearer-" + refreshToken)
        .build();
}

네번째 코드는 JWT관련 코드이다. 검증이 완료된 상태의 정보로 토큰을 생성하고 Redis DB

저장하면 자동으로 없으면 생성하고, 기존에 있으면 수정하게 된다.

추가 내용

Refresh 토큰을 사용하는 목적은 실제 인증 목적으로 쓰이는 Access-Token을 발행하는 용도로 쓰인다. 그래서 Access-Token의 기간을 1시간 Refresh-Token의 기간을 훨씬 길게 사용한다. 이로인해서 Access-Token만 노출시켜 취약한 보안을 해결할 수 있다.

우리가 사용하는 어플들은 대부분 Access-Token을 재발행할 때 새로운 로그인을 하지 않고 연장되는 모습을 볼 수 있다. 이것을 위해서 클라이언트 서버가 접근을 Intercept하여 토큰의 기간을 연장하는 과정을 갖는다. 아래 코드는 그 과정에서 조회되는 Api이다.

Access Token 만료 시 작동하는 로직

@Transactional(readOnly = true)
public TokenResponseDto reissueAccessToken(String token) {

   // token 앞에 "Bearer-" 제거
   String resolveToken = resolveToken(token);

   // 토큰 검증 메서드
   // 실패시 jwtTokenProvider.validateToken(resolveToken)에서 exception을 리턴
	 JwtTokenProvider.validateToken(resolveToken);

   Authentication authentication = jwtTokenProvider.getAuthentication(resolveToken);
   // 디비에서 확인
   RefreshRedisToken refreshRedisToken = refreshRedisRepository.findById(authentication.getName()).get();

   // 토큰이 같은지 확인
   if (!resolveToken.equals(refreshRedisToken.getToken())) {
     throw new RuntimeException("not equals refresh token");
   }

	 // 재발행해서 저장
   String newToken = jwtTokenProvider.createRefreshToken(authentication);
   RefreshRedisToken newRedisToken = RefreshRedisToken.createToken(authentication.getName(), newToken);
   refreshRedisRepository.save(newRedisToken);

   // accessToken과 refreshToken 모두 재발행
   return TokenResponseDto.builder()
       .accessToken("Bearer-" + jwtTokenProvider.createAccessToken(authentication))
       .refreshToken("Bearer-" + newToken)
       .build();
}
profile
도전하는 개발자 이강희입니다.

0개의 댓글