인증
(Authentication)은 주체
(Principal)의 신원
(Identity)을 증명하는 과정
주체
(Principal): 유저, 기기, 시스템 등
유저(사용자)
크레덴셜
(Credential): 주체가 인증을 위해 제시하는 신원 증명 정보
Password
인가
(Authorization, 권한부여): 인증을 마친 유저에게 권한
을 부여하여 특정 리소스에 접근을 허용
접근 통제
(Access control, 접근 제어): 어플리케이션 리소스에 접근하는 행위를 제어
서블릿 필터
를 적용해 보안을 처리
AbstractSecurityWebApplicationInitializer
베이스 클래스를 상속해 구현configure()
매서드를 이용해 다음과 같은 기본 보안 설정 적용 가능
// TodoSecurityInitalizer.java
public class TodoSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public TodoSecurityInitializer() {
super(TodoSecurityConfig.class);
}
}
configure(HttpSecurity http)
매서드는 기본적으로 다음의 특징을 지님
인증
을 받도록 강제함HTTP 기본인증
(httpBasic()) 및 폼 기반 로그인
(formLogin()) 기능은 기본적으로 킴기본 로그인 페이지
를 보이도록 구성configure(HttpSecurity http) 매서드를 오버라이딩하여 더 정교한 인가 규칙
을 적용 가능
// TodoSecurityConfig.java
@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}user") // .password("user")하면 인코딩 에러발생
.authorities("USER")
.and()
.withUser("admin").password("{noop}admin") // .password("admin")하면 인코딩 에러발생
.authorities("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests() // URL 접근 보안의 시작
.antMatchers("/todos*").hasAuthority("USER")
.antMatchers(HttpMethod.DELETE, "/todos*").hasAuthority("ADMIN")
.and()
.formLogin()
.and()
.csrf().disable();
}
}
authorizeRequests()
부터 시작되며, 여러가지 매처를 통해 규칙을 지정가능
antMatchers
로 규칙을 지정
이때 “/todos*” 처럼 마지막에
와일드카드
를 넣어줘야 쿼리 매개변수가 있는 URL도 걸림
그렇지 않다면, 해커들이 아무 쿼리 변수나 덧붙여 URL 관문을 통과할 수 있음
configure(AuthenticationManagerBuilder auth)
매서드를 통해 메모리에 유저 정보를 저장
PasswordEncoder
를 찾지 못해 예외가 발생{noop}
를 붙여줌으로써, 내부적으로 NoOpPasswoedEncoder를 사용하겠다는 선언비권장
클래스라 좋은 방법은 아니지만, 실제 메모리에 정보를 저장하는 일은 거의 없으므로 임시사용formLogin
기능을 다시 활성화 시켜줘야 함CSFR
방어 기능을 키게 되면, 폼이 제대로 동작하지 않으므로 비활성화(활성방법 후술)CSRF
방어기능은 스프링 시큐리티 기본 설정 그대로 켜두면 되지만, 필요시 csrf().disable() 로 해제 가능
CSRF 공격
(Cross Site Request Forgery)은 웹 어플리케이션 취약점 중 하나로
인터넷 사용자(희생자)가 자신의 의지와는 무관하게공격자가 의도한 행위
(수정, 삭제, 등록 등)를 특정 웹사이트에요청
하게 만드는 공격
토큰
값을 생성/보관하는 CsrfFilter를 추가재전송
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
다음의 로그인 서비스는 HTTP 자동 구성
을 활성화하면 자동 등록
되지만, 직접 커스터마이징 하는 경우 명시적
으로 구성해야 함
예외 처리
나 보안 컨텍스트
연계등 Spring Security 필수 기능
protected void configure(HttpSecurity http){
http.securityContext()
.and()
.exceptionHandling();
}
서블릿 API
연계 기능
protected void configure(HttpSecurity http){
http.servletApi();
}
httpBasic()
으로 구성HTTP 기본 인증
을 적용하면 대부분 브라우저는 로그인 대화상자를 띄우거나, 특정 로그인 페이지로 이동시킴
http
...
.httpBasic();
HTTP 기본 인증과 폼 기반 로그인을 동시에 활성화 할 경우,
폼 기반 로그인
이 우선시 됨
formLogin()
매서드로 로그인 서비스를 구현하면, 유저가 로그인 정보를 입력하는 폼 페이지가자동 랜더링
됨http ... .formLogin();
다른 로그인 페이지
를 사용하고 싶다면, login.jsp
파일을 작성 후 wepapp 루트디렉토리
에 위치시킴
인수로 전달
하고, 기본 리다이렉트 페이지 및 에러처리
http
...
.formLogin()
.loginPage("/login.jsp")
.defaultSuccessUrl("/messageList")
.failureUrl("login.jsp?error=true");
에러 메세지
를 표시
<form>
...
<c:if test="${not empty parma.error}">
Reason: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
...
</form>
logout()
매서드로 구현
http
...
.logout();
POST
요청일 경우에만 작동하며 로그아웃한 유저는 기본 경로(초기화면)으로 이동다른 URL
로 보내고 싶다면, 다음처럼 logoutSuccessUrl()
매서드에 지정
http
...
.logout().logoutSuccessUrl("/logout.success.jsp")
.and()
.headers();
캐싱
함으로 인해 로그인된 페이지로 이동하는 문제 발생
headers()
매서드로 보안 헤더를 활성화하여 브라우저의 페이지 캐싱을 방지content sniffing
방지 및 X-Frame
방어를 활성화하는 역할도 수행
content sniffing
: 바이트 스트림을 읽어 그 안의 데이터 형식을 추론하는 해킹, MIME 스니핑이라고도 불림
X-Frame
: 클릭 재킹은 웹 사용자를 클릭하여 사용자가 클릭 한 것으로 보이는 것과 다른 것을 클릭하여
기밀 정보를 공개하거나 자신의 컴퓨터를 제어하는 악의적인 기법이다.
anonymous()
매서드에 유저명과 익명유저의 권한을 주어 다음과 같이 지정
http
...
.and()
.anonymous().principal("quest").authorities("ROLE_GUEST");
// 간단하게 .anonymous()로 동작가능
rememberMe()
매서드를 통해 구현
토큰
으로 인코딩해 유저 브라우저 쿠키
로 저장자동
로그인 시킴
http
...
.rememberMe();
그러나
정적
인 리맴버 미 토큰은 해커가 얼마든지 빼낼 수 있어잠재적인 보안 이슈
가 존재
이에 Spring Security는 토큰을rolling
시키는 기술을 지원하며 토큰 저장을 위한 별도의 DB가 필요
모든
공급자의 인증 과정을 통과해야 함저장소
(메모리, RDBMS, LDAP 저장소)등에서 가져온 결과와 대조하여 인증을 수행단방향 해시
함수를 이용해 암호화
하여 저장
MD5
, SHA
등을 지원캐싱
하는 기능 제공적고
, 정보를 수정할 일이 없을
경우
// TodoSecurityConfig.java
@Configuration
@EnableWebSecurity
public class TodoSecurityConfig extends WebSecurityConfigurerAdapter {
// 한 사람씩 withUser 매서드를 이용하여 추가
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin@ya2do.io").password("secret").authorities("ADMIN","USER").and()
.withUser("marten@@ya2do.io").password("{noop}user").authorities("USER").and()
.withUser("jdoe@does.net").password("unknown").disabled(true).authorities("USER");
}
...
DB
에 저장하는 경우가 압도적조회
기능을 제공
// TodoSecurityConfig.java
...
@Override
protected void configure(AuthenticationManagerBuilder auth) throws 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");
}
In memory
방식의 경우, passwordEncoder()
매서드에 패스워드 인코더를 지정해 암호화하여 저장 가능
// TodoSecurityConfig.java
...
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.jdbcAuthentication()
.passwordEncoder(passwordEncoder())
.dataSource(dataSource());
}
BCrypt
:단방향 해시 알고리즘
으로 설계시 부터 패스워드 저장을 목적으로 설계되어 강력함
LDAP
: LDAP는 조직이나, 개체, 그리고 인터넷이나 기업 내의 인트라넷 등
네트웍 상에 있는 파일이나 장치들과 같은 자원 등의위치
를 찾을 수 있게 해주는 소프트웨어프로토콜
- LDAP의 저장소 구성은
ldapAuthentication()
매서드가 담당- 유저 및 그룹을 검색하는 필터와 베이스는
콜백 매서드
로 지정- Spring Security는 각 그룹마다
ROLE_
접두어를 붙혀 권한으로 사용// TodoSecurityConfig.java ... @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .ldapAuthentication() .contextSource() .ldif("") .url("ldap://ldap-server:389/dc=springrecipes,dc=com") .managerDn("cn=Directory Manager").managerPassword("ldap") .and() .userSearchFilter("uid={0}").userSearchBase("ou=people") .groupSearchFilter("member={0}").groupSearchBase("ou=groups") .passwordCompare() .passwordEncoder(new BCryptPasswordEncoder()) .passwordAttribute("userPassword"); }
LDAP
을 지원하는 자바 기반 오픈소스 디렉토리 서비스 엔진인OpenDS
는 기본적으로SSHA
를 사용하므로,
LdapShaPasswordEncoder
를 지정해야 함
SSHA
: Salted Secure Hash Algorithm로서, 기존의 해싱의 단점(동일한 문자열에 대해 같은 해쉬값)을 극복하는 방법으로
무작위 문자열
을 추가해 같은 문자열에 대해 매번 다른 해쉬값을 가지도록 하여 보안성을 높이는 기법
접근 통제 결정
은 유저가 리소스에 접근 가능
한지 판단하는 행위로서 유저 인증 상태
와 리소스 속성
에 따라 좌우되며,
Spring Security에서는 AccessDecisionManager 인터페이스
구현을 통해 이를 판단
거수 방식
으로 동작하는 다음 3가지 결정 관리자
를 기본 제공
AffirmativeBased
: 하나의 거수기만 거수해도 접근 허용ConsensusBased
: 거수기 전원이 만장일치해야 접근 허용UnanimousBased
: 거수기 전원이 기권 또는 찬성해야 접근 허용(적어도 반대는 없어야 함)위의 결정 관리자를 이용하기 위해, 접근 통제 결정에 대한 거수기 그룹
을 구성해야 하며
각 거수기는 AccessDecisionVoter 인터페이스
를 구현하고, 유저의 리소스 접근에 대해 찬성, 기권, 반대중 하나의 의사를 표명
AffirmativeBased
를 기본 접근 통제 결정 관리자로 임명하고 다음 두 거수기를 구성
RolVoter
: 유저 롤을 기준으로 접근 허용 여부를 거수, ROLE_ 접두어(다른 접두어도 가능)로 시작하는 접근 속성만 처리AuthenticatedVoter
: 유저 인증 레벨을 기준으로 접근 허용 여부를 거수하며, 다음 세 가지 접근 속성만 처리위 3가지는 순서대로
인증레벨
이 정해지며(1번이 가장 높음), 유저의 인증 레벨이 리소스 접근 레벨보다높으면
찬성
SpEL
(Spring Expression Language)를 사용and
, or
, not
등을 조합해 강력하고 유연한 표현식 사용 가능
// TodoSecurityConfig.java
...
@Override
protected void configure(HttpSecurity http) throws Exception {
/*
* 유저가 ADMIN 롤을 가지고 있거나, 로컬 머신에서 로그인한 유저일 경우 삭제 권한 부여
* 유저 IP주소에 따라 허용 여부를 결정
* 주소가 127.0.0.1 또는 0:0:0:0:0:0:0:1 일때 찬성, 그렇지 않으면 반대
*/
http.authorizeRequests()
.antMatchers("/todos*").hasAuthority("USER")
.antMatchers(HttpMethod.DELETE, "/todos*").hasAuthority("ADMIN")
.antMatchers("/messageDelete*)
.access("hasRole('ROLE_ADMIN') or hasIpAddress('127.0.0.1') or
hasIpAddress('0:0:0:0:0:0:0:1')")
...
}
Spring Security는 앞선 내장 함수를 제공하지만, SecurityExpressionOperations 인터페이스를 구현해 직접
등록해 사용 가능
// ExtendWebSecurityExpressionRoot.java
/*
* 이 클래스가 상속받은 WebSecurityExpressionRoot 클래스의
* 상위 클래스인 SecurityExpressionRoot 클래스가
* SecurityExpressionOperations 인터페이스 구현 클래스
*/
public class ExtendedWebSecurityExpressionRoot extends WebSecurityExpressionRoot {
public ExtendedWebSecurityExpressionRoot(Authentication a, FilterInvocation fi) {
super(a, fi);
}
/*
* 로컬 머신 로그인 여부를 체크하는 매서드를 구현해 추가
* 커스텀 표현식으로 원하는 표현식 유연하게 추가 가능
*/
public boolean localAccess() {
return hasIpAddress("127.0.0.1") || hasIpAddress("0:0:0:0:0:0:0:1");
}
}
커스텀 표현식
을 등록해 사용하기 위해서, SecurityExpressionHandler 인터페이스
구현체를 생성해야 함
// ExtendWevSecurityExpressionHandler.java
public class ExtendedWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {
private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();
@Override
protected SecurityExpressionOperations
createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
// 앞선 코드에서 생성한 ExtendedWebSecurityExpressionRoot 인스턴스 생성
ExtendedWebSecurityExpressionRoot root =
new ExtendedWebSecurityExpressionRoot(authentication, fi);
root.setPermissionEvaluator(getPermissionEvaluator());
root.setTrustResolver(trustResolver);
root.setRoleHierarchy(getRoleHierarchy());
return root;
}
...
}
createSecurityExpressionRoot()
매서드를 오버라이드해 구현한 인스턴스를 생성expressionHandler()
매서드에 지정하여 커스텀 표현식을 활용
// TodoSecurityConfig.java
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.expressionHandler(new ExtendedWebSecurityExpressionHandler())
.antMatchers("/todos*").hasAuthority("USER")
.antMatchers(DELETE, "/todos*")
.access("hasRole('ROLE_ADMIN') or localAccess()") // 커스텀 표현식 사용
.and()
...
비권장
@ 어노테이션
을 통해 Spring Bean으로 사용
// AccessChecker.java
public class AccessChecker {
public boolean hasLocalAccess(Authentication authentication) {
boolean access = false;
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:0:0:0:1");
}
return access;
}
}
동일한
기능을 제공하지만, Spring Security 클래스를 상속하지 않음
// TodoSecurityConfig.java
...
// 사용할 표현식 클래스를 Spring Bean으로 등록
@Bean
public AccessChecker accessChecker() {
return new AccessChecker();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/todos*").hasAuthority("USER")
.antMatchers(HttpMethod.DELETE, "/todos*")
// 커스텀 매서드를 어노테이션을 통해 Bean을 부름으로써 사용
.access("hasAuthority('ADMIN') or @accessChecker.hasLocalAccess(authentication)")
.and()
...
Spring Security는 빈 인터페이스나 구현 클래스에서 대상 매서드에 다음의 어노테이션
을 선언함으로 매서드 호출을 보안
위와 같은 어노테이션을 붙인 후, 구성 클래스 레벨에 @EnableGlobalMethodSecurity
를 붙이면 보안 모드로 작동
@Secured
어노테이션을 매서드에 붙이면 보안이 적용
// TodoServiceImpl.java
@Service
@Transactional
class TodoServiceImpl implements TodoService {
...
@Override
@Secured("USER")
public List<Todo> listTodos() {...}
@Override
@Secured("USER")
public void save(Todo todo) {...}
@Override
@Secured("USER")
public void complete(long id) {...}
@Override
@Secured({"USER", "ADMIN"})
public void remove(long id) {...}
@Override
@Secured("USER")
public Todo findById(long id) {...}
}
@Secured
어노테이션을 붙인 후, String[]
타입의 access 속성에 매서드별 접근 허용 권한
설정@EnableGlobalMethodSecurity
를 붙이고, 속성값
설정
@Configuration
@EnableGlobalMethodSecurity(securedEnable = true)
public class TodoWebconfiguration{...}
@PreAuthorize
, @PostAuthorize
같은 어노테이션에 SpEL
기반의 보안 표현식을 적용@EnableGlobalMethodSecurity
의 prePostEnabled
속성을 true
로 설정
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TodoWebconfiguration{...}
// TodoServiceImple.java
@Service
@Transactional
class TodoServiceImpl implements TodoService {
...
@Override
@PreAuthorize("hasAuthority('USER')")
public List<Todo> listTodos() {...}
@Override
@PreAuthorize("hasAuthority('USER')")
public void save(Todo todo) {...}
@Override
@PreAuthorize("hasAuthority('USER')")
public void complete(long id) {...}
@Override
@PreAuthorize("hasAnyAuthority('USER', 'ADMIN')")
public void remove(long id) {...}
/*
* returnObject 표현식으로 결과를 처리
* 할 일을 등록한 유저 이외의 다른 유저가 호출해 Todo 객체에 접근하면 보안 예외를 던짐
*/
@Override
@PreAuthorize("hasAuthority('USER')")
@PostAuthorize("returnObject.owner == authentication.name")
public Todo findById(long id) {...}
}
@PreAuthorize
는 매서드 호출 직전
, @PostAuthorize
는 매서드 호출 직후
각각 동작보안 표현식
을 사용하거나, returnObject 표현식
으로 매서드 호출 결과를 받아올 수 있음
@PreFilter
/@PostFilter
는 단순히 접근 권한을 체크제한
을 둠
// TodoServiceImple.java
...
@Override
@PreAuthorize("hasAuthority('USER')")
@PostFilter("hasAnyAuthority('ADMIN') or filterObject.owner == authentication.name")
public List<Todo> listTodos() {
return todoRepository.findAll();
}
@PreFilter
/@PostFilter
는 매서드 입출력을 필터링하는 편리한 수단이지만,
결과가 대용량일 경우성능저하
를 심각하게 유발하므로주의
Spring Security가 제공하는 보안 처리용 JSP 태그 라이브러리
를 사용
JSP 스크립트릿
(<% … %>)을 사용하여 Spring Security API를 호출할 수 있지만, 좋은 방법이 아니라비권장
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<sec:authentication>
태그를 이용하면, 현재 유저의 Authentication
객체를 가져올 수 있어property 속성
에 명시
하는 방법으로 랜더링 가능
<!-- 유저 주체명은 name 프로퍼티로 가져와 표시가능 -->
<h4>To-dos for <sec:authentication property="name" /></h4>
var 속성
에 이름을 지정 가능
<!-- 허용 권한 목록을 authorities 변수에 담고, <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>
하나만
지녀도 되는 경우
<!-- 두 권한 중 하나만 가져도 랜더링하려면 다음처럼 hasAnyRole 사용 -->
<td>
<sec:authorize access="hasAnyRole('ROLE_ADMIN', 'ROLE_USER')">
${todo.owner}
</sec:authorize>
</td>
않을
경우
<!-- 주어진 권한을 모두 갖고 있지 않은 유저에게만 보이게 하는 경우 -->
<td>
<sec:authorize access="!hasRole('ROLE_ADMIN') and !hasRole('ROLE_USER')">
${todo.owner}
</sec:authorize>
</td>
Spring Security는 자체로 ACL
(Access Control List, 접근 통제 목록)을 설정하는 전용 모듈
을 지원하며,
ACL에는 도메인 객체와 연결하는 ID를 비롯해 여러 개의 ACE
(Access Control Entry, 접근 통제 엔트리)가 포함되며 ACE는 다음처럼 구성
퍼미션
(Permission, 인가받은 권한): 각 비트 값은 특정 퍼미션을 의미하는 비트 마스크
이며 각 비트는다섯 가지
기본 퍼미션
과 해당하는비트
의 쌍으로 표현
이 중 안쓰는 나머지 비트를 이용해 퍼미션을임의
로 지정 가능
보안 식별자
(SID, Security Identity): 각 ACE는 특정 SID
에 대한 퍼미션을 지님JDBC
로 RDBMS에 접속하여 ACL 데이터
를 저장/조회
하는 기능을 기본 지원JDBC 구현체
및 API
를 지원하며,캐싱
하는 기능을 제공
<!-- ehcache.xml -->
...
<cache name="aclCache"
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="3600"
overflowToDisk="true"
/>
ACL 모듈
은 Java로 구성이 안되므로
, Bean
그룹으로 구성후 등록해야 하므로 설정 파일
을 작성하고 위치 등록
public class TodoSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public TodoSecurityInitializer() {
super(TodoSecurityConfig.class, TodoAclConfig.class); // TodoAclConfig 파일 위치 등록
}
}
AclService
, MutableAclService
두 인터페이스로
정의AclService는 읽기
작업을, 그 하위 인터페이스 MutableAcleService는 나머지
(생성, 수정, 삭제)를 각각 기술
// TodoAclConfig.java
@Configuration
public class TodoAclConfig {
private 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 EhCacheCacheManager ehCacheManagerFactoryBean() {
return new EhCacheCacheManager();
}
@Bean
public AuditLogger auditLogger() {
return new ConsoleAuditLogger();
}
// 자신이 가지고 있는 Permission 값으로 주어진 SID에 ACL 접근을 허용할지를 결정
@Bean
public PermissionGrantingStrategy permissionGrantingStrategy() {
return new DefaultPermissionGrantingStrategy(auditLogger());
}
/*
* 각 프로퍼티 카테고리별로 필요한 권한을 지정하는 방식으로
* 주체가 특정 ACL 프로퍼티를 변경할 권한을 갖고 있는지 판단
* 해당 코드에서는 ADMIN 권한을 지닌 유저만 ACL 소유권, 감사 세부등 ACL/ACE 정보 수정 가능
*/
@Bean
public AclAuthorizationStrategy aclAuthorizationStrategy() {
return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ADMIN"));
}
@Bean
public AclCache aclCache(CacheManager cacheManager) {
return new SpringCacheBasedAclCache(cacheManager.getCache("aclCache"), permissionGrantingStrategy(), aclAuthorizationStrategy());
}
/*
* 룩업 성능을 높이고자 고급 DB 기능을 사용하려면
* 직접 LookupStrategy 인스턴스를 구현해 룩업 전략을 만들어 사용
*/
@Bean
public LookupStrategy lookupStrategy(AclCache aclCache) {
return new BasicLookupStrategy(this.dataSource, aclCache, aclAuthorizationStrategy(), permissionGrantingStrategy());
}
/*
* ACL 구성 파일의 핵심이 되는 JdbcMutableAclService 인스턴스
* 첫 번째 인수는 ACL 데이터를 저장할 DB에 사용하는 데이터 소스
* 두 번째 인수는 표준/호환 SQL문으로 기본 룩업을 수행하는 LookupStrategy 인터페이스 구현체
* 세 번째 인수는 ACL에 적용할 캐시 인스턴스
*/
@Bean
public AclService aclService(LookupStrategy lookupStrategy, AclCache aclCache) {
return new JdbcMutableAclService(this.dataSource, lookupStrategy, aclCache);
}
@Bean
public AclPermissionEvaluator permissionEvaluator(AclService aclService) {
return new AclPermissionEvaluator(aclService);
}
}
DI
를 이용해 앞서 정의한 ACL 서비스를 이용해 도메인 객체용 ACL
을 관리
Todo-List에서는 할 일을 등록/삭제할 때마다 각각 ACL을 생성/삭제해야 함
// TodoServiceImple.java
@Service
@Transactional
class TodoServiceImpl implements TodoService {
...
/*
* 유저가 할 일을 등록하면 할 일 ID와 ACL 객체의 ID를 이용해 ACL을 생성함
* 생성한 ACL에 대해서는 해당 USER와 ADMIN이 READ, WRITE, DELETE를 할 수 있도록 삽입
*/
@Override
@PreAuthorize("hasAuthority('USER')")
public void save(Todo todo) {
this.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);
}
// 유저가 등록한 할 일을 삭제하면 해당 ACL도 함께 삭제
@Override
@PreAuthorize("hasAnyAuthority('USER','ADMIN')")
public void remove(long id) {
todoRepository.remove(id);
ObjectIdentity oid = new ObjectIdentityImpl(Todo.class, id);
mutableAclService.deleteAcl(oid, false);
}
...
}
트랜잭션
을 걸어놓아야 그 트랜잭션 안에서 SQL문 수행 가능
따라서, ACL을 관리하는 앞선 매서드의
@Transactional
어노테이션을 붙임
트랜잭션 관리자
를 추가하고, @EnableTransactionManagement
를 붙임
@Configuration
...
@EnableTransactionManagement
public class TodoWebConfig implements WebMvcConfigurer {
...
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
ACL
을 부착했으므로, 이 객체에 속한 매서드마다
접근 통제 결정을 내리는 것이 가능
유저가 할 일을 삭제하려고 하면 ACL을 보고 그 유저가 정말 삭제할
권한
이 있는지 체크 가능
애너테이션
으로 간편하게 구현 가능
@PreAuthorize
/@PreFilter
로 유저의 매서드 실행
, 특정 매서드 인수의 사용 권한
이 있는지 체크 가능@PostAuthorize
/@PostFilter
로 유저가 매서드 실행 결과에 접근
하거나,필터링
할 수 있는지 체크 가능@EnableGlobalMethodSecurity
의 prePostEnabled
속성을 true
로 설정해 사용 가능
@EnableGlobalMethodSecurity(prePostEnabled = true)
표현식
을 사용하여 ACL로 매서드를 보안하려면 커스텀 퍼미션 평가기
를 다음처럼반드시
설정
// TodoWebConfig.java
@Configuration
...
public class TodoWebConfig implements WebMvcConfigurer {
...
@Bean
public AclPermissionEvaluator permissionEvaluator(AclService aclService) {
return new AclPermissionEvaluator(aclService);
}
}
@Service
@Transactional
class TodoServiceImpl implements TodoService {
...
/*
* @PreAuthorize는 유저가 매서드를 수행할 퍼미션을 갖고 있는지 체크
* #message는 message라는 이름의 매서드 인수를 지칭
* hasPermission은 Spring Security의 기본 표현식
* @PostFilter는 컬렉션 중 현재 유저가 읽을 권한이 없는 원소를 제거, 표현식에 사용된 filterOject가 컬렉션의 원소
* @PostAuthorize는 하나의 결과값이 사용 가능한지(유저가 그에 맞는 권한을 지니는지)를 체크, 반환값은 returnObject 키워드로 사용
*/
@Override
@PreAuthorize("hasAuthority('USER')")
@PostFilter("hasAnyAuthority('ADMIN') or hasPermission(filterObject, 'read')")
public List<Todo> listTodos() {...}
@Override
@PreAuthorize("hasAuthority('USER')")
public void save(Todo todo) {...}
@Override
@PreAuthorize("hasPermission(#id, 'com.apress.springrecipes.board.Todo', 'write')")
public void complete(long id) {...}
@Override
@PreAuthorize("hasPermission(#id, 'com.apress.springrecipes.board.Todo', 'delete')")
public void remove(long id) {...}
@Override
@PostFilter("hasPermission(filterObject, 'read')")
public Todo findById(long id) {...}
}