Spring Security 기능 활용 #2 (Configuration, WebSecurityConfigurerAdapter)
<개요>
- WebSecurityConfigurerAdapter 사용
- Annotation기반의 설정
<내용>
- 기본적인 세팅 방법 및 제공메소드를 알아본다.
- 특정 IP 의 호출만 가능하도록 해본다. (white list)
- hasIpAddress 활용을 위한 API, SpEL을 사용해본다.
- IP subnet , IPv4/v6 차이점 확인
1. WebSecurityConfigurerAdapter
- 우리가 일반적으로 가장 많이 사용하는 방법이다.
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception
{
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// swagger-ui 관련 접속 url - permitAll 추가
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger.json").permitAll()
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/webjars/**").permitAll()
- Session은 STATELESS를 기본으로 하며 각 url에 permitAll() 권한을 부여하는 것으로 시작하였다.
- 상세한 정책은 antMatchers() 를 사용하며 permitAll(), anonymous(), denyAll(), authenticated(), hasAuthority()등 다양한 방법을 통해서 정의가 가능하다.
- hasRole() or hasAnyRole()
특정 역할을 가지는 사용자 - hasAuthority() or hasAnyAuthority()
특정 권한을 가지는 사용자 - hasIpAddress()
특정 아이피 주소를 가지는 사용자 - permitAll() or denyAll()
접근을 전부 허용하거나 제한 - rememberMe()
리멤버 기능을 통해 로그인한 사용자 - anonymous()
인증되지 않은 사용자 - authenticated()
인증된 사용자
- 내부 구현방식을 알아보기 위해서 Spring 내부소스를 보면 다음과 같다.
public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
extends
AbstractInterceptUrlConfigurer<ExpressionUrlAuthorizationConfigurer<H>, H> {
static final String permitAll = "permitAll";
private static final String denyAll = "denyAll";
private static final String anonymous = "anonymous";
private static final String authenticated = "authenticated";
private static final String fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe";
public ExpressionInterceptUrlRegistry permitAll() {
return access(permitAll);
}
public ExpressionInterceptUrlRegistry anonymous() {
return access(anonymous);
}
public ExpressionInterceptUrlRegistry rememberMe() {
return access(rememberMe);
}
public ExpressionInterceptUrlRegistry denyAll() {
return access(denyAll);
}
- 내부적으로 SpEL을 활용하고 access메소드를 통해서 처리하는 것을 볼 수 있다.
2. Annotation을 통한 관리
- @PreAuthorize, @PostAuthorize, @Secured 를 사용하여 Controller레벨에서 메소드별 관리도 가능하다.
@RestController
public class FilterController {
@PreAuthorize("hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/api/a", method = RequestMethod.POST)
public @ResponseBody
List<String> testMethod()throws Exception {
...
}
- SpEL을 사용하는 등 내부적인 처리는 1번방식과 동일하다.
- 다음과 같은 전역설정을 필요로 하며 되도록 @Configuration 과 같은 성격들은 한곳에 모아두는 것을 추천한다.
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
3. hasIpAddress 예제
- SecurityConfig에 다음과 같이 추가를 하게되면 로컬에서만 /login 으로 접근하는 것이 가능하다.
.authorizeRequests()
.antMatchers("/login").hasIpAddress("127.0.0.1")
- 이 때 IP, Subnet등의 다양한 방법이 가능하다.
e.g) 192.168.1.0/24, 172.16.0.0/16
- 여러 개의 ip를 list로 처리하고 싶다면? hasIpAddress()로는 처리가 어렵다. 힌트는 1번에서 살펴보았던 access()메소드이다.
hasIpAddress()도 내부적으로 access()를 통해서 처리하고 있다.
public ExpressionInterceptUrlRegistry hasIpAddress(String ipaddressExpression) {
return access(ExpressionUrlAuthorizationConfigurer
.hasIpAddress(ipaddressExpression));
}
- 즉, hasIpAdress("ip #1") or hasIpAdress("ip #2") 의 형태로 SpEL을 작성하여 access()메소드를 호출하면 될 것 같다.
4. 구현소스
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception
{
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/login").access( buildSpEL(ssoProperties.getAllowedIps()) )
...
private String buildSpEL(List<String> ipLists){
if(ipLists.isEmpty()){
return "hasIpAddress('0.0.0.0/0')";
}
return ipLists.stream()
.map(s -> String.format("hasIpAddress('%s')",s) )
.collect(Collectors.joining(" or "));
}
- 특정 ip 목록이 추가될 경우 SpEL처리를 하며, 빈 값이 넘어올 경우 전체 ip에 대해서 허용해주도록 한다.
@ConfigurationProperties(prefix = "1.2.3")
@Getter
@Setter
@ToString
public class Properties {
private SsoProperties ssoProperties;
}
public class SsoProperties {
private List<String> allowedIps = new ArrayList<>();
}
- ip목록의 경우 수시로 바뀔수 있기 때문에 별도 properties를 활용하도록 한다.
<수행결과>
- FilterSecurityInterceptor에 의해서 처리되었으며 처리 결과는 다음과 같다.
<주의사항>
- IPv6를 사용하고 있을 경우 ip형식이 다음과 같이 처리가 되기 때문에 허용주소를 "127.0.0.1"로 한 경우 403오류가 발생한다.
- IPv6로 주소를 사용하거나 JVM 기동시 -Djava.net.preferIPv4Stack=true 옵션을 사용하면 된다.
<참조>
https://namu.wiki/w/%EC%84%9C%EB%B8%8C%EB%84%B7%20%EB%A7%88%EC%8A%A4%ED%81%AC
https://ko.wikipedia.org/wiki/IPv6