728x90
스프링 기반 REST API 개발
5. REST API 보안 적용
포스팅 참조 정보
GitHub
공부한 내용은 GitHub에 공부용 Organizations에 정리 하고 있습니다
해당 포스팅에 대한 내용의 GitHub 주소
실습 내용이나 자세한 소스코드는 GitHub에 있습니다
포스팅 내용은 간략하게 추린 핵심 내용만 포스팅되어 있습니다
https://github.com/freespringlecture/spring-rest-api-study/tree/chap05-02_apply_springsecurity
해당 포스팅 참고 인프런 강의
https://www.inflearn.com/course/spring_rest-api/dashboard
실습 환경
- Java Version: Java 11
- SpringBoot Version: 2.1.2.RELEASE
2. 스프링 시큐리티 적용
스프링 시큐리티
- 웹 시큐리티 (Filter 기반 시큐리티): 웹 요청에 보안인증을 함
- 메서드 시큐리티: 웹과 상관없이 어떠한 메서드가 호출 됐을 때 인증 또는 권한을 확인해줌
- 이 둘 다 Security Interceptor를 사용합니다
- 리소스에 접근을 허용할 것이냐 말것이냐를 결정하는 로직이 들어있음
스프링 시큐리티 동작흐름
- 요청이 들어왔을 때 서블릿 필터가 가로챔
- 웹 시큐리티 Interceptor 쪽으로 인증을 보냄
- 요청에 스프링 시큐리티 필터를 적용해야하는지 판단
- 스프링 시큐리티 필터에 들어오면 인증정보를 확인
- SecurityContextHolder(기본 구현체는 ThreadLocal)에서 인증 정보를 꺼내려고 시도
- 꺼내면 인증된 사용자가 이미 있는거고 없으면 인증을 한 적이 없는 것(현재 사용자가 없는 것)
- 현재 사용자가 없다면 AuthenticationManager(로그인 담당)를 사용해서 로그인을 함
- 인증이 되면 인증이 된 사용자는 SecurityContextHolder에 저장해둠
- AuthenticationManager은 두개의 서비스가 있음
UserDetailsService
: 입력받은 username에 해당하는 password 를 읽어오는 서비스PasswordEncoder
: 입력한 password가 사용자의 password와 일치하는지 검사하는 서비스
- AccessDecisionManager에서 인증이 되면 요청한 리소스에 접근할 권한이 충분한가를 User Role로 확인
ThreadLocal
- JavaSDK에 들어있는 한 Thread 내에서 공유하는 저장소
- 어떤 Application에서 데이터를 넘겨주고 싶을 때 메세지 파라메터에 넘겨주지 않아도됨
- 한 Thread에서 실행되는 메서드라면 ThreadLocal에 넣어놓고 다른 메서드에서 Thread로 꺼내서 쓰면됨
- 데이터베이스랑 비슷함
AuthenticationManager
여러가지 방법으로 인증을 할 수 있음
- Basic Authentication
- username과 password를 입력받음
- 인증 요청 헤더에서 Authentication, basic, username + password 을 모두 합쳐서 인코딩함
UserDetailsService
인터페이스를 사용해서 입력받은 username에 해당하는 password를 읽어옴- 읽어온 password와 사용자가 입력한 password가 매칭하는지 password Encoder로 검사
- 매칭이 되면 로그인이 된거고 Authentication 객체를 만들어서 SecurityContextHolder에 저장을 해둠
의존성 추가
스프링 시큐리티가 의존성에 들어오면 기본적으로 모든 요청이 인증이 필요하게됨
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
AccountRepository 추가
JpaRepository 를 상속 받고 findByEmail
를 생성한다
package me.freelife.rest.accounts;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface AccountRepository extends JpaRepository<Account, Long> {
Optional<Account> findByEmail(String username);
}
AccountService 구현
- Account를 스프링 시큐리티가 이해할 수 있는 타입인 UserDetails로 변환
- 스프링 시큐리티가 제공하는 User라는 클래스를 사용해서 구현하면 전체 인터페이스를 다 구현하지 않아도 되어서 편리함
- ROLE를 GrantedAuthority 변환해야됨
package me.freelife.rest.accounts;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class AccountService implements UserDetailsService {
@Autowired
AccountRepository accountRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Account account = accountRepository.findByEmail(username)
.orElseThrow(() -> new UsernameNotFoundException(username)); // account 객체가 없으 UsernameNotFoundException 에러를 던짐
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());
}
}
AccountServiceTest 코드 구현
package me.freelife.rest.accounts;
import me.freelife.rest.common.TestDescription;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class AccountServiceTest {
@Autowired
AccountService accountService;
@Autowired
AccountRepository accountRepository;
@Test
@TestDescription("유저 인증 테스트")
public void findByUsername() {
// Given
String password = "freelife";
String username = "freelife@gmail.com";
Account account = Account.builder()
.email(username)
.password(password)
.roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
.build();
this.accountRepository.save(account);
// When
UserDetailsService userDetailsService = (UserDetailsService) accountService;
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// Then
assertThat(userDetails.getPassword()).isEqualTo(password);
}
}
테스트 다 깨짐 (401 Unauthorized)
깨지는 이유는 스프링 부트가 제공하는 스프링 시큐리티 기본 설정 때문
728x90
'개발강의정리 > Spring' 카테고리의 다른 글
[스프링 기반 REST API 개발] 5-4. 스프링 시큐리티 기본 설정 (0) | 2020.04.04 |
---|---|
[스프링 기반 REST API 개발] 5-3. 예외 테스트 (0) | 2020.04.03 |
[스프링 기반 REST API 개발] 5-1. Account 도메인 추가 (0) | 2020.04.01 |
[스프링 기반 REST API 개발] 4-4. 테스트 코드 리팩토링 (0) | 2020.03.31 |
[스프링 기반 REST API 개발] 4-3. Events 수정 API 구현 (0) | 2020.03.30 |
댓글