package.springbook.config;
import org.springframework.security.config.annotation.web.builders.*;
import org.springframework.security.config.annotation.web.configuration.*;
@EnableWebSecurity //클래스에 @EnableWebSecurity를 지정하면 스프링 시큐리티가 제공하는 설정 클래스가 임포트되고 스프링 시큐리티를 이용할 때 필요한 컴포넌트의 빈이 자동으로 정의된다.
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //부모클래스로 WebSecurityConfigurerAdapter클래스를 지정한다. 상속하면 기본적으로 적용되는 빈의 정의를 간단히 커스터마이징 할 수 있다.
@Override
public void configure(WebSecurity web){
web.ignoring().antMatchers("/resources/**"); //보안기능이 필요 없는 리소스(CSS나 자바스크립트)에는 스프링 시큐리티를 적용하지 않는다.
}
}
이렇게 만든 설정 클래스를 사용해 DI 컨테이너가 만들어지도록 정의
web.xml의 설정 예
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<!-- contextConfigLocation에 작성한 설정 클래스를 지정 -->
<param-name>contextConfigLocation</param-name>
<param-value>springbook.config.WebSecurityConfig</param-value>
</context-param>
xml파일 작성
XML기반 설정 방식을 사용할 때는 다음과 같은 파일을 작성.
XML파일(security-context.xml)의 작성 예
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd
">
<!-- 스프링 시큐리티가 제공하는 XML 네임스페이스를 활성화.
이 예에서는 sec라는 이름을 할당하고 있다.
네임스페이스를 이용하면 스프링 시큐리티의 컴포넌트를 빈으로 간단히 정의할 수 있다. -->
<sec:http>
<!-- <sec:http>요소를 정의. 정의하면 시큐리티를 이용할 때 필요한 컴포넌트의 빈이 자동으로 정의 -->
<sec:intercept-url pattern="/**" access="isAuthenticated()" />
<sec:form-login />
<!-- 이 예에서는 보안 설정이 제대로 적용되었는지 확인하기 쉽도록 요청되는 모든 경로에 인증을 하도록 만들었다.
인증방식으로는 폼 기반 인증 기능을 사용 -->
</sec:http>
<sec:authentication-manager />
<!-- <sec:authentication-manager>요소를 정의해 인증용 컴포넌트를 빈으로 정의한다.
이 요소를 정의하지 않으면 서버를 가동할 때 오류가 발생한다. -->
</beans>
보안 기능이 필요없는 리소스(CSS나 자바스크립트)에 대해서는 다음과 같이 빈을 정의해서 스프링 시큐리티가 인증을
하지 않게 만든다. 만약 <sec:http>요소가 여러개 정의되어 있다면 정의한 순서대로 경로 패턴을 매칭하기 때문에
위에서 설정한 <sec:http>요소보다 다음 설정을 순서상 먼저 기술 해야 한다.
스프링 시큐리티가 인증하지 않도록 하기위한 빈 정의 예
<!-- 인증이 필요없는 리소스에 대한 경로 패턴을 지정 -->
<sec:http pattern="/resources/**" security="none" />
이렇게 만든 XML파일을 사용해 DI컨테이너가 만들어지도록 정의한다.
web.xml의 설정 예
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<!-- contextConfiglocation에 작성한 XML파일을 지정 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/META-INF/spring/security-context.xml</param-value>
<context-param>
서블릿 필터 설정
마지막으로 스프링 시큐리티에서 제공하는 서블릿 필터 클래스(FilterrChainProxy)를 서블릿 컨테이너에 등록한다.
web.xml의 설정 예
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<!-- 스프링 프레임워크가 제공하는 DelegatingFilterProxy를 사용해 DI컨테이너에서 관리되는
빈(FilterChainProxy)을 서블릿 컨테이너에 등록.
서블릿 필터의 이름으로 DI 컨테이너에서 관리되는 빈의 이름(springSecurityFilterChain)을
지정한다. -->
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 스프링 시큐리티를 적용할 URL 패턴을 지정. 이 예에서는 모든 요청에 대해 스프링 시큐리티를 적용-->
보통 엔터프라이즈 어플리케이션을 개발할 때는 하나의 처리를 수행하기 위해 여러개의 컴포넌트를 조합해서
구현하는 경우가 일반적이다.
이때 사용되는 컴포넌트에는 '공통으로 사용되는 기능을 따로 분리한 컴포넌트', '데이터베이스에 접근하기 위한
컴포넌트', '외부 시스템이나 서비스에 접속하기 위한 컴포넌트' 등과 같이 다양한 컴포넌트가 있다.
이처럼 하나의 처리를 구현하기 위해 여러개의 컴포넌트를 통합할 때 의존성 주입이라는 접근 방식이 큰 힘을
발휘한다. 예를들어, 사용자를 등록하는 클래스를 구현한다고 가정하면, 사용자 등록을 위해 필요한 처리 흐름은
다음과 같다.
1. 등록하려는 사용자 계정이 이미 등록되어 있는지 확인한다.
2. 등록하려는 사용자의 패스워드를 해시(Hash)한다.
3. 사용자 정보를 저장한다.
이 같은 처리를 구현하는데 필요한 인터페이스는 다음과 같다.
사용자 등록을 처리하는 인터페이스
public interface UserService {
//사용자 정보를 등록한다.
void register(User user, String rawPassword);
}
패스워드를 해시화하는 인터페이스
public interface PasswordEncoder {
//패스워드를 해시화한다.
String encode(CharSequence rawPassword);
}
사용자 정보를 관리하는 인터페이스
public interface UserRepository {
//사용자 정보를 저장한다.
User save(User user);
//사용자 계정명이 일치하는 사용자 수를 카운트한다.
int countByUsername(String username);
}
사용자 등록을 처리하는 구현 클래스
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public UserServiceImpl(javax.sql.DataSource dataSource) {
//데이터베이스 방식으로 사용자 정보를 관리하는 구현 클래스
this.userRepository = new JdbcUserRepository(dataSource);
//Bcrypt 알고리즘으로 해시화하는 구현 클래스
this.passwordEncoder = new BCryptPasswordEncoder();
}
public void register(User user, String rawPassword) {
if(this.userRepository.countByUsername(user.getUsername()) > 0) {
//같은 사용자 계정의 사용자가 있다면 예외를 발생시킨다.
throw new UserAlreadyRegisteredException();
}
//입력된 원본 패스워드를 해시화 한 후, 사용자 정보로 설정한다.
user.setPassword(this.passwordEncoder.encode(rawPassword));
this.userRepository.save(user);
}
}
위 예제에서는 생성자에서 userRepository와 passwordEncoder를 초기호 ㅏ하기 위해 UserRepository 와
PasswordEncoder의 구현 클래스를 직접 생성해서 할당한다.
그래서 UserServiceImpl클래스를 개발하는 단계에서는 의존하는 컴포넌트 클래스가 이미 완성되어 있어야 한다.
이처럼 필요한 컴포넌트를 생성자에서 직접 생성하는 방식은 일단 클래스가 생성되고 나면 이미 생성된
UserRepository나 PasswordEncoder의 구현 클래스를 교체하는것이 사실상 어려울 수 있다.
이러한 클래스 간의 관계를 두고 '클래스 간의 결합도가 높다'라고 말한다.
엔터프라이즈 애플리케이션을 개발할 때는 다양한 컴포넌트를 조합하는 것이 일반적이라고 했는데,
많은 컴포넌트에 의존해야 하는 클래스를 이 같은 방식으로 개발하는것은 상당히 비효율적이다.
예를들어 의존하는 클래스가 아직 개발중이거나, 미들웨어 벤더가 제공해줘야 하는 클래스가 있는데
그 벤더의 제품이 완성되지 않았을 수도 있다.
결국 모든 컴포넌트가 제대로 모양새를 갖추려면 개발의 막바지에 이르러서야 오류없이 조립할 수 있다.
해결 대안으로는 필요한 컴포넌트가 완성 될 때 까지 임시로 dummy 클래스를 만들어서 대체하는 방법이 있다.
다만, 언젠가는 제대로 구현해서 교체해야 할 코드이기 때문에 개발규모가 커지면 커질수록 재 작업의 양이 늘어난다.
UserServiceImpl 클래스의 결합도를 낮추려면 우선 생성자 안에서 UserRepository와 passwordEncoder의
구현 클래스를 직접 생성하는 대신, 생성자의 인수로 받아서 할당하는 방법을 생각해볼 수 있다.
이렇게 DI 컨테이너를 통해 각 컴포넌트의 인스턴스를 생성하고 통합 관리하면서 얻을 수 있는 장점은
컴포넌트간의 의존성 해결 뿐만이 아니다.
어떤 컴포넌트는 반드시 단 하나의 인스턴스만 만들어서 재사용 되도록 싱글턴(singleton)객체로 만들어야 하고
어떤 컴포넌트는 매번 필요할 때마다 새로운 인스턴스를 사용하도록 프로토타입(prototype)객체로 만들어야 한다.
이러한 인스턴스의 스코프(scope)관리를 DI컨테이너가 대신한다.
각 인스턴스가 필요로 하는 공통 처리 코드를 외부에서 자동으로 끼워넣는 AOP기능도 DI컨테이너가 대신 해준다.
DI 개요
의존성 주입이라고도 하며 IoC라고 하는 소프트웨어 디자인 패턴 중 하나다.
DI 컨테이너에서 인스턴스를 관리하는 방식에는 다음과 같은 장점이 있다.
1. 인스턴스의 스코프를 제어할 수 있다.
2. 인스턴스의 생명 주기를 제어할 수 있다.
3. AOP 방식으로 공통 기능을 집어넣을 수 있다.
4. 의존하는 컴포넌트 간의 결합도를 낮춰서 단위 테스트 하기 쉽게 만든다.
스프링의 공식 문서에서는 DI가 아닌 IoC컨테이너라고 기재하고 있다.
ApplicationContext와 빈 정의
스프링 프레임워크에서는 ApplicationContext가 DI 컨테이너의 역할을 한다.
DI컨테이너에서 인스턴스 꺼내기
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//설정클래스(Configuration class)를 인수로 전달하고 DI컨테이너를 생성한다.
//설정클래스는 여러개 정의 할 수도 있다.
UserService userService = context.getBean(UserService.class);
//DI컨테이너에서 UserService 인스턴스를 가져온다.
여기서 AppConfig 클래스는 DI컨테이너에서 설정파일 역할을 하며, 자바로 작성돼 있어서
자바 컨피규레이션클래스(Java Configuration Class)라고도 한다.
그리고 이렇게 자바 컨피규레이션 클래스로 설정하는 방식을 자바 기반 설정방식이라고 하며, 다음과 같은 형태로
작성한다.
자바기반 설정 방식의 예
@Configuration
public class AppConfig {
@Bean
UserRepository userRepository() {
return new UserRepositoryImpl();
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
UserService userService() {
return new UserServiceImpl(userRepository(), passwordEncoder());
}
}
그리고 이 설정을 사용하는 애플리케이션과 ApplicationContext의 관계는 다음과 같다.
@Configuration 과 @Bean 어노테이션을 사용해서 DI컨테이너에 컴포넌트를 등록하면 애플리케이션은
DI 컨테이너에서 있는 빈(Bean)을 ApplicationContext 인스턴를 통해 가져올 수 있다.
스프링 프레임워크에서는 DI 컨테이너에 등록하는 컴포넌트를 빈이라고 하고, 이 빈에 대한 설정(Configuration)
정보를 '빈 정의(Bean Definition)'라고 한다.
또한 DI컨테이너에서 빈을 찾아오는 행위를 룩업(lookup)이라고 한다.
DI 컨테이너에서 빈 가져오기
UserService userService = context.getBean(UserService.class);
//가져오려는 빈의 타입(Type)을 지정하는 방법이다.
//지정한 타입에 해당하는 빈이 DI 컨테이너에 오직 하나만 있을 때 사용한다.
//스프링프레임워크 3.0 부터 사용할 수 있다.
UserService userService = context.getBean("userService", UserService.class);
//가져오려는 빈의 이름과 타입을 지정하는 방법.
//지정한 타입에 해당하는 빈이 DI컨테이너에 여러개 있을 때 이름으로 구분하기 위해 사용.
//2.5.6버전까지는 반환값이 Object 타입이라서 원하는 빈의 타입으로 형변환 해야 했지만
//3.0.0버전 이후부터는 형변환 하지 않아도 된다.
UserService userService = (UserService) context.getBean("userService");
//가져오려는 빈의 이름을 지정하는 방법이다.
//반환값이 Object타입이라서 원하는 빈의 타입으로 형변환 해야 한다.
//1.0.0버전부터 사용할 수 있다.
빈 설정 방법
자바 기반 설정 방식이나 XML 기반 설정 방식만 사용해서 빈을 정의할 수도 있지만 대부분의 경우 자바 기반
설정 방식과 어노테이션 기반 설정 방식을 조합하거나 XML 기반 설정 방식과 어노테이션 기반 설정 방식의
조합을 많이 사용한다.
ApplicationContext를 생성하는 다양한 방법
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
//자바 기반의 설정방식으로 AnnotationConfigApplicationContext의 생성자에
//@Configuration 어노테이션이 붙은 클래스를 인수로 전달한다.
ApplicationContext context = new AnnotationConfigApplicationContext("com.example.app");
//어노테이션 개반 설정방식으로 AnnotationConfigApplicationContext의 생성자에
//패키지명을 인수로 전달한다.
//지정된 패키지 이하의 경로에서 컴포넌트를 스캔한다
ApplicationContext contet =
new ClassPathXmlApplicationContext("META-INF/spring/applicationContext.xml");
//XML 기반의 설정방식으로 ClassPathXmlApplicationContext의 생성자에 XML파일을 인수로 전달한다.
//경로에 접두어(Prefix)가 생략된 경우에는 클래스패스 안에서 상대경로로 설정파일을 탐색한다.
ApplicationContext context =
new FileSystemXmlApplicationContext("./spring/applicationContext.xml");
//XML기반의 설정방식으로 FileSystemXmlApplicationContext의 생성자에 XML 파일을 인수로 전달한다.
//경로에 접두어가 생략된 경우에는 JVM의 작업 디렉토리 안에서 상대경로로 설정파일을 탐색한다.
ApplicationContext는 단독 애플리케이션에서 스프링 프레임워크를 사용하거나 JUnit으로 만든 테스트 케이스 안에서
스프링 프레임워크를 구동할 때 사용된다.
웹 애플리케이션에서는 스프링 MVC를 활용하게 되는데, 이때는 웹 환경에 맞게 확장한 WebApplicationContext를