아카이브

[스프링 기반 REST API 개발] Account에 스프링 시큐리티 적용하기 본문

Spring/스프링 기반 REST API 개발

[스프링 기반 REST API 개발] Account에 스프링 시큐리티 적용하기

주멘이 2021. 1. 9. 12:22

스프링 시큐리티

  • 웹 시큐리티 (Filter 기반 시큐리티): 웹 요청에 보안인증을 함
  • 메서드 시큐리티: 웹과 상관없이 어떠한 메서드가 호출됐을 때 인증 또는 권한을 확인해줌
  • 이 둘 다 Security Interceptor를 사용합니다
    • 리소스에 접근을 허용할 것이냐 말것이냐를 결정하는 로직이 들어있음
  • Filter 기반으로 동작하기 때문에 Spring MVC와 분리되어 동작한다.
  • 보안 관련 용어
    • 접근 주체(Principal) : 애플리케이션에 접근하는 유저
    • 인증(Authentication) : 접근한 유저를 식별하고, 접근할 수 있는지 검사
    • 인가(Autorize) : 인증된 유저가 애플리케이션을 이용할 수 있는지 검사

SecurityFilterChain

브라우저가 서버에 데이터를 요청하면 여러 ServletFilter를 거친 후, DispatcherServelt에 전달된다.

이때 Spring Security에서 등록했던 Filter를 이용해 보안 관련 처리를 진행한다.

Security Filter들은 연결된 여러 Filter들로 구성되어 있기에 Chain이라는 표현을 사용하고 있다.

SecurityFilterChain 구조

Spring Security 동작 흐름

동작흐름

1. AuthenticationFilter (UsernamePasswordAuthenticationFilter)는 Http Request를 가로챈다. 

  • 인증이 필요한 요청이라면 사용자의 JSESSIONIDSecurity Context에 있는지 판단하고, 없으면 로그인 페이지로 이동시킨다.
  • 로그인에서 온 요청이라면 입력받은 username, password를 이용해 UsernamePasswordAuthenticationToken을 만든다. 
    • Token이 유효한 계정인지 판단하기 위해 AuthenticationManager로 전달한다.

2. AuthenticationManager는 AuthencationProvider에게 인증 책임을 넘긴다.

  • AuthencationProvider는 개발자가 직접 커스텀해서 비밀번호 인증 로직을 직접 구현할 수 있다.

3. AuthencationProvider는 UserDetailsService를 실행해 비밀번호 인증 로직을 처리한다.

  • UserDetailsService는 DB에 저장된 정보가 일치하면 UserDetails를 구현한 Object(User)를 반환한다.
  • UserDetailsServiec는 implements 해야 한다.

4. 인증이 완료되면 AuthenticationManager는 Authentication을 반환하며, SecurityContext에 이를 저장한다.

5. AuthenticationFilter에게 인증 성공 유무를 전달한다.

  • 성공하면 AuthenticationSuccessHandler를 호출한다.
  • 실패하면 AuthenticationFailureHandler를 호출한다.

AuthenticationManager의 기본적인 인증

여러 가지 방법으로 인증을 할 수 있음

  • Basic Authentication
    1. username과 password를 입력 받음
    2. 인증 요청 헤더에서 Authenticationbasicusername + password을 모두 합쳐서 인코딩함
    3. UserDetailsService 인터페이스를 사용해서 입력받은 username에 해당하는 password를 읽어옴
    4. 읽어온 password와 사용자가 입력한 password가 매칭 하는지 password Encoder로 검사
    5. 매칭이 되면 로그인이 된 거고 Authentication 객체를 만들어서 SecurityContextHolder에 저장을 해둠

AccountRepository 추가하기

public interface AccountRepository extends JpaRepository<Account, Integer> {
    Optional<Account> findByEmail(String username);
}

AccountService 구현하기

  • UserDetails로 변환하기 위해(기본적으로 제공하는 User class)
  • ROLE은 GrantedAuthority로 변환
@Service
public class AccountService implements UserDetailsService {

    private final AccountRepository accountRepository;

    @Autowired
    public AccountService(AccountRepository accountRepository) {
        this.accountRepository = accountRepository;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByEmail(username)
                .orElseThrow(() -> new UsernameNotFoundException(username));

        return new User(account.getEmail(), account.getPassword(), authorities(account.getRoles()));
    }

    private Collection<? extends GrantedAuthority> authorities(Set<AccountRole> roles) {
        return roles.stream()
                .map(r -> new SimpleGrantedAuthority("ROLE_" + r.name()))
                .collect(Collectors.toSet());
    }
}

 

findByUsername Test 코드 작성

    @Test
    void findByUsername() {
        //Given
        String username = "jumen@naver.com";
        String password = "5215";
        Account account = Account.builder()
                .email(username)
                .password(password)
                .roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
                .build();

        this.accountRepository.save(account);

        // When
        UserDetailsService userDetailsService = accountService;
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);

        // Then
        assertThat(userDetails.getPassword()).isEqualTo(password);


    }