[데브코스] Spring Security - 6
Updated: Categories: TILW16D5 - Spring Security - JPA 연동 / Spring Session에 대해 알아보자
Security - JPA 연동
1. 엔티티 생성
- JDBC 연동때와 동일한 테이블 사용
- 하지만 정해진 테이블 구조가 있던 JDBC 연동과 다르게 테이블 형식은 자유로움
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);
}
- 하지만 이렇게 만들면 쿼리가 세번 실행되게 된다.
- 사용자 테이블 조회
- 사용자가 속한 그룹 테이블 조회
- 그룹 테이블에 부여된 권한 테이블 조회
- 따라서 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
의 구현체임)를 반환함
- Security에서 사용되는 User 객체(
@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) 지점이 되는것을 방지하기 위해 외부 스토리지는 보통 클러스터로 구성됨
Spring Session
- 역시 Spring Boot에서 Session Cluster를 구현하기 위한 다양한 기능을 제공
- Session을 저장하기 위한 외부 스토리지를 추상화함으로써 일관된 API로 JDBC, Redis, Hazelcast 등 다양한 스토리지를 활용할 수 있다.
- 이 중
spring-session-jdbc
는 세션 정보를 DB에 저장하는 방식
설정
- 세션 테이블 정보는
org/springframework/session/jdbc/
경로 하위 sql들에 정의되어 있음 - 어플리케이션 로드시 해당 sql을 통해 세션 테이블이 정의된다.
- 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 {
...
}
동작 원리
SessionRepository
- Session의 CRUD 기능을 제공
- 스토리지 종류에 따라 다양한 구현체를 제공함
MapSessionRepository
: In-Memory Map기반이며, 별도의 의존 라이브러리 필요 없음RedisIndexedSessionRepository
: redis 기반이며,@EnableRedisHttpSession
어노테이션으로 생성됨JdbcIndexedSessionRepository
: jdbc 기반이며,@EnableJdbcHttpSession
어노테이션으로 생성됨
SessionRepositoryFilter
HttpServletRequest
,HttpServletResponse
인터페이스 구현을SessionRepositoryRequestWrapper
,SessionRepositoryResponseWrapper
구현체로 교체함HttpServletRequest
,HttpServletResponse
인터페이스의 Session 처리와 관련한 처리를 OverrideSession
관련 생성 및 입출력은SessionRepository
인터페이스를 통해 처리함HttpSession
인터페이스에 대해 Spring Session 구현체HttpSessionWrapper
를 사용하도록 함HttpSessionWrapper
구현체는org.springframework.session.Session
인터페이스를 포함하고 있음- 스토리지 종류에 따라
org.springframework.session.Session
인터페이스 구현체가 달라짐
- 스토리지 종류에 따라
Spring Security와의 관계
- Spring Session의
SessionRepositoryFilter
클래스는 Spring Security의DelegatingFilterProxy
보다 먼저 실행됨 - Spring Security의
SecurityContextPersistenceFilter
는SecurityContextRepository
인터페이스 구현체를 통해 사용자의SecurityContext
를 가져오거나 갱신함SecurityContextRepository
인터페이스 기본 구현은 Session을 이용하는HttpSessionSecurityContextRepository
클래스HttpServletRequest
인터페이스의 getSession() 메소드를 통해 Session을 가져옴- 바로 이 지점에서
HttpServletRequest
인터페이스의 스프링 세션 구현체인SessionRepositoryRequestWrapper
클래스가 사용됨
- 바로 이 지점에서
- 결과 : Spring Security는 Spring Session과 독립적으로 동작한다