Miracle Morning, LHWN

19. Spring Security 를 활용하여 OpenStack 연동해보기 본문

IT 기술/[JAVA] Spring Boot

19. Spring Security 를 활용하여 OpenStack 연동해보기

Lee Hye Won 2021. 6. 2. 09:38

# 먼저 Spring Web 설정을 한다.

 

// configure/WebMvcConfiguration.java

package com.spring.openstack.configure;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        registry.addResourceHandler("/css/**").addResourceLocations("classpath:/static/css");
        registry.addResourceHandler("/js/**").addResourceLocations("classpath:/static/js/");
        registry.addResourceHandler("/images/**").addResourceLocations("classpath:/static/images/");
    }
}

/static/** 패턴으로 요청이 들어오면 자동으로 Resource 하위의 /static/ 으로 매핑시킨다. Build 가 완료되면 Resource 폴더는 Root 하위 폴더에 위치한다.

(※ 여기서 classpath:/ 는 Resource/ 경로와 매핑된다.)

 

// configure/WebMvcConfiguration.java

package com.spring.openstack.configure;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableGlobalMethodSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
                .antMatchers("/i18n/**")
                .antMatchers("/static/**")
                .antMatchers("/css/**")
                .antMatchers("/js/**")
                .antMatchers("/images/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.headers().frameOptions().sameOrigin()
                .and().formLogin().loginPage("/login")
                .and().logout().logoutUrl("/logout")
                .and().authorizeRequests().antMatchers("/login", "/").permitAll()
                .and().authorizeRequests().anyRequest().authenticated();
        http.csrf().disable();
    }
}

 

 

web.ignoring() 을 통해 인증을 하지 않고도 접근 가능하도록 설정해준다.

 

/login 경로로 접근하면 로그인 페이지가 나오도록 설정해주고,

/login 경로와 / (Root) 경로는 모두 접근 가능하도록 설정한다. 그 외의 모든 Request (anyRequest()) 는 인증을 해야한다.

 

다음은 Filter 구현이다.

 

전체적인 Spring Security 구조에서 Filter, Manager, Provider 는 인증에 대한 것만 구현하고,

인증에 성공/실패했을 경우에 대한 로직은 Handler 에서 처리하는 형태를 권장한다.

 

// OpenStackFilter.java

package com.spring.openstack.configure.filters;

import org.openstack4j.api.exceptions.ClientResponseException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class OpenStackFilter extends UsernamePasswordAuthenticationFilter {
    private boolean postOnly = true;
    private SessionAuthenticationStrategy sessionAuthenticationStrategy = new NullAuthenticatedSessionStrategy();
    private boolean continueChainBeforeSuccessfulAuthentication = false;

    public String obtainDomain(HttpServletRequest httpServletRequest) { return (String)httpServletRequest.getParameter("domain"); }

    public OpenStackFilter() {
    }

    public OpenStackFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    @Override
    public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionAuthenticationStrategy) {
        this.sessionAuthenticationStrategy = sessionAuthenticationStrategy;
    }

    @Override
    public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
        this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
    }

    // POST 로 넘어온 username, password 를 추출하여 인증을 시도하는 부분이다.
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        String username = obtainUsername(request);
        username = (username != null) ? username : "";
        username = username.trim();

        String password = obtainPassword(request);
        password = (password != null) ? password : "";

        String domain = obtainDomain(request);
        domain = (domain != null) ? domain : "";
        domain = domain.trim();

        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, password);
        usernamePasswordAuthenticationToken.setDetails(domain);

        return this.getAuthenticationManager().authenticate(usernamePasswordAuthenticationToken);
    }

    private boolean checkExpire(String tokenExpire) throws ParseException {
        DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
        Date tokenExpireDateTime = dateFormat.parse(tokenExpire);

        return tokenExpireDateTime.after(new Date()); // 현재 시간과 비교하여 tokenExpireDateTime 이 더 뒤에 있으면 true 이다. 즉, 아직 만료되지 않았을 경우 true 를 반환.
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        // 인증이 필요없으면 doFilter 를 통해 다음 Chain 이 실행되도록 한다.
        if(!requiresAuthentication(httpServletRequest, httpServletResponse)) {
            chain.doFilter(request, response);
            return;
        }

        // Session 을 가져와서 그 안에 있는 token 정보를 가져온다.
        HttpSession httpSession = httpServletRequest.getSession();
        String tokenId = (String)httpSession.getAttribute("unscopedTokenId");
        String tokenExpire = (String)httpSession.getAttribute("tokenExpire");

        // 이미 인증이 된 경우이다.
        if(tokenId != null && tokenExpire != null) {
            try {
                if(checkExpire(tokenExpire)) {
                    //TODO : 인증객체 구현 필요
                    chain.doFilter(httpServletRequest, httpServletResponse);
                } else {
                    unsuccessfulAuthentication(httpServletRequest, httpServletResponse, new AuthenticationServiceException("Token has been expired."));
                }
            } catch (ClientResponseException clientResponseException) {
                unsuccessfulAuthentication(httpServletRequest, httpServletResponse, new AuthenticationServiceException("Token Id is invalidated."));
            } catch (ParseException parseException) {
                unsuccessfulAuthentication(httpServletRequest, httpServletResponse, new AuthenticationServiceException("Token expire date time format invalidated."));
            }
        } else {
            // 아직 인증이 되지 않은 경우이다. (토큰이 없으면 POST 로 넘어온 username, password, domain 으로 인증을 진행한다.)
            if(this.postOnly && httpServletRequest.getMethod().equals("POST")) {
                String username = this.obtainUsername(httpServletRequest);
                String password = this.obtainPassword(httpServletRequest);
                String domain = this.obtainDomain(httpServletRequest);

                if(username != null && password != null && domain != null) {
                    try {
                        Authentication authenticationResult = attemptAuthentication(httpServletRequest, httpServletResponse);
                        if(authenticationResult == null) {
                            return;
                        }

                        this.sessionAuthenticationStrategy.onAuthentication(authenticationResult, httpServletRequest, httpServletResponse);

                        if(this.continueChainBeforeSuccessfulAuthentication) {
                            chain.doFilter(httpServletRequest, httpServletResponse);
                        }
                        successfulAuthentication(httpServletRequest, httpServletResponse, chain, authenticationResult);
                    } catch (InternalAuthenticationServiceException internalAuthenticationServiceException) {
                        unsuccessfulAuthentication(httpServletRequest, httpServletResponse, internalAuthenticationServiceException);
                    } catch (AuthenticationServiceException authenticationServiceException) {
                        unsuccessfulAuthentication(httpServletRequest, httpServletResponse, authenticationServiceException);
                    }
                } else {
                    unsuccessfulAuthentication(httpServletRequest, httpServletResponse, new AuthenticationServiceException("Bye bye."));
                }
            } else {
                unsuccessfulAuthentication(httpServletRequest, httpServletResponse, new AuthenticationServiceException("Bye bye."));
            }
        }
    }
}

 

Comments