CHAPTER 7 스프링 시큐리티

  • 인증 (Authentication) : 주체(principal)의 신원을 증명하는 과정. 주체는 자신을 인증해달라고 크레덴셜을 제시한다.
  • 권한부여 (Authorization) : 인증을 마친 유저에게 권한을 부여해 대상 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정. 권한부여는 반드시 인증 과정 이후 수행되어야한다. 롤 형태로 부여하는 것이 일반적.
  • 접근 통제 (Access Control) : 애플리케이션 특정 리소스에 접근하는 행위를 제어하는 것.

레시피 7-1 URL 접근 보안하기

  • URL에 미인가 외부 유저가 제약없이 접근할 수 있도록 보안한다.
  • Spring Security는 HTTP 요청에 서블릿 필터를 적용해 보안을 처리한다.
    • AbstractSecurityWebApplicationInitializer 클래스를 상속해 필터를 등록하고 자동으로 bean을 등록할 수 있다.
    • WebSecurityConfigurerAdapter 의 configure() 메소드를 이용해 웹 애플리케이션 보안을 쉽게 구성할 수 있다.
      • form-based login service : 유저가 애플리케이션에 로그인하는 기본 폼 페이지를 제공한다.
      • HTTP Basic authentication : 요청 헤더에 표시된 HTTP 기본 인증 크레덴셜을 처리한다. 원격 프로토콜, 웹 서비스를 이용해 인증요청을 할 때도 쓰인다.
      • 로그아웃 서비스 : 유저를 로그아웃 시키는 핸들러를 기본 제공한다.
      • 익명 로그인 (anonymous login) : 익명 유저도 주체를 할당하고 권한을 부여해 일반 유저처럼 관리할 수 있다.
      • 서블릿 API 연계 : HttpServletRequest.isUserInRole(), HttpServletRequest.getUserPricipat() 같은표준 서블릿 API를 이용해 웹 애플리케이션의 보안정보에 접근할 수 있다.
      • CSFR : 사이트 간 요청 위조 방어용 토큰을 생성해 HttpSession에 넣을 수 있다.
      • 보안 헤더 : 보안이 적용된 패키지에 대해 캐시를 해제해 보안 기능을 제공한다.

1. 스프링 시큐리티 필터 등록

  • AbstractSecurityWebApplicationInitializer 클래스를 상속해 필터를 등록한다.
    • 생성자는 하나 이상의 클래스를 인자로 받아 보안 기능을 작동할 수 있다.
public class TodoSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
    
    public TodoSecurityInitializer() {
		super(TodoSecurityInitializer.class);
    }
}
  • 스프링 시큐리티를 웹/서비스 레이어의 Config 클래스에 설정해도 되지만 다른 클래스에 나눠서 설정하는 편이 좋다.

      @Configuration
      @EnableWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
            
      }
    

2. URL 접근 보안

  • WebSecurityConfigurerAdapter.configure(HttpSecurity http) 메서드

    • 기본적으로 anyRequest().authenticated() 로 모든 요청이 강제로 인증을 받도록 한다.
    • HTTP 기본 인증 (httpBasic()) 과 폼 기반 로그인 (formLogin())을 기본으로 설정한다.
      @Configuration
      @EnableWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
            
          @Override
          protected void configure(AuthenticationManagerBuilder auth) {
              auth.inMemoryAuthentication()
                  .withUser("marten@ya2do.io").password("user").authorities("USER")
                  .and()
                  .withUser("admin@ya2do.io").password("admin").authorities("USER", "ADMIN");
          }
            
          @Overried
          protected void configure(HttpSecurity http) throw Exception {
              http.authorizedRequest()
                  .antMatchers("/todos*").hasAuthorities("USER")
                  .antMatchers(HTTPMethod.DELETE, "/todos*").hasAuthorities("ADMIN")
                  .and().formLogin()
                  .and().csrf().disable();
          }
      }
    
    • configure() 메소드는 매개변수를 다르게 해서 조건을 설정할 수 있다.
    • URL 접근 보안은 _authorizeRequest()_로 시작해 매처를 이용해 규칙을 정할 수 있다.
      • 예제에서는 유저가 어떤 권한을 가져아하는 지 _antMatcher()_로 매칭 규칙을 정한다.
    • configure(AuthenticationManagerBuilder auth)로 인증 서비스를 구성한다.
      • DB나 LDAP 저장소를 조회해 유저를 인증하는 방법을 지원한다.
      • 단순한 경우는 위처럼 직접 지정할 수 있다.

레시피 7-2 웹 애플리케이션 로그인 하기

  • 스프링 시큐리티는 다양한 방법으로 __유저가 로그인__할 수 있게 지원한다.
    • 폼 기반 로그인 지원
    • HTTP 요청 헤더에 포함된 기본 인증 크레덴셜 처리 기능이 구현되어 있다.
    • 모두가 접근할 수 있는 페이지를 처리하기 위해 익명 로그인 서비스도 제공한다.
    • 자동 로그인 서비스도 제공한다.

스프링 시큐리티 기본 기능

  • 예외처리나 보안 컨텍스트 연계 등 스프링 시큐리티의 필수 기능은 인증 기능을 활성화하기 전에 켠다.

      protected void configure(HttpSecurity http) {
          http.securityContext()
              .and()
              .exceptionHandling();
      }
    
  • 서블릿 API 연계 기능을 켜야 HttpServletRequest에 있는 메소드를 이용해 뷰에서 체크가 가능하다.

      protected void configure(HttpSecurity http) {
          http.servletApi();
      }
    

HTTP 기본 인증

  • Http 기본 인증은 httpBasic()메소드로 구성한다.

  • HTTP 기본 인증을 이용하면 로그인 대화상자를 띄우거나 유저를 특정 로그인 페이지로 이동시켜 로그인을 유도한다.

    • HTTP 기본 인증과 폼 기반 로그인을 함께 활성화하는 경우 폼 기반 로그인을 우선 사용하므로 기본 인증을 사용할 때에는 폼 기반 로그인을 비활성화한다.
      @Configuration
      @EnableWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
      	@Override
          protected void configure(HttpSecurity http) throws Exception {
              http.
                  ...
                  .htttpBasic();
          }
      }
    

폼 기반 로그인

  • formLogin()메소드로 폼 기반 로그인을 구성하면 유저가 로그인 정보를 입력하는 폼 페이지가 자동으로 렌더링된다.

    • 스프링 시큐리티의 기본 로그인 페이지는 /login이므로 커스터마이징할 경우 루트 경로에 login.jsp 를 추가한다.
    • loginPage() : 커스텀하게 만든 로그인 페이지의 경로를 인수로 넣어 로그인 페이지를 설정한다.
    • defaultSuccessUrl() : 인수로 리다이렉트할 기본 대상 URL을 설정한다.
    • failureUrl(): 로그인 실패 시 이동할 페이지를 설정한다.
      @Configuration
      @EnableWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
      	@Override
          protected void configure(HttpSecurity http) throws Exception {
              http.
                  ...
                  .formLogin()
                  .loginPage("/login.jsp")
                  .defaultSuccessUrl("/todos")
                  .failureUrl("/login.jsp?error=ture");
          }
      }
    

로그아웃 서비스

  • 로그아웃 기능은 logout()으로 설정한다.

    • 기본 url은 /logout으로 POST 요청일 때만 작동한다.
    • logoutSuccessUrl() : 로그아웃한 유저는 기본 경로인 컨텍스트 루트로 이동하지만, 다른 url로 보내고 싶은 경우 인자로 넣어주어서 설정한다.
    • headers(): 로그아웃 이후 브라우저에서 뒤로가면 로그인된 이전 페이지로 돌아가는 모순이 발생하는데, 이 메소드를 이용하면 브라우저가 페이지를 캐시하지 않아 해결할 수 있다.
      @Configuration
      @EnableWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
      	@Override
          protected void configure(HttpSecurity http) throws Exception {
              http.
                  ...
                  .logout()
                  .logoutSuccessUrl("/logout-success.jsp")
                  .headers;
          }
      }
    

익명 로그인 구현

  • anonymous()메서드에 익명 유저의 권한을 지정한다.

      @Configuration
      @EnableWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
      	@Override
          protected void configure(HttpSecurity http) throws Exception {
              http.
                  ...
                  .and()
                  .anonymous().principal("guest").authorities("ROLE_GUEST");
          }
      }
    

###리멤버 미 기능

  • rememberMe() 메소드로 구성한다.
    • 유저명, 패스워드, 리멤버미 만료시간, 개인키를 하나의 토큰으로 인코딩해 브라우저 쿠키로 저장한 뒤 재접속하면 토큰 값을 가지고와 유저를 자동로그인시킨다.
    • 보안에 문제가 있을 수 있어 토큰을 롤링하는 기능을 제공한다.
@Configuration
@EnableWebSecurity
public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
            ...
            .and()
            .rememberMe();
    }
}

레시피 7-3 유저 인증하기

  • 유저가 애플리케이션에 로그인해 보안 리소스에 접근할 수 있도록 주체를 인증하고 권한을 부여하도록 한다.
    • AuthenticationProvider 를 하나이상 이용해 인증을 수행한다.
      • 유저는 모든 공급자의 인증과정을 통과해야한다.
      • 시큐리티에는 기본 공급자 구현체가 내장되어 있고 자체 XML 엘리먼트로 구성할 수 있다.
      • 대부분 저장소에서 가져온 결과와 대조해 유저를 인증한다.
    • 패스워드는 해커의 공격을 당할 수 있으니 __단방향 해시 함수__로 암호화해서 저장한다.
    • 시큐리티는 패스워드 인코딩 알고리즘을 지원하고 기본 인코더가 있다.

인메모리 방식으로 유저 인증하기

애플리케이션 유저가 많지 않고 정보를 수정할 일이 거의 없다면, 시큐리티 구성 파일에 유저 세부를 정의해 애플리케이션 메모리에 로드하는 방법을 사용할 수 있다.

  • inMemeoryAuthentication()메소드 다음에 한사람 씩 withUser()로 연결해 유저명, 패스워드, 휴면상태, 허용 권한을 지정한다.
@Configureation
@EnalbeWebSecurity
public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throw Exception {
        auth.inMemeoryAuthentication()
            .withUser("marten@ya2do.io").password("user").authorities("USER")
            .and()
            .withUser("admin@ya2do.io").password("admin").authorities("USER", "ADMIN")
            .and()
            .withUser("jdoe@does.net").password("unknown").disalbed(true)
            	.authorities("USER");
    }
}

DB 조회 결과에 따라 유저 인증하기

  • 유저 세부는 관리 편의상 DB에 저장하는 경우가 많아 스프링 시큐리티는 SQL문을 DB에서 실행해 조회하는 기능을 제공한다.

    • 유저 기본 정보 및 권한 정보를 조회하는 쿼리를 usersByUsernameQuery(), authoritiesByUsername Query()에 지정해 사용한다.
      @Configureation
      @EnalbeWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throw Exception {
              auth.jdbcAuthentication()
                  .dataSource(dataSource)
                  .usersByUsernameQuery(
              		"SELECT username, password, 'true' as enabled "
                      + "FROM member WHERE username = ?")
                  .authoritiesByUsernameQuery(
              		"SELECT member.username, member_role.role as authorities "
              		+ "FROM member, member_role "
              		+ "WHERE member.username = ? AND member.id = member_role.member_id");
          }
      }
    

패스워드 암호화하기

  • 스프링 시큐리티는 패스워드 암호화 알고리즘을 제공한다.

  • AuthenticationManagerBuilder의 passwordEncorder()메소드에 패스워드 인코더를 지정하면 유저 저장소에 패스워드를 암호화해서 저장할 수 있다.

@Configureation
@EnalbeWebSecurity
public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throw Exception {
        auth.jdbcAuthentication()
            .passwordEncorder(new BCryptPasswordEncoder())
            .dataSource(dataSource)
            .usersByUsernameQuery(
        		"SELECT username, password, 'true' as enabled "
                + "FROM member WHERE username = ?")
            .authoritiesByUsernameQuery(
        		"SELECT member.username, member_role.role as authorities "
        		+ "FROM member, member_role "
        		+ "WHERE member.username = ? AND member.id = member_role.member_id");
    }
}

유저 세부 캐시하기

  • 캐시 기능을 제공하는 구현체를 선택해 구성한다.

  • 스프링은 Ehcache가 기본으로 내장되어 있어 클래스패스 루트의 ehcache.xml을 작성해 사용한다.

  • 스프링 시큐리티는 Ehcache인스턴스를 참조하는 EhCacheBasedUserCache와 스프링 캐시 추상체를 이용하는 SpringCacheBasedUserCache 두 가지 구현체를 제공한다.

    • jdbcAuthentication() 메소드를 사용하는 경우에만 유저캐시를 쉽게 구성할 수 있다.
    • CacheManager를 구성하고 Ehcache에 이를 전달한다.
      <ehcache>
      	<diskStore path="java.io.tmpdir" />
            
          <defaultCache maxElementInmemeory="1000"
                        eternal="false"
                        timeToIdleSeconds="120"
                        timeToLiveSeconds="120"
                        overflowToDisk="true" />
            
          <cache name="userCache"
                 maxElementInmemeory="100"
                 eternal="false"
                 timeToIdleSeconds="600"
                 timeToLiveSeconds="3600"
                 overflowToDisk="true" />
      </ehcache>
    
      @Configuration
      public class MessageBoardConfiguration {
      	@Bean
          public EhCacheCacheManager cacheManager() {
              EhCacheCacheManager cacheManager = new EhCacheCacheManager();
              cacheManager.setCacheManager(ehCacheManager.getObject());
              return cacheManager;
          }
            
          @Bean
          public EhCacheManagerFactoryBean ehCacheManager() {
              return new EhCacheMangerFactoryBean();
          }
      }
    
      @Configureation
      @EnalbeWebSecurity
      public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
          @Autowired
          private CacheManager cacheManager;
            
          @Bean
          public SpringCacheBasedUserCache userCache() throw Exception {
              Cache cache = cacheManager.getCache("userCache");
              return new SpringCacheBasedUserCache(cache);
          }
            
          @Override
          protected void configure(AuthenticationManagerBuilder auth) throw Exception {
              auth.jdbcAuthentication()
                  .passwordEncorder(new BCryptPasswordEncoder())
                  .userCache(userCache())
                  .dataSource(dataSource)
                  .usersByUsernameQuery(
              		"SELECT username, password, 'true' as enabled "
                      + "FROM member WHERE username = ?")
                  .authoritiesByUsernameQuery(
              		"SELECT member.username, member_role.role as authorities "
              		+ "FROM member, member_role "
              		+ "WHERE member.username = ? AND member.id = member_role.member_id");
          }
      }
    

레시피 7-4 접근 통제 결정하기

  • 접근 통제 결정은 유저가 리소스에 접근 가능한지 판단하는 것으로 유저 인증 상태와 리소스 속성에 따라 좌우된다.

  • 스프링 시큐리티는 AccessDecisionManager인터페이스를 구현한 관리자가 판단한다.

    • 필요시 인터페이스 구현체를 만들어서 사용할 수 있다.

    • 스프링 시큐리티는 거수 방식으로 작동하는 세 가지 접근 통제 결정 관리자를 기본으로 제공한다.

      접근 통제 결정 관리자 접근 허용 조건
      AffirmativeBased 하나의 거수기만 거수해도 접근 허용
      ConsensusBased 거수기 전원이 만장일치해야 접근 허용
      UnanimousBased 거수기 전원이 기권 또는 찬성해야 접근 허용(반대가 존재하지 않아야)

접근 통제 결정 관리자

  • 접근 통제 결정에 대한 거수기 그룹을 구성한다.
  • 각 거수기는 AccessDecisionVoter 인터페이스를 구현해 유저의 접근 요청에 대해 ACCESS_GRANTED, ACCESS_ABSTATIN, __ACCESS_DENIED__를 반환한다.
  • 별도로 관리자를 명시하지 않으면 AffirmativeBased가 기본이고, 다음 두 거수기를 구성한다.
    • RoleVoter : 유저 롤을 기준으로 접근 허용 여부를 거수한다. ROLE_접두어로 시작하는 접근 속성만 처리하며 유저가 리소스 접근에 필요한 롤과 동일한 롤을 보유하면 찬성, 하나라도 부족하면 반대한다. ROLE_ 접두어로 시작하는 속성이 없는 경우 기권한다.
    • AuthenticatedVoter : 유저 인증 레벨을 기준으로 접근 허용 여부를 거수하며 IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED, IS_AUTHENTICATED_ANONYMOUSLY 세 가지 접근 속성만 처리한다. 유저의 인증 레벨이 리소스 접근에 필요한 레벨보다 높으면 찬성한다. 레벨은 순서대로 높음 -> 낮음이다.

기본 접근 통제 결정 관리자

스프링 시큐리티는 접근 통제 관리자를 따로 지정하지 않으면 기본접근 통제 관리자를 자동으로 구성한다.

@Bean
public AffirmativeBased accessDecisonManager() {
    List<AccessDecisionVoter> decisionVoters = Arrays.asList(new RoleVoter(), new 				AuthenticatedVote());
    return new AffirmativeBased(decisonVoters);
}

@Override
protected void configure(HttpSecurity http) throw Exception {
    http.authorizedRequests()
        .accessDecisonManager(accessDecisionManager)
    ...
}
  • 기본으로 대부분의 인증 요건은 충족되지만 부족할 경우 직접 작성한다.
    • 작성한 거수기는 커스텀 접근 결정 관리자에 추가한다.
public class IpAddressVoter implements AccessDecisionVoter<Object> {
    private static final String IP_PREFIX = "IP_";
    private staitc final String IP_LOCAL_HOST = "IP_LOCAL_HOST";
    
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return (attribute.getAttribute() != null) &&
            	attribute.getAttribute().startsWith(IP_PREFIX);
    }
    
    @Override
    public int vote(Authentication authenticiation, Object object, 																	Collection<ConfigAttribute> configList) {
        if (!(authentication.getDeatils() instanceOf WebAuthenticationDetails)) {
            return ACCESS_DENIED;
        }
        
        ...
           
        return ACCESS_GREANTED;
    }
}
http.authorizeRequests()
    .accessDecisionManager()
    .antMatchers(HttpMethod.DELETE, "/todos*").access("ADMIN, IP_LOCALHOST");

표현식을 통해 접근 통제하기

  • 정교하고 복잡한 접근 통제 규칙을 적용해야 한다면 SpEL을 사용한다.
  • 스프링 시큐리티는 WebExpressionVoter 거수기를 거느린 접근 통제 관리자를 빈으로 자동 구성한다.
표현식 설명
hasRole(role) /
hasAuthority(authority)
현재 유저가 주어진 롤 및 권한을 가지고 있으면 true
hasAnyRole(role1, role2)/
hashasAuthority(auth1, auth2)
현재 유저가 주어진 롤 중 하나만 가지고 있어도 true
hasIPAddress(ipAddr) 현재 유저가 주어진 IP 주소와 일치하면 true
principal 현재 유저
Authenticiation 스프링 시큐리티 인증 객체
permitAll 항상 true
DenyAll 항상 false
isAnonymous() 익명 유저면 true
isRememberMe() 리멤버 미를 통해 로그인하면 true
isAuthenticated() 익명유저가 아니면 true
isFullyAutenticated() 리멤버미, 익명유저 둘 다 아니면 true
  • Voter를 커스터 마이징 하는 대신 매처를 이용할 수 있다.
    • has*() 메소드를 이용해 통제
    • access() 를 이용해 메소드 안에 표현식을 추가해 통제
@Configuration
@EnableWebSecurity
public class ToolSecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
            .authorizedRequests()
            .antMatchers("/todos*").hasAnyRole("USER", "GUEST")
            .antMatchers(HTTPMethod.DELETE, "/todos*")
            .access("hasRole('ROLE_ADMIN') or hasIpAddress('127.0.0.1')")
        ...
    }
}
  • SecurityExpressionOpertions 인터페이스를 구현해 스프링 시큐리티에 등록해 기능을 확장할 수 있다.
    • 표현식을 추가하고 싶을 때는 기본 구현체를 확장해서 사용하는게 알기 쉽다.
    • 구현체를 사용하기 위해서는 SecurityExpressionHandler 를 생성해서 추가해줘야된다.
    • 그리고 expressionHandler() 메서드에 지정해서 사용한다.

스프링 빈을 표현식에 넣어 접근 통제 결정하기

  • 표현식 내부에 커스텀 클래스를 만들어 사용하면 @Syntax 형식으로 불러와 사용할 수 있다.
public class AccessChecker {
    public boolean hasLocalAccess(Authentication authentication) {
        boolean access = true;
        if (authentication.getDetails() instanceOf WebAuthenticationDetails) {
            WebAuthenticationDetails details = 
                (WebAuthenticationDetails) authentication.getDetails();
            String address = details.getRemoteAddress();
            access = address.equals("127.0.0.1") || address.equals("0:0:0:0:1");
        }
        return access;
    }
}
@Bean
public AccessChecker accessChecker() {
	return new AccessChecker();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.
            .authorizedRequests()
            .antMatchers("/todos*").hasAnyRole("USER", "GUEST")
            .antMatchers(HTTPMethod.DELETE, "/todos*")
            .access("hasRole('ROLE_ADMIN') or @accessChecker.hasLocalAccess(auth)")
        ...
    }

레시피 7-5 메서드 호출 보안하기

서비스 레이어에서 메소드 호출 자체를 보안해야하는 경우를 사용한다.

예를 들면, 서비스레이어에 위치한 여러메서드를 하나의 컨트롤러가 호출할 때 메서드 별로 개발 보안 정책을 세밀하게 적용하고 싶은 경우가 있다.

  • 빈 인터페이스나 구현 클래스에서 보안 대상 메소드에 @Secured, @PreAuthorized/@PostAuthorized, @PreFilter/ @PostFilter 어노테이션을 붙여 선언하고 설정 클래스에서 @EnableGlobalMethodSecurity를 붙이면 적용이 가능하다.
    • @EnableGlobalMethodSecurity 는 보안을 적용할 빈을 포함한 애플리케이션 컨텍스트 구성 클래스에 붙여야한다.

어노테이션을 붙여 보안하기

  • 메소드에 @Secured를 붙이면 보안이 적용된다.
public class MessageBoardServiceImpl implements MessageBoardService {
    @Secured({"ROLE_USER", "ROLE_GUEST"})
    public List<Message> listMessage() {
        ...
    }
    
    @Secured("ROLE_USER")
    ...
}
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true)
public class TodoWebConfiguration { ... }

애너테이션 + 표현식으로 메서드 보안하기

  • @PreAuthorized/@PostAuthorized 애너테이션에 표현식을 작성해 사용한다.

  • @EnableGlobalMethodSecurity(prePostEnabled = true)

      @PreAuthrize("hasAuthority('USER')")
      public void save(Todo todo) {
          ...
      }
    

애너테이션 + 표현식으로 거르기

  • @PreFilter/ @PostFilter : 접근 권한이 없는 요소의 입출력 변수를 필터링한다.

    • 데이터가 대용량인 경우 성능을 심각하게 떨어뜨린다.
      @PreAuthrize("hasAuthority('USER')")
      @PostFilter("hasAnyAuthority('ADMIN') or filterObject.owner == authentication.name")
      public void save(Todo todo) {
          ...
      }
    

레시피 7-6 뷰에서 보안 처리하기

  • 스프링 시큐리티가 제공하는 보안처리용 JSP 태그 라이브러리를 이용한다.

    • 웹 애플리케이션 뷰 화면에서 주체명이나 허용 권한 등 유저 인증 관련 정보를 표시하거나 유저 권한에 따라 조건부로 콘텐트를 감추는 기능을 구현할 수 있다.

인증 정보 표시하기

  • 유저의 주체명과 허용 권한 표시
  1. 스프링 시큐리티 태그 라이브러리 임포트
<% taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<% taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
  1. 유저의 주체명 가져오기
  • <sec:authentication> 를 이용하면 현재 유저의 Authentication 객체를 가지고 올 수 있다.
<h4>
    Todos for <sec:authentication property="name" />
</h4>
  • jsp 변수에 프로퍼티를 옮겨담아 var 속성에 이름을 지정할 수 있다.
    • 허용 권한(authorities) 목록을 jsp 변수에 담고 <c:forEach> 태그로 하나씩 꺼내서 랜더링한다.
<sec:authentication property="authorities" var="authorities" />
<ul>
	<c:forEach items="${authorities}" var="authority">
    	<li>${authority.authority}</li>
    </c:forEach>
</ul>
    

뷰 콘텐트를 조건부 렌더링 하기

  • <sec:authorize> 를 이용하면 유저 권한에 따라 뷰 콘텐트를 조건부로 표시할 수 있다.
  • ROLE_ADMIN, ROLE_USER 권한을 모두 지닌 유저일 경우만 태그로 감싼 부분을 랜더링한다.
<td>
	<sec:authorize access="hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')">
    	${todo.owner}
    </sec:authorize>
</td>

레시피 7-7 도메인 객체 보안 처리하기

  • 도메인 객체마다 주체별로 접근 속성을 달리해서 처리해야하는 경우도 있다.
  • 스프링 시큐리티는 자체로 ACL(Access Control List : 접근 통제 목록) 을 설정하는 전용 모듈을 지원한다.
    • ACL에는 도메인 객체와 연결하는 ID를 비롯해 여러 개의 ACE(Acess Control Entity) 가 있다.
    • ACE는 두 가지 핵심 요소로 구성된다.
      • Permission(인가받은 권한) : 각 비트 값은 특정 퍼미션을 의미하는 비트 마스크로 구성되어 있다. BasePermission 클래스는 다섯가지 퍼미션 READ(0), WRITE(1), CREATE(2), DELETE(3) ADMINISTRATION(4)가 상수로 정의되어 있다. 이 중 사용하지 않는 나머지 비트를 이용해 퍼미션을 임의로 지정할 수 있다.
      • 보안 식별자(SID, Security IDentity) : 각 ACE는 특정 SID에 대한 퍼미션을 가진다. SID는 주체(PrincipleSid)일 수도 있도 퍼미션과 연관된 권한일 수도 있다. 스프링 시큐리티에는 ACL 객체 모델의 정의뿐만 아니라 이 모델을 읽고 관리하는 API도 정의되어 있다. 또 이 API를 구현한 JDBC 구현체까지 제공한다. 아울러 ACL을 더욱 쉽게 사용할 수 있도록 접근 통제 결정 거수기나 JSP 태그 같은 편의 기능도 마련되어 있어 애플리케이션의 다른 보안 장치들과 함께 사용할 수 있다.

ACL 서비스 설정하기

  • 스프링은 JDBC로 RDBMS에 접속해 ACL 데이터를 저장/조회하는 기능을 지원한다.
  1. ACL은 도메인 객체마다 별도로 둘 수 있어 전체 ACL 개수가 많아 질 수 있다. 하지만, 스프링 시큐리티는 ACL 객체를 캐시하는 기능을 지원해 ehcache.xml 에 캐시를 설정하면 된다.
<ehcache>
	...
    <cache name="aclCache"
           maxElementsInMemory="1000"
           eternal="false"
           timeToIdleSeconds="600"
           timeToLiveSeconds="3600"
           overflowToDisk="true"/>
</ehcache>
  1. 애플리케이션에서 사용할 ACL 서비스 설정
  • 스프링 시큐리티는 ACL 모듈을 자바로 구성할 수 있게 지원하지 않으므로 그냥 스프링 빈 그룹으로 구성해야 한다.
public class TodoSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
    public TodoSecurityInitializer() {
        super(TodoSecurityConfig.class, TodoAclConfig.class);
    }
}
  • 스프링 시큐리티에서 ACL 서비스 작업은 AclService, MutableAclService 인터페이스로 정의한다.
    • AclService는 Acl 읽기 작업, MutableAclService는 수정,삭제, 생성을 각각 기술한다.
    • ACL 읽기만 할 경우 JdbcAclService 같은 AclService 구현체를 그 외에는 JdbcMutableAclService 같은 MutableAclService 구현체를 각각 골라쓰면 된다.
@Configuration
public class TodoConfig {
    privat final DataSource dataSource;
    
    public TodoAclConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    
    @Bean
    public AclEntryVoter aclEntryVoter(AclService aclService) {
        return new AclEntryVoter(aclService, "ACL_MESSAGE_DELETE",
                                new Permission[] {BasePermission.ADMINISTRATION, BasePermission.Delete});
    }
    
	...
    @Bean
    public LookupStrategy lookupStrategy(AclCache aclCache) {
    	return new BasicLookupStrategy(this.dataSource, aclCache, aclAuthorizationStrategy(), permissionGrantingStrategy());    
    }
        
    @Bean
    public AclService aclService(LookupStrategy lookupStrategy, AclCache aclCache) {
    	return new JdbcMutableAclService(this.dataSource, lookupStrategy, aclCache);    
    }
}
  • ACL 구성 파일의 핵심인 ACL 서비스 빈은 JdbcMutableService 인스턴스이다.
    • 첫 번째 인자는 ACL 데이터를 저장할 DB에 접속하는데 사용되는 데이터 소스, 세 번째 인수는 ACL에 적용할 캐시 인스턴스이다.
  • BasicLookupStrategy 는 표준/호환 SQL문으로 룩업을 수행하는 스프링 시큐리티의 유일한 LookupStrategy 구현체 이다.

도메인 객체에 대한 ACL 관리하기

  • 백엔드 서비스와 DAO에서는 DI를 이용해 앞서 정의한 ACL 서비스를 이용하여 도메인 객체용 ACL을 관리한다.
@Service
@Transactional
class TodoServiceImpl implements TodoService {
    private final TodoRepository todoRepository;
    private final MutableAclService mutableAclService;
    
    TodoServiceImpl(TodoServiceRepository todoRepository, 
                    MutableAclService mutableAclService) {
        this.todoRepository = todoRepository;
        this.mutableAclServcie mutableAclService;
    }
    
    @Override
    @PreAuthorize("hasAutority('USER')")
    public void save(Todo todo) {
        todoRepository.save(todo);
        ObjectIdentity oid = new ObjectIdentityImpl(Todo.class, todo.getId());
        MutableAcl acl = mutableAclService.createAcl(oid);
        acl.insertAce(0, READ, new PrincipalSid(todo.getOwner()), true);
        acl.insertAce(1, WRITE, new PrincipalSid(todo.getOwner()), true);
        acl.insertAce(2, DELETE, new PrincipalSid(todo.getOwner()), true);
        acl.insertAce(3, READ, new GrantedAuthoritySid("ADMIN"), true);
        acl.insertAce(4, WRITE, new GrantedAuthoritySid("ADMIN"), true);
        acl.insertAce(5, DELETE, new GrantedAuthoritySid("ADMIN"), true);
    }
    
    @Override
    @PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
    public void remove(long id) {
        todoRepository.remove(id);
        
        ObjectIdentity oid = new ObjectIdentityImpl(Todo.class, id);
        mutableAclService.deleteAcl(oid, false);
    }
}

표현식을 이용해 접근 통제 결정하기

  • @PreAuthorized/@PreFilter 로 유저가 메소드 실행이나 인수에 사용권한이 있는 지 체크 가능하다.
  • @PostAuthorized/@PostFilter 로 유저가 메소드 실행 결과나 ACL에 따라 그 결과를 필터링할 수 있는지 체크할 수 있다.
  • 이들을 사용하려면 @ㄷEnalbeGlobalMethodSecurity(prePostEnabled=true) 를 설행해야 한다.