본문으로 바로가기

💡 스프링 시큐리티 시작하기(기본세팅)

1.  dependency 추가

<Maven 이용> 

<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>4.2.2.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>4.2.2.RELEASE</version>
</dependency>
</dependencies>

 

<Gradle 이용>

dependencies {
	compile 'org.springframework.security:spring-security-web:4.2.2.RELEASE'
	compile 'org.springframework.security:spring-security-config:4.2.2.RELEASE'
}

버전은 사용하는 프로젝트에 맞게 바꿔주시면 됩니다.

2. Java Configuration

WebSecurityConfigurerAdapter를 상속받은 config 클래스에 @EnableWebSecurity 어노테이션을 달면SpringSecurityFilterChain이 자동으로 포함됩니다.

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

작성한 뒤, configure 를 오버라이딩 하여 접근 권한을 작성합니다.

@EnableWebSecurity  //스프링시큐리티 사용을 위한 어노테이션선언

public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{
//WebSecurityConfigurerAdapter 상속
    
    private AuthenticationProvider authenticationProvider;
    
    public SpringSecurityConfig(/*UserDetailsService userDetailsService, 
                                PasswordEncoder passwordEncoder,*/
                                AuthenticationProvider authenticationProvider) {

        this.authenticationProvider = authenticationProvider;
    }
    
    /*
     * 스프링 시큐리티가 사용자를 인증하는 방법이 담긴 객체.
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        /*
         * AuthenticationProvider 구현체
         */
        auth.authenticationProvider(authenticationProvider);
//        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }
    
    /*
     * 스프링 시큐리티 룰을 무시하게 하는 Url 규칙(여기 등록하면 규칙 적용하지 않음)
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring()
            .antMatchers("/resources/**")
            .antMatchers("/css/**")
            .antMatchers("/vendor/**")
            .antMatchers("/js/**")
            .antMatchers("/favicon*/**")
            .antMatchers("/img/**")
        ;
    }
    
    /*
     * 스프링 시큐리티 규칙
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()//보호된 리소스 URI에 접근할 수 있는 권한을 설정
            .antMatchers("/login*/**").permitAll() //전체 접근 허용
            .antMatchers("/logout/**").permitAll()
            .antMatchers("/myPage").hasRole("ADMIN")//admin이라는 롤을 가진 사용자만 접근 허용
            .antMatchers("/chatbot/**").permitAll()
            .anyRequest().authenticated()
        .and().logout()
              .logoutUrl("/logout")
              .logoutSuccessHandler(logoutSuccessHandler())
        .and().csrf()//csrf 보안 설정을 비활성화
              .disable()//해당 기능을 사용하기 위해서는 프론트단에서 csrf토큰값 보내줘야함
        .addFilter(jwtAuthenticationFilter())//Form Login에 사용되는 custom AuthenticationFilter 구현체를 등록
        .addFilter(jwtAuthorizationFilter())//Header 인증에 사용되는 BasicAuthenticationFilter 구현체를 등록
        .exceptionHandling()
              .accessDeniedHandler(accessDeniedHandler())
              .authenticationEntryPoint(authenticationEntryPoint())
        ;
    }
    
    /*
     * SuccessHandler bean register
     */
    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        CustomAuthenticationSuccessHandler successHandler = new CustomAuthenticationSuccessHandler();
        successHandler.setDefaultTargetUrl("/index");
        return successHandler;
    }
    
    /*
     * FailureHandler bean register
     */
    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
        CustomAuthenticationFailureHandler failureHandler = new CustomAuthenticationFailureHandler();
        failureHandler.setDefaultFailureUrl("/loginPage?error=error");
        return failureHandler;
    }
    
    /*
     * LogoutSuccessHandler bean register
     */
    @Bean
    public LogoutSuccessHandler logoutSuccessHandler() {
        CustomLogoutSuccessHandler logoutSuccessHandler = new CustomLogoutSuccessHandler();
        logoutSuccessHandler.setDefaultTargetUrl("/loginPage?logout=logout");
        return logoutSuccessHandler;
    }
    
    /*
     * AccessDeniedHandler bean register
     */
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        CustomAccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
        accessDeniedHandler.setErrorPage("/error/403");
        return accessDeniedHandler;
    }
    
    /*
     * AuthenticationEntryPoint bean register
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return new CustomAuthenticationEntryPoint("/loginPage?error=e");
    }
    
    /*
     * Form Login시 걸리는 Filter bean register
     */
    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
        jwtAuthenticationFilter.setFilterProcessesUrl("/login");
        jwtAuthenticationFilter.setUsernameParameter("username");
        jwtAuthenticationFilter.setPasswordParameter("password");
        
        jwtAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        jwtAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
        
        jwtAuthenticationFilter.afterPropertiesSet();
        
        return jwtAuthenticationFilter;
    }
    
    /*
     * Filter bean register
     */
    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() throws Exception {
        JwtAuthorizationFilter jwtAuthorizationFilter = new JwtAuthorizationFilter(authenticationManager());
        return jwtAuthorizationFilter;
    }
    @Bean
	public PasswordEncoder passwordEncoder() {//간단하게 비밀번호 암호화
		return new BCryptPasswordEncoder(); 
	}
}

여기 사용된 BCryptPasswordEncoder 암호화에 대해서는 추가로 포스팅하도록 하겠습니다.

 

📌 다음은 antMatchers() 로 지정할 수 있는 항목들입니다.

  • hasRole() or hasAnyRole()
    특정 권한을 가지는 사용자만 접근할 수 있습니다.
  • hasAuthority() or hasAnyAuthority()
    특정 권한을 가지는 사용자만 접근할 수 있습니다.
  • hasIpAddress()
    특정 아이피 주소를 가지는 사용자만 접근할 수 있습니다.
  • permitAll() or denyAll()
    접근을 전부 허용하거나 제한합니다.
  • rememberMe()
    리멤버 기능을 통해 로그인한 사용자만 접근할 수 있습니다.
  • anonymous()
    인증되지 않은 사용자가 접근할 수 있습니다.
  • authenticated()
    인증된 사용자만 접근할 수 있습니다.

Role은 역할이고 Authority는 권한이지만 사실은 표현의 차이입니다. 
Role은 “ADMIN”으로 표현하고 Authority는 “ROLE_ADMIN”으로 표기합니다.

3.UserDetails 구현

user detail 을 구현할 때는 UserDetails 를 Implements 해줍니다.

1 @Slf4j
2 @Service
3 public class CustomUserDetailService implements AuthenticationUserDetailsService<Authentication> {
4
5    @Override
6    public UserDetails loadUserDetails(Authentication token) {
7        User user = (User) token.getPrincipal();
8
9        if (user == null) {
10            throw new PreAuthenticatedCredentialsNotFoundException("USER IS NULL");
11        }
12
13                // DB에 접근해서 직접 정보를 가져오는게 일반적입니다.
14    
15        return new CustomUserDetails().setUser(user).setGrantedAuthorities(user.getAuthorities());
16    }
17
18 }

서비스를 구현할 때는 AuthenticationUserDetailsService 를 implement합니다.

4. AuthenticationSuccessHandler 구현

Form Login(AuthenticationFilter)에서 인증이 성공했을 때 수행될 핸들러

SimpleUrlAuthenticationSuccessHandler를 상속받아 클래스를 구현합니다.

추상클래스가 아니라도 인터페이스형태로도 가능합니다.

성공시 인증토큰을 쿠키에 넣어주거나, index 페이지로 리다이렉트 하는 역할을 수행합니다.

@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler implements ExceptionProcessor{
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws ServletException, IOException {
        log.debug("CustomAuthenticationSuccessHandler.onAuthenticationSuccess ::::");
        /*
         * 쿠키에 인증 토큰을 넣어준다.
         */
        super.onAuthenticationSuccess(request, response, authentication);
    }
 
    @Override
    public void makeExceptionResponse(HttpServletRequest request, HttpServletResponse response,
            Exception exception) {
    }
    
}

5. AuthenticationFailureHandler 구현

Form Login 실패시 수행되는 핸들러

SimpleUrlAuthenticationFailureHandler를 상속받으며, 실패시 로그인 페이지로 리다이렉트 하는 역할을 수행합니다.

@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler implements ExceptionProcessor{
    
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        log.debug("CustomAuthenticationFailureHandler.onAuthenticationFailure ::::");
        super.onAuthenticationFailure(request, response, exception);
    }
 
    @Override
    public void makeExceptionResponse(HttpServletRequest request, HttpServletResponse response,
            Exception exception) {
    }
    
}

6. LogoutSuccessHandler 구현

로그아웃에 성공했을 시 수행되는 핸들러

SimpleUrlLogoutSuccessHandler를 상속하며,로그아웃 성공시 로그인 페이지로 리다이렉트 하는 역할을 수행합니다.

@Slf4j
public class CustomLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler{
    
    @Override    
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        log.debug("CustomLogoutSuccessHandler.onLogoutSuccess ::::");
        super.onLogoutSuccess(request, response, authentication);
    }
    
}

 

7. JwtAuthenticationFilter 구현

Form Login시 걸리는 Filter

UsernamePasswordAuthenticationFilter를 상속하며, 사용자가 Form으로 입력한 로그인 정보를 인터셉트해서 AuthenticationManager에게 Authentication 객체를 넘겨줍니다.

@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter{
    
    private boolean postOnly = true;
    
    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }
    
    /*
     * 해당 필터에서 인증 프로세스 이전에 요청에서 사용자 정보를 가져와서
     * Authentication 객체를 인증 프로세스 객체에게 전달하는 역할
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        log.debug("JwtAuthentication.attemptAuthentication ::::");
        
        /*
         * POST로 넘어왔는지 체크
         */
        if(postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        
        if(StringUtils.isEmpty(username)) {
            username = "";
        }
        if(StringUtils.isEmpty(password)) {
            password = "";
        }
        
        username = username.trim();
        
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        
        setDetails(request, authRequest);
        
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
    
}

 

스프링 시큐리티는 사용자를 인증하고, 로그인 후 애플리케이션의 각 기능들에 대한 권한을 부여하는 기능을 구현하는데 사용되는 프레임워크로, 각 핸들러, 필터들을 거쳐 동작한다는 것이 포인트입니다. 스프링시큐리티에는 많은 클래스를 상속받고 복잡하게 구성되어 있어, 도식화된 그림을 보면 전체적인 구조나 흐름을 이해하는데 도움이 될 것 같습니다. 

 

출처: https://devuna.tistory.com/59 [튜나 개발일기:티스토리]