[데브코스] Spring Security - 6

Updated: Categories:

W16D5 - Spring Security - JPA 연동 / Spring Session에 대해 알아보자

Security - JPA 연동

1. 엔티티 생성

  • JDBC 연동때와 동일한 테이블 사용
  • 하지만 정해진 테이블 구조가 있던 JDBC 연동과 다르게 테이블 형식은 자유로움

image

2. 권한 리스트 설정

  • Group 엔티티에서 양방향 매핑을 걸어줘서 GroupPermission 엔티티 리스트를 가지게 한다.
  • 그리고 권한 리스트를 반환하는 getAuthorities 메소드를 만들어 GroupPermission에서 권한 스트링을 실제권한 객체(GrantedAuthority)로 변환해준다
@Entity
@Table(name = "groups")
public class Group {
    ...

    @OneToMany(mappedBy = "group")
    private List<GroupPermission> permissions = new ArrayList<>();

    public List<GrantedAuthority> getAuthorities() {
        return permissions.stream()
                .map(gp -> new SimpleGrantedAuthority(gp.getPermission().getName()))
                .collect(toList());
    }

    ...
}

3. Repository 설정

  • 다음과 같이 사용자 ID를 통해 엔티티를 찾는 쿼리를 등록해주자.
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByLoginId(String loginId);
}
  • 하지만 이렇게 만들면 쿼리가 세번 실행되게 된다.
    1. 사용자 테이블 조회
    2. 사용자가 속한 그룹 테이블 조회
    3. 그룹 테이블에 부여된 권한 테이블 조회

image

  • 따라서 JPQL Fetch join으로 세 테이블을 한번에 join하여 조회한다
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("select u from User u join fetch u.group g left join fetch g.permissions gp join fetch gp.permission where u.loginId = :loginId")
    Optional<User> findByLoginId(String loginId);
}

4. Service 설정

  • 이렇게 만든 JPA 구조를 Spring Security 인증 과정에서 사용하도록 설정해줘야 한다.
  • Security에서 username(login_id)로 객체를 만드는 UserDetailsService 클래스를 상속받아 loadUserByUsername 메소드를 JPA를 사용해 재정의해준다.
    • Security에서 사용되는 User 객체(UserDetails의 구현체임)를 반환함
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByLoginId(username)
                .map(user ->
                        // 엔티티 객체가 아닌 security에서 사용하는 User 객체이다!
                        User.builder()
                                .username(user.getLoginId())
                                .password(user.getPasswd())
                                .authorities(user.getGroup().getAuthorities())
                                .build()
                )
                .orElseThrow(() -> new UsernameNotFoundException("Could not found user for " + username));
    }

}

5. WebSecurityConfig 설정

  • 이제 커스텀한 UserService 클래스를 WebSecurityConfig 클래스에서 반영해주면 된다.
  • AuthenticationManagerBuilder.userDetailsService() 메소드를 사용하여 커스텀 User 서비스를 적용하면 끝!!
private UserService userService;

@Autowired
public void setUserService(UserService userService){
    this.userService = userService;
}

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService);
}


Session

문제점

  • 지금까지의 세션은 스프링 서버 인메모리로 관리되었음
  • 이 경우 사용자가 로그인했을때 서버를 내렸다가 올리면 어떻게 될까?
    • 사용자의 로그인 세션은 브라우저의 쿠키에 저장되어 있으나 서버에서 해당 세션은 유실되게 되고 인증이 풀리게 된다.
  • 또한 서버 메모리를 잡아먹기 때문에 세션이 쌓일수록 서버 성능이 저하된다.

Session Cluster

  • 세션을 서버 인메모리로 관리하는 것이 아닌 외부 스토리지에서 관리해야 한다. (당연)
    • 외부 스토리지는 조회 속도를 위해 보통 In-Memory 데이터베이스를 많이 사용함
    • Sticky Connection(동일한 사용자가 발생시킨 요청은 동일한 WAS에서 처리됨을 보장) 제약에서 자유로움
  • 외부 스토리지가 SPOF(single point of failure) 지점이 되는것을 방지하기 위해 외부 스토리지는 보통 클러스터로 구성됨

image


Spring Session

  • 역시 Spring Boot에서 Session Cluster를 구현하기 위한 다양한 기능을 제공
  • Session을 저장하기 위한 외부 스토리지를 추상화함으로써 일관된 API로 JDBC, Redis, Hazelcast 등 다양한 스토리지를 활용할 수 있다.
  • 이 중 spring-session-jdbc는 세션 정보를 DB에 저장하는 방식

설정

  • 세션 테이블 정보는 org/springframework/session/jdbc/ 경로 하위 sql들에 정의되어 있음
  • 어플리케이션 로드시 해당 sql을 통해 세션 테이블이 정의된다.

image

  • 1. application.yaml에 다음 정보를 추가해준다.
spring: 
    #...
    session:
      store-type: jdbc
      jdbc:
        initialize-schema: never / always / embedded
  • initialize-schema 속성
    • never : sql을 실행하지 않음
    • always : sql 항상 실행
    • embedded : embedded DB인 경우에만 실행


  • 2. WebMvcConfigure@EnableJdbcHttpSession 어노테이션을 달아준다.
@EnableJdbcHttpSession
@Configuration
public class WebMvcConfigure implements WebMvcConfigurer {
    ...
}


동작 원리

image

SessionRepository

  • Session의 CRUD 기능을 제공
  • 스토리지 종류에 따라 다양한 구현체를 제공함
    • MapSessionRepository : In-Memory Map기반이며, 별도의 의존 라이브러리 필요 없음
    • RedisIndexedSessionRepository : redis 기반이며, @EnableRedisHttpSession 어노테이션으로 생성됨
    • JdbcIndexedSessionRepository : jdbc 기반이며, @EnableJdbcHttpSession 어노테이션으로 생성됨

SessionRepositoryFilter

  • HttpServletRequest, HttpServletResponse 인터페이스 구현을 SessionRepositoryRequestWrapper, SessionRepositoryResponseWrapper 구현체로 교체함
  • HttpServletRequest, HttpServletResponse 인터페이스의 Session 처리와 관련한 처리를 Override
    • Session 관련 생성 및 입출력은 SessionRepository 인터페이스를 통해 처리함
    • HttpSession 인터페이스에 대해 Spring Session 구현체 HttpSessionWrapper를 사용하도록 함
    • HttpSessionWrapper 구현체는 org.springframework.session.Session 인터페이스를 포함하고 있음
      • 스토리지 종류에 따라 org.springframework.session.Session 인터페이스 구현체가 달라짐

Spring Security와의 관계

  • Spring Session의 SessionRepositoryFilter 클래스는 Spring Security의 DelegatingFilterProxy 보다 먼저 실행됨
  • Spring Security의 SecurityContextPersistenceFilterSecurityContextRepository 인터페이스 구현체를 통해 사용자의 SecurityContext를 가져오거나 갱신함
    • SecurityContextRepository 인터페이스 기본 구현은 Session을 이용하는 HttpSessionSecurityContextRepository 클래스
    • HttpServletRequest 인터페이스의 getSession() 메소드를 통해 Session을 가져옴
      • 바로 이 지점에서 HttpServletRequest 인터페이스의 스프링 세션 구현체인 SessionRepositoryRequestWrapper 클래스가 사용됨
  • 결과 : Spring Security는 Spring Session과 독립적으로 동작한다