스프링 시큐리티 공부

사실 프로젝트를 계속하면서 시큐리티를 해야지 해야지 했는데… 제대로 하질 못했던것 같아서 이번에 제대로 하려고 정리하려한다.

그래서 스프링 시큐리티 인 액션 이라는 책의 예제를 바탕으로 까먹지 않도록 계속해서 핵심 내용만 정리하려고 한다.

책을 다 때기까지 화이팅이다!!!

cURL

  • 우리는 HTTP 테스트를 postman등으로 하는 경우도 있지만, 리눅스 혹은 mac 환경이라면 curl 을 통해서도 확인을 할 수 있다.
  • 예제에서 현재는 HelloController 에 다음과 같이 코드를 짠경우입니다.
    • 의존성으로는, Spring-web과 Spring-security를 넣었습니다.
package com.example.example1;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello!";
    }
}
  • 이후 실행하고 확인을 하려고한다면 다음과 같은 명령어로 확인이 가능합니다.
    • curl -v http://localhost:8080/hello

  • 확인을 해보면 401 이 반환이 된것을 알 수 있습니다. 이는 Unauthorized 입니다.
  • 이후 스프링에서 나온 값과 함게 -u attribute를 통해서 접근을 한다면 인증이 됩니다.

  • -u 를 사용한다면, <username>:<password> 문자열을 Base64로 인코딩을 하고 Basic이 붙은 Authorization 헤더의 값으로 보냅니다.

스프링 시큐리티의 인증 프로세스

  • 우리는 다음 그림을 통해서 다음과 같은 사실을 알 수 있습니다.
    • 인증 필터는 인증 요청을 인증 관리자에게 위임하고 응답을 바탕으로 보안 컨텍스트를 구성합니다/
    • 인증 관리자는 인증 공급자를 이용해 인증을 처리합니다
    • 인증 공급자는 인증 논리를 구현합니다
    • 인증 공급자는 사용자 관리 책임을 구현하는 사용자 세부 정보 서비스를 인증 논리에 이용합니다.
    • 인증 공급자는 암호 관리를 구현하는 암호 인코더를 인증 논리에 이용합니다.
    • 보안 컨텍스트는 인증 프로세스 후 인증 데이터를 유지합니다.

→ 우리는 위오아 같은 예시에서 자동으로 구성되는 UserDetailService , PasswordEncoder 라는 빈이 생깁니다.

PasswordEncoder

  • 이러한 passwordEncoder 는 다음과 같은 일을 합니다.
    • 암호를 인코딩합니다.
    • 암호가 기존 인코딩과 일치하는지를 확인합니다.
  • 이때 AuthenticationProvider 는 인증 논리를 정의하고 사용자와 암호의 관리를 위임합니다.
    • 이는 UserDetailsService, PsswordEncoder 에 제공된 기본 구현을 이용해서 구현이 되어있습니다.

기본 구성 재정의

  • 이제 기본적으로 구성되어있던, UserDetailServicePasswordEncoder 에 대해서 재정의를 해서 새로운 방법으로 구성하는 방법을 공부할것 입니다.

UserDetailsService 재정의

  • 이를 재정의 하기 위해서 우선 InMemoryUserDetailManager을 이용해서 구현을 해봅니다.
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig {
    @Bean
    public UserDetailsService userDetailsService() {
        var userDetailsService = new InMemoryUserDetailsManager();

        var user = User.withUsername("haechan")
                .password("12345")
                .authorities("read") // 권한 목록
                .build();

        userDetailsService.createUser(user);

        return userDetailsService;
    }
}
  • 우리는 새로운 haechan이라는 이름을 가진 유저를 read 권한을 가지도록 생성을 했습니다.
    • 이후 createUser 를 통해서 유저를 생성합니다.
    • 이대로만 실행하면 PasswordEncoder 가 비어있으므로, 이도 재정의 해야합니다
     

@Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
  • 이때 사용하는 NoOpPasswordEncoder 는 그냥 암호화하지 않고 기본적인 텍스트로 문자열비교만하는것입니다.

  • 이제 값이 제대로 나오게 됩니다.

엔드포인트 권한 부여로 구성 재정의

  • 앞에서는 user를 생성하고 password를 encoding을 해서 인증을 통과하는 방법을 배웠습니다.
  • 이번에는 엔드포인트 권한을 통해서 어떤 엔드포인트에 접근할 때 인증을 허용하는 방법을 배워보겠습니다.
  • 우리는 이를 통해 WebSecurityConfigureAdapter 클래스를 확장해서 configure(HttpSecurity http) 메서드를 재정의해 권한을 허용시킨다
    • 하지만 현재는 @Deprecated 되어있는 것을 알 수 있다.

  • 이를 해결하기 위해서는 이제, SecurityFilterChain을 Bean으로 등록해서 사용을 하도록 변경이 되었습니다.
@Configuration
public class ProjectConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .httpBasic(Customizer.withDefaults())
                .authorizeRequests((authz) -> authz
                        .anyRequest().permitAll());
        return http.build();
    }
}

 

  • 이후 실행을 하면, permitAll() 을 해놓았기 때문에 어떤 요청에도 다음과 같이 자격증명없이 모든 엔드포인트를 접근할 수 있는 것을 볼 수 있습니다.

다른 방법으로 구성을 설정하는 법

  • 우리는 위에서 UserDetailsServicePsswordEncoder를 재정의 했었는데, 이에 대해서 ㄱ동일한 구성을 수행하는 다른 방법도 존재합니다.
  • 두 객체를 빈으로 정의하지 않고 원래는 WebSecurityConfigurerAdapter 클래스를 상속받아 configure(AuthenticationManagerBuilder 로 설정이 가능합니다.
    • 하지만, 현재는 Deprecate 되어있는 상황이라 다음과 같은 코드를 다믕ㅁ으로 변경을 합니다.
/*** 수정 전 **/
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter{
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
       var userDetailsService = new InMemoryUserDetailsManager(); // 사용자를 메모리에 저장하기 위한 방식

        var user = User.withUsername("haechan")
                .password("12345")
                .authorities("read")
                .build();

        userDetailsService.createUser(user); // 사용자를 추가함

        auth.userDetailsService(userDetailsService) // configure 메서드에서 UserdetailsService와 PasswordEncoder를 설정
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

}
/** 수정 후 **/
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class ProjectConfig {
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        var user = User.withUsername("haechan")
                .password("{noop}12345")
                .authorities("read")
                .build();

        return new InMemoryUserDetailsManager(user);
    }
}
  • 다음 링크를 참고하면, 변경된 사항이 존재한다.
    • 비밀번호는 {noop} 을 넣어서 작성하면 NoOpPasswordEncoder로 사용이 가능합니다.
/** 최종 코드 **/ 
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class ProjectConfig {
    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        var user = User.withUsername("haechan")
                .password("{noop}12345")
                .authorities("read")
                .build();

        return new InMemoryUserDetailsManager(user);
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests((authz) -> authz.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}
  • 우리는 현재 인-메모리 방식으로 사용자를 구성하였지만, 보통은 데이터베이스에 저장하거나 다른 시스템을 가져와야합니다.
    • 결론적으로 이 방법은 권장하지 않습니다

AuthenticationProvider 구현 재정의

  • 이번은 이 그림에서 인증 관리자를 재정의해서 이번에는 구현해보려고합니다.
  • 우리가 재정의할 AuthenticationProvider 는 인증 논리를 구현하며, AuthenticationManager 에서 요청을 받고 사용자를 찾는 것을 UserDetailsService 에서 암호검증을 PasswordEncoder에 위임을합니다.
package com.example.example1;

import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = String.valueOf(authentication.getCredentials());

        if ("haechan".equals(username) && "12345".equals(password)) {
            return new UsernamePasswordAuthenticationToken(username, password, Arrays.asList());
        }
        throw new AuthenticationCredentialsNotFoundException("authentication에서 오류가 발생했습니다");
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class
                .isAssignableFrom(authentication);
    }
} 
  • 우리는 다음과 같이, “haechan”, “12345”를 username과 password로 받으면, 새로운 UsernamePasswordAuthenticationToken을 생성하고 반환을 합니다.
  • 이후 커스텀하는 Configuration은 다음과 같이 작성을 합니다. (스프링 시큐리티 5부터 변경되었습니다.)
package com.example.example1;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;

@Configuration
@EnableWebSecurity
public class ProjectConfig {

    @Autowired
    private CustomAuthenticationProvider authenticationProvider;

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder = 
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder.authenticationProvider(authenticationProvider);
        return authenticationManagerBuilder.build();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests((authz) -> authz.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}
  • 이 또한, 제대로 되는 것을 볼 수 있습니다.

프로젝트에 여러 구성 클래스 이용

  • 우리는 지금까지 하나의 구성 클래스만 사용을 했지만, 구성 클래스도 책임을 분리하는 것이 좋습니다.
  • 이제 UserManagementConfigWebAuthorizationConfig 로 나누어서 환경을 구성할 예정입니다
    • UserManagementConfigUserDetailsServicePasswordEncoder 의 Bean만 포함을 합니다.
    • 이는, WebSecurityConfigureAdapter를 확장할 수 있기 때문에 두 객체를 빈으로 구성을 했습니다.
      • (현재는 WebSecurityConfigureAdapater 를 상속받지 않습니다. Security 5부터)
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class UserManagementConfig {
    @Bean
    public InMemoryUserDetailsManager userDetailsManager() {
        var user = User.withUsername("haechan")
                .password("{noop}12345")
                .authorities("read")
                .build();
        return new InMemoryUserDetailsManager(user);
    }
}
package com.example.example1;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebAuthorizationConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests((authz) -> authz.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }
}

Reference

  • 스프링 시큐리티 인 액션 - 로렌티우 스필카

+ Recent posts