Spring Boot ACL

By | 2021년 10월 6일
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);
    }
}

답글 남기기