몰입하며 나아가는 개발이란

Framework

Spring Security

류하을 2022. 4. 4. 10:00

Spring Security

스프링 시큐리티(Spring Security)란?

스프링 시큐리티는 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크 입니다. 스프링 시큐리티에서는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 된다는 장점이 있습니다.

필터(Filter)란?

Http 요청과 응답을 변경할 수 있는 재사용 가능한 코드이며, 서블릿 2.3규약에서 새롭게 추가되었습니다. 필터는 객체의 형태로 존재하며 클라이언트로부터 오는 요청과 최종자원(서블릿/JSP/기타) 사이에 위치하여 클라이언트의 요청 정보를 알맞게 변경할 수 있으며, 또한 필터는 최종 자원과 클라이언트로 가는 응답 사이에 위치하여 최종 자원의 요청 결과를 알맞게 변경 할 수 있습니다.filter

시큐리티 필터체인(SecurityFilterChain)이 란?

스프링시큐리티에서 웹 애플리 케이션에 주로 영향을 주는 방식은 ServeltRequest 필터를 사용하는 방식이며, 필터들이 애플리케이션에 대한 모든 요청을 감싸서 처리합니다. 스프링 시큐리티에서 여러개의 필터들은 아래 그림과 같이 체인 형태를 이루면서 동작합니다.

spring-security filter
필터 설명
SecurityContectPersistenceFilter SecurityContexRepository에서 SecurityContext를 로드하고 저장하는 일을 담당함.
LogoutFilter 로그아웃 URL로 지정된 가상URL에 대한 요청을 감시하고 매칭되는 요청이 있으면 사용자를 로그아웃시킴.
UsenamePasswordAuthenticationFilter 사용자명과 비밀번호로 이뤄진 폼 기반 인증에 사용하는 가상URL요청을 감시하고 요청이 있으면 사용자의 인증을 진행함.
DefaultLoginPageGeneratingFilter 폼기반 또는 OpenID 기반 인증에 사용하는 가상URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML을 생성함.
BasicAuthenticationFilter HTTP 기본인증 헤더를 감시하고 이를 처리함.
RequestCacheAwareFilter 로그인 성공 이후 인증 요청에 의해 가로채어진 사용자의 원래 요청을 재구성하는데 사용됨.
SecurityContextHolderAwareRequestFilter HttpServeltRequest를 HttpServletRequestWrapper를 상속하는 하위 클래스로 감싸서 필터 체인상 하단에 위치한 요청 프로세서에 추가 컨텍스트를 제공함.
AnonymousAuthenticationFilter 이 필터가 호출되는 시점까지 사용자가 아직 인증을 받지 못했다면 요청 관련 인증 토큰에서 사용자가 익명 사용자로 나타나게됨.
SessionManagementFilter 인증된 주체를 바탕으로 세션 트래킹을 처리해 단일 주체와 관련한 모든 세션들이 트래킹되도록 도움.
ExceptionTranslationFilter 이 필터는 보호된 요청을 처리하는 동안 발생할 수 있는 기대한 예외의 기본 라우팅과 위임을 처리함.
FilterSecurityInterceptor 이 필터는 권한부여와 관련한 결정을 AccessDecisionManager에게 위임해 권한부여 결정 및 접근 제어 결정을 쉽게 만들어 줌.

Spring Boot에서 Security 적용

pom.xml 추가
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>
SecurityCongif.java 생성 및 설정

WebSecurityConfigurerAdapter를 상속받아 @override protected void configure(HttpSecurity http) 보안설정 method 작성

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // http 접근 권한 및 로그인 로그아웃 설정
    @Override
    protected void configure(HttpSecurity http) {
        http.authorizeRequests().antMatchers("/**").permitAll();
    }
    // 사용자 인증 설정
    @Override protected void configure(AuthenticationManagerBuilder auth){
        try {
         auth.inMemoryAuthentication().withUser("admin").password("admin").roles("ADMIN"); 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    // 리소스 관련 security 룰을 적용시키지 않는 url 설정
    @Override public void configure(WebSecurity web){
        try {
            web.ignoring()
               .antMatchers("/resources/**")
               .antMatchers("/css/**")
               .antMatchers("/js/**")
               .antMatchers("/favicon*/**")
               .antMatchers("/img/**");  
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
protected void configure(HttpSecurity http){} 내
http 접근 권한 설정 Method
http.authorizeRequests()
    .antMatchers("/member/**").authenticated()
    .antMatchers("/admin/**").authenticated()
    .antMatchers("/**").permitAll();
Method
authorizeRequests() HttpServletRequest 요청 URL에 따라 접근 권한을 설정합니다.
antMatchers("pathPattern") 요청 URL 경로 패턴을 지정합니다.
authenticated() 인증된 유저만 접근을 허용합니다.
hasAuthority() or hasAnyAuthority() 특정 권한을 가지는 사용자만 접근할 수 있습니다.
permitAll() 모든 유저에게 접근을 허용합니다.
anonymous() 인증되지 않은 유저만 허용합니다.
denyAll() 모든 유저에 대해 접근을 허용하지 않습니다.
http 로그인 설정 Method
http.formLogin()
    .loginPage("/login")
    .defaultSuccessUrl("/")
    .permitAll();
Method
formLogin() form Login 설정을 진행합니다.
loginPage("path") 커스텀 로그인 페이지 경로와 인증 경로를 등록합니다.
defaultSeuccessUrl("path") 로그인 인증을 성공하면 이동하는 페이지를 등록합니다.
successHandler(AuthenticationSuccessHandler) 로그인 인증을 성공하면 실행할 로직 객체로 상속받아 설정합니다.
http 로그아웃 설정 Method
http.logout()
    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
    .logoutSuccessUrl("/login")
    .invalidateHttpSession(true);
Method
logout() 로그아웃 설정을 진행합니다.
logoutRequestMatcher(new AntPathRequestMatcher("path")) 로그아웃 경로를 지정합니다.
logoutSuccessUrl("path") 로그아웃 성공 시 이동할 경로를 지정합니다.
invalidateHttpSession(true) 로그아웃 성공 시 세션을 제거합니다.

http 접근권한 동적 설정(DB활용)

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    AdminService adminService;

    @Override
    protected void configure(HttpSecurity http) {
        try {
            setAntMachers(http);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    protected void setAntMachers(HttpSecurity http) throws Exception {
        // DB 테이블에서 url 과 role 을 가져옴
        List<SecurityUrl> list = adminService.getAuthPath();
        for (SecurityUrl one : list) {
            http.authorizeRequests().antMatchers(one.getMenu_url())
                .hasAuthority(one.getAuth_type());
        }
    }
}

auth 사용자인증 동적설정(DB활용)

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // UserDetailsService를 구현한 Service 클래스
    @Autowired
    AdminService adminService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) {
        try {
            auth.authenticationProvider(authenticationProvider(adminService));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //
    @Bean
    public DaoAuthenticationProvider authenticationProvider(AdminService adminService){
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(adminService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    // 암호화 설정
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}
@Service
public class AdminService implements UserDetailsService{

    @Autowired
    AdminDao adminDao;

    // id 를 받아 UserDetails를 구현한 클래스 반환
    @Override
    public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException{

        // 프로젝트별 DB테이블 구조에 맞는 로직 적용
        AdminUser user = adminDao.ckeckAdminMember(id);

        String codeString = adminDao.getAuthCodeList(user.getGrade_seq());
        List<String> auth_code_list = new ArrayList<>();
        if(codeString !=null){
            auth_code_list = Arrays.asList(codeString.split(","));
        }
        Map<String, Object> maps = new HashMap<>();
        maps.put("auth_code_list", auth_code_list);
        List<Map<String, Object>> authList = adminDao.getMenuList(maps);

        ArrayList<AdminUser> userAuthes = new ArrayList<>();
        for (Map<String,Object> one : authList) {
            AdminUser adminUser = new AdminUser();
            adminUser.setAdmin_id(user.getAdmin_id());
            adminUser.setAdmin_pass(user.getAdmin_pass());
            adminUser.setRole((String)one.get("AUTH_TYPE"));
            adminUser.setAdmin_status(user.getAdmin_status());
            userAuthes.add(adminUser);
        }

        if(userAuthes.size() == 0) {
            throw new UsernameNotFoundException(id+" 회원을 찾을 수 없습니다.");
        }

        return new UserDetailsVo(userAuthes);
    }

}
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

// 프로젝트별 DB테이블 구조에 맞는 로직 적용
public class UserDetailsVo implements UserDetails {

    private static final long serialVersionUID = 1L;
    private ArrayList<AdminUser> user;

    public UserDetailsVo(ArrayList<AdminUser> userAuthes){
        this.user = userAuthes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> authorities = new ArrayList<>();

        for (int i = 0; i < user.size(); i++) {
            authorities.add(new SimpleGrantedAuthority(user.get(i).getRole()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.get(0).getAdmin_pass();
    }

    @Override
    public String getUsername() {
        return user.get(0).getAdmin_id();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return user.get(0).getAdmin_status().equals("Y");
    }

}

마치며

spring security의 개념을 설명하고 실제 적용된 코드를 기반으로 간단한 적용방법을 서술하였다. 커스텀 login 페이지 적용, 로그인 성공시 실행, 로그아웃 성공시 실행, 로그인 실패시 에러 페이지 등등의 설정들을 더 작성이 가능하므로, 각 프로젝트 성격 및 기획에 맞추어 필요한 기능들은 추가로 구현하여야한다. security만 놓고봐도 알아야할 calss 인증 방식 각각의 설정 등 이 많이 있으므로 가볍게 적용이 가능하지만 깊이는 조금 있는 편이므로 충분히 더 알아봐야할 것이다.

'Framework' 카테고리의 다른 글

JNA(Java Native Access)란?  (0) 2022.03.31
Javadoc 작성방법  (0) 2022.03.30
Logback 이란?  (0) 2022.03.29
Javadoc이란? Javadoc 사용방법  (0) 2022.03.28