Table of Contents
Spring Boot ACL
목표
인증키를 발급하고, 발급된 인증키를 기반으로 API Access 를 허용한다.
// $ curl -H "X-Authorization: $some_secret_token" http://localhost/user
$.ajax({
url: 'http://localhost/user',
headers: { 'X-Authorization': '$some_secret_token' }
});
인증 데이타 생성
create table tbl_api_user(
id bigint(20) unsigned not null auto_increment,
primary key (id),
unique key unique_id (id),
access_domain varchar(255) NOT NULL,
access_token VARCHAR(100) NOT NULL,
access_expire varchar(10),
modify_time timestamp not null default current_timestamp on update current_timestamp,
insert_time timestamp not null default current_timestamp
);
INSERT INTO tbl_api_user(access_domain, access_token, access_expire)
VALUES('http://localhost:8080', LEFT(REPLACE(UUID(), '-', ''), 50), '2099-12-31');
SELECT * FROM tbl_api_user;
CORS 설정
public class CorsFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equals(request.getMethod())) {
// for CORS preflight channel
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
}
RSA 키 인증 설정
public class RsaAuthToken extends AbstractAuthenticationToken {
private Integer principal;
private Object credentials;
private final String accessToken;
private final String remoteHost;
public RsaAuthToken(String accessToken, String remoteHost) {
super(null);
this.accessToken = accessToken;
this.remoteHost = remoteHost;
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return principal;
}
public void setPrincipal(Integer principal) {
this.principal = principal;
}
public String getAccessToken() {
return accessToken;
}
public String getRemoteHost() {
return remoteHost;
}
}
public class RsaHeaderKeyFilter extends OncePerRequestFilter {
public RsaHeaderKeyFilter() {
Logger log = LoggerFactory.getLogger(RsaHeaderKeyFilter.class);
log.info("RsaHeaderKeyFilter init");
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String accessToken = request.getHeader("X-Authorization");
String referer = request.getHeader("referer"); // TODO : referer hijacking 은 나중에 생각하자.
if ("localhost".equals(request.getServerName())) {
referer = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort();
}
if (accessToken != null && referer != null) {
Authentication auth = new RsaAuthToken(accessToken, referer);
SecurityContextHolder.getContext().setAuthentication(auth);
System.out.println("xAuth : " + accessToken);
System.out.println("referer : " + referer);
}
filterChain.doFilter(request, response);
}
}
Spring Security 설정
@Component
public class TokenAuthenticationProvider implements AuthenticationProvider {
// Service 를 주입받고, 서비스에 Spring Boot Cache 를 적용하여 부하를 줄일 수 있다.
private final ApiUserRepository userRepository;
@Autowired
public TokenAuthenticationProvider(ApiUserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RsaAuthToken rsaAuthToken = (RsaAuthToken) authentication;
List<ApiUser> userList = userRepository.findByAccessToken(rsaAuthToken.getAccessToken());
if(userList.isEmpty()){
throw new UnknownUserException("Invalid access token : " + rsaAuthToken.getAccessToken());
}
for (ApiUser user : userList) {
if (rsaAuthToken.getRemoteHost().startsWith(user.getAccessDomain())) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
if (user.getAccessExpire().compareTo(LocalDate.now().format(formatter)) > 0) {
rsaAuthToken.setPrincipal(user.getId());
return rsaAuthToken;
}
}
}
throw new UnknownUserException("Access denied : " + rsaAuthToken.getRemoteHost());
}
@Override
public boolean supports(Class<?> authentication) {
return RsaAuthToken.class.isAssignableFrom(authentication);
}
}
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final TokenAuthenticationProvider authenticationProvider;
public WebSecurityConfig(TokenAuthenticationProvider authenticationProvider) {
this.authenticationProvider = authenticationProvider;
}
@Bean
CorsFilter corsFilter() {
return new CorsFilter();
}
@Bean
RsaHeaderKeyFilter rsaHeaderKeyFilter() {
return new RsaHeaderKeyFilter();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// curl -v "http://localhost:8080/health/index.html"
// curl -v http://localhost:8080/v1/esrestaurant/?q=%EA%B9%80%EC%B9%98
// curl -v -H "X-Authorization: XXXXXXXXXXXXXXXXXXXXX" "http://localhost:8080/v1/esrestaurant/?q=%EA%B9%80%EC%B9%98%EC%B0%8C%EA%B0%9C&s=10&p=0&las=35.48608721246216&lae=38.49363416030283&los=125.93452195073536&loe=127.94537104338305"
// curl -v -H "X-Authorization: XXXXXXXXXXXXXXXXXXXXX" "https://nb.skyer9.pe.kr:28080/v1/esrestaurant/?q=%EA%B9%80%EC%B9%98%EC%B0%8C%EA%B0%9C&s=10&p=0&las=35.48608721246216&lae=38.49363416030283&los=125.93452195073536&loe=127.94537104338305"
http
.addFilterBefore(corsFilter(), SessionManagementFilter.class)
.addFilterBefore(rsaHeaderKeyFilter(), BasicAuthenticationFilter.class)
.authorizeRequests().antMatchers("/health/**").permitAll()
.anyRequest().authenticated();
}
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(authenticationProvider);
}
}