Spring Security (1)

박영준·2022년 12월 9일
0

Spring

목록 보기
8/58

1. 정의

  • Spring 기반 애플리케이션의 보안을 담당하는 스프링 하위 Framework
    (스프링 프레임워크가 웹 서버 구현에 편의를 제공해줌)
    → 개발자가 보안 로직을 하나씩 작성하지 않아도 되는 편리성을 제공

  • '인증(Authentication)'과 '인가(Authorization)'에 대한 부분을
    Filter의 흐름에 따라 처리
    → 인증, 인가 처리 로직에 대한 독립적인 운용이 가능

  • 요청이 들어오면 Servlet FilterChain을 자동으로 구성한 후 거치게 한다.

  • (기본적으로) 세션 / 쿠키 방식을 사용
    참고: 서버 인증

  • 일반적인 해킹 공격(CSRF, 세션고정 등...)에 대해 보호

CSRF (Cross-site request forgery, 사이트 간 요청 위조)
정상적인 사용자가 의도치 않은 위조요청을 보내는 것

문제점?
도메인 A 에서, 인증된 사용자 B 가 위조된 request를 포함한 link, email을 사용하였을 경우(클릭, 또는 사이트 방문만으로도),
도메인 A 에서는 사용자 B 가 일반 유저인지, 악용된 공격인지 구분할 수가 없다.

해결책?

<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

spring security에서 CSRF protection 이 default 로 설정되는데,
html에서 csrf 토큰이 포함되어야 요청을 받아들임으로써 위조 요청을 방지하는 방식으로
상태를 변화시킬 수 있는 POST, PUT, DELETE 요청으로부터 보호한다.(GET요청을 제외한)

2. 동작 과정

  • spring security 는 요청이 들어오면 Servlet Filter Chain을 자동으로 구성하고 거치게한다.
    (FilterChain : 여러 filter를 chain형태로 묶어놓은 것)

  • DelegatingFilterProxy -> FilterChainProxy -> SecurityFilterChain(여기에 spring 필터들이 있음)

1) FilterChain

여러 Filter 를 chain 형태로 묶어놓은 것

2) Filter

  • J2EE 표준 스펙 기능

  • Dispatcher Servlet 에 Client 요청이 전달되기 전후에
    URL 패턴에 맞는 모든 요청에 대해 필터링을 할 수 있는 기능을 제공
    → 즉, 스프링 컨테이너에서 제공되는 기능이 아니라 웹 컨테이너(톰캣 같은)에 의해 관리되는 서블릿의 기술

  • 전역적으로 동작해야하는 보안 검사(CSRF, XSS 방어 등)를 통해, 올바른 요청이 아닐 경우 이를 차단한다.
    → 요청이 스프링 컨테이너까지 전달되지 못해, 안정성↑

    따라서,
    Spring Security는 이런한 기능을 활용하기위해, Filter를 사용하여 인증/인가를 구현!

    참고: Servlet (서블릿), Servlet Container

3) DelegatingFilterProxy

  • filter는 Servlet기술이므로, 스프링 컨테이너 밖에서 동작한다. (그래서, 과거에는 스프링으로 filter를 통제할 수 X)
    → 그러나 최근, DelegatingFilterProxy 기술로 spring bean 으로 filter 를 등록하여 사용 가능
    → 즉, DelegatingFilterProxy를 활용하여, bean을 등록을 할 수 있다.

  • FiterChain들을 Servlet Container 기반의 필터 위에서 동작하기 위해 중간 연결을 도움

  • DelegatingFilterProxy 는 내부적으로 요청을 위임할 FilterChainProxy 를 가지고 있다.

4) FilterChainProxy

  • DelegatingFilterProxy에 의해 bean으로 등록됨

  • FilterChainProxy 또한 처리를 위임하기 위한 SecurityFilterChain 을 들고 있다.

  • Spring Security는 FilterChainProxy를 활용해, SecurityFilterChain에 있는 filter들을 사용 가능
    → 즉, proxy를 2번 활용하여, SecurityFilterChain 안에 있는 filter들을 활용할 수 있게 하는 것

5) SecurityFilterChain

(1) 정의

  • FilterChainProxy에서의 요청에 대해 호출해야하는 스프링 보안 Filter를 결정하는데에 사용되는 Filter

  • SecurityFilterChain 하나만 존재하지 않고 여러 개 존재할 수 있다.

    @RequiredArgsConstructor
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final CustomAuthenticationProvider authProvider;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.authenticationProvider(authProvider);		// authenticationProvider 추가해주기
        }
    }
    • 코드를 보면 List 형태인걸 볼 수 있다.
    • 즉, 설정에 따라 필터를 추가하거나 삭제할 수 있는 것
      • 이 설정은 WebSecurityConfigurerAdapter 를 이용하여 쉽게 설정 가능

(2) FilterChainProxy 의 필요성(장점)

DelegatingFilterProxy로 bean을 filter로 등록할 수 있다.
그럼에도, 굳이 FilterChainProxy를 사용해 Proxy를 한 번 더 사용하여 등록을 한다.

  1. 모든 Spring Security의 서블렛 이용에 대한 시작점을 제공
    → 문제가 생기면, FilterChainProxy에 디버깅 포인트를 잡아서 빠르게 오류 수정 가능

  2. Spring Security의 중심점으로 잡음으로서, 선택이 아닌 필수 작업들을 누락없이 실행 가능

    • 예시 : 메모리 낭비를 방지해, SecurityContext를 지우기
    • 예시 : Http Firewall를 적용하여, 특정 공격으로부터 어플리케이션을 보호
  3. SecurityFilterChain의 호출을 유연하게 조절 가능
    → FilterChainProxy를 사용하면, RequestMatcher 인터페이스를 활용하여 HttpServletRequest의 조건을 걸어 호출 가능
    (원래 서블렛 컨테이너는 URL을 따라서만 호출할지 안할지를 결정)

  4. FilterChainProxy는 어떤 SecurityFilterChain를 사용할지 결정하는데 사용
    → 즉, 한 어플리케이션 안에서 여러가지 인증 방식(session, jwt...)을 사용하는데에 설정을 완전히 분리할 수 있는 환경을 제공

(3) AbstractAuthenticationProcessingFilter

➀ 정의

  • 사용자의 credential 을 인증하기 위한 베이스 Filter

  • SecurityFilterChain 안에는 spring security에서 제공하는 여러가지 filter들이 있는데,
    Authentication(로그인)을 담당하는 필터가 AbstractAuthenticationProcessingFilter

  • 로그인에 필요한 공통적인 로직을 가지고 있는 필터

  • 추상 클래스
    → 그래서, SecurityFilterChain안에 직접 들어갈 수 없고, 그 대신에 이를 상속 받은 filter들이 속해 있다.

➁ 동작 순서

(UsernamePasswordAuthenticationFilter 의 구동(로그인) 과정)

  1. 사용자가 form을 통해, 로그인 정보(username과 password)가 담긴 Request를 보냄
    (HttpServletRequest로 들어옴)

  2. AuthenticationFilter 는 해당 요청을 받아서,
    UsernamePasswordAuthenticationToken(username과 password를 담고있음)을 생성(발급)하고,
    Authentication Manager에게 처리 위임을 한다.
    (이때, UsernamePasswordAuthenticationToken의 타입은 Autentication)
    → 이 토큰은 해당 요청을 처리할 수 있는 Provider을 찾는데 사용됨

  3. Authentication Manager 는 인증용 객체(UsernamePasswordAuthenticationToken)를 전달 받는다.
    (이때, List형태로 Provider들을 갖고 있다.)
    (ProviderManager 는 AuthenticationManager의 구현체이다.)
    (AuthenticationManager : 여러개의 AuthenticationProvider 구현체를 가지고, 요청에 맞는 구현체에 UsernamePasswordAuthenticationToken을 전달)

  4. 해당 토큰을 처리할 수 있는 AuthenticationProvider 를 선택하고,
    실제 인증을 할 AuthenticationProvider에게 인증용 객체를 다시 전달

  5. 실제 인증이 시작되면,
    AuthenticationProvider 인터페이스가 실행되고, DB에 있는 사용자 정보와 입력한 로그인 정보를 비교

  6. UserDetailsService 의 loadUserByUsername메소드 수행하면,
    AuthenticationProvider 인터페이스에서는 authenticate() 메소드를 오버라이딩 하게 된다.
    (AuthenticationProvider는 여러개 일 수도 있음)
    → 인증용 객체(authenticate() 메소드의 파라미터)로 로그인 정보를 가져옴

  7. UserDetailsService 인터페이스 를 사용해서,
    AuthenticationProvider 인터페이스에서 DB에 접근해 넘어온 사용자 정보와 일치하는 정보를 조회하고 가져온다.

  8. 조회에 성공하면
    UserDetailsService 인터페이스는 화면에서 입력한 사용자의 username으로 loadUserByUsername() 메소드를 호출해서,
    DB에 있는 사용자의 정보를 UserDetails 타입의 객체를 리턴한다.
    → AuthenticationProvider는 DB에서 가져온 이용자 정보와 화면에 입력한 로그인 정보를 비교해서,
    일치하면 Authentication 참조를 리턴하고
    일치 하지 않으면(또는 사용자가 존재하지 않으면) 예외를 던진다.

  9. 인증이 완료되면
    AuthenticationFilter는 Authentication 객체(사용자 정보가 담긴)를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행
    (실패 시, AuthenticationFailureHandler를 실행)

③ 동작 순서 (추가 설명)

CustomSecurityFilter
SecurityFilterChain 내부에 SecurityFilter 를 custom 으로 만들 수도 있다.

@RequiredArgsConstructor

//기존에 Spring 이 가지고 있는 filter(OncePerRequestFilter)를 상속받음
public class CustomSecurityFilter extends OncePerRequestFilter {

    private final UserDetailsServiceImpl userDetailsService;
    private final PasswordEncoder passwordEncoder;


    @Override
    //request: request(HTTP 객체) 를 파라미터로 받는다
    //FilterChain: Filter 끼리 이동함 (Filter 가 Chain 으로 서로 연결돼있으므로)
    //FilterChain filterChain 과 아랫줄(filterChain.doFilter(request,response)): 여기 Filter 끝나면 다음 Filter 로 이동하는데,
                                                                            // request,response 를 담은 채로 여기 FilterChain 을 이용해서 다음 Filter 로 이동한다
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        //getParameter: Client 쪽에서 넘어오는 파라미터 값("username", "password")을 가지고 올 수 있음
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        System.out.println("username = " + username);
        System.out.println("password = " + password);
        System.out.println("request.getRequestURI() = " + request.getRequestURI());

        //DB 를 확인함
        //getRequestURI(): 들어온 URL 를 확인
        //username 과 password 가 null 이 아닌 경우 && "/api/user/login" 또는 "/api/test-secured" URL 을 포함하는 경우라면 (그냥 확인하기 쉽게 만들어놓은 것일뿐임)
            //애초에 username 과 password 가 null 이면, user 를 찾을 수 없음
        if(username != null && password  != null && (request.getRequestURI().equals("/api/user/login") || request.getRequestURI().equals("/api/test-secured"))){
            //Client 에서 가져온 username 으로 user 가 있는지 확인
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // 비밀번호 확인
            //Client 쪽에서 받아온 password 와 DB 에서 가져온 password 를 비교
            if(!passwordEncoder.matches(password, userDetails.getPassword())) {
                throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
            }

            // 인증 객체 생성 및 등록
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            //인증 객체 만들기
            //순서대로 (principal, credentials, authorities)
            //userDetails: 현재 userDetails 안에는 user, password 데이터가 들어가있는 상태
            //null: 위에서 비밀번호는 확인 했으므로 이제 필요없음 --> 그래서 null 값을 넣음
            Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            context.setAuthentication(authentication);

            SecurityContextHolder.setContext(context);
        }

        //filterChain: 여기 Filter 끝나면 다음 Filter 로 이동하는데, request,response 를 담은 채로 여기 FilterChain 을 이용해서 다음 Filter 로 이동한다
        filterChain.doFilter(request,response);
    }
}

UsernamePasswordAuthenticationFilter

  • AbstractAuthenticationProcessingFilter를 상속받은 클래스(Filter)

  • SecurityFilterChain 안에 들어있음

  • username과 password를 form 태그로 전달해서 로그인
    (Form Login 기반을 사용할 때 username 과 password 확인하여 인증)
    (Form Login 기반: 인증이 필요한 URL 요청이 들어왔을 때, 인증이 되지 않았다면 로그인페이지를 반환하는 형태)

    Form Login

    1. 유저가 인증되지 않은(not authorized) 리소스 /private에 인증되지 않은(unauthenticated) 요청을 보낸다.(GET)
    2. Spring Security의 FilterSecurityInterceptor는 인증되지 않은 요청을 AccessDeniedException 예외를 던짐으로써 거부되었음을 나타낸다.
    3. 유저가 인증받지 않았으므로, ExceptionTranslationFilter는 인증을 시작하도록 만들고 로그인 페이지(AuthenticationEtryPoint가 구성된)로 리다이렉트를 보낸다.
      (대부분 AuthenticationEntryPoint는 LoginUrlAuthenticationEntryPoint의 인스턴스이다.)
    4. 브라우저는 리다이렉트된 로그인 페이지를 요청한다.(GET)
    5. 어플리케이션에 무언가가 로그인 페이지를 렌더링 해야 한다.
    6. username, password가 넘겨지고, UsernamePasswordAuthenticationFilter가 username과 password를 인증한다.

    Default Form Login

    아이디 : user
    비밀번호 : 서버 시작시마다 변경됨

번호 4 中
AuthenticationProvider 를 구현한 구현체

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final CustomUserDetailsService userDetailsService;	// userDetailsService라는 서비스 객체를 주입받음.
    private final SamplePasswordEncoder passwordEncoder;

    public CustomAuthenticationProvider(CustomUserDetailsService userDetailsService, SamplePasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = (String) authentication.getCredentials();

        // 위의 userDetailsService라는 서비스 객체를 주입은 실제 인증이 실행되는 authenticate 메서드에서 UserDetail 객체를 뽑아오는데에 활용됨.
        // UserDetails 객체를 활용해서 인증을 진행
        	// userDetailsService.loadUserByUsername의 구현은 다음 코드 참고
        UserDetails user = userDetailsService.loadUserByUsername(username);
        if (user == null) {
            throw new BadCredentialsException("username is not found. username=" + username);
        }

        if (!this.passwordEncoder.matches(password, user.getPassword())) {
            throw new BadCredentialsException("password is not matched");
        }

        return new CustomAuthenticationToken(username, password, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return CustomAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

번호 6 中
userDetailsService.loadUserByUsername 의 구현은 개발자가 직접 해야한다.
→ 서비스마다 unique로 설정되어있는 컬럼이 다르고, 스피링은 어떤 컬럼이 기준이 되어 DB에서 찾아올지 모르기 때문

// userDetailsService를 상속받아 직접 구현
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        Optional<Member> memberOptional = memberRepository.findByEmail(email);
        if (memberOptional.isPresent()) {
            return new PrincipalDetails(memberOptional.get());
        } else {
            throw new UsernameNotFoundException(email);
        }
    }
}

UserDetailsService
- username/password 인증방식을 사용할 때, 사용자를 조회하고 검증한 후 UserDetails를 반환
- Custom하여 Bean으로 등록 후 사용 가능

UserDetails
- 검증된 UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용됨
- 해당 인증객체는 SecurityContextHolder에 세팅됨
- Custom하여 사용가능

UserDetails, UserDetailsService Custom 할 경우
Filter 단에서 인증할 때, custom을 하지 않으면 자동으로 기본 설정을 사용한다고 인지하고 디폴트 password 를 준다.

번호 8 中
만약, 인증에 실패했을 경우 (Failure)

  • SecurityContextHolder(인증된 사용자의 정보를 담는 곳) : 비워지게 됨
  • RememberMeServices(로그인 유지하는 기능) : 이곳에서 실패 로직이 발동됨 (단, 이 기능을 설정해놨을 때만 발동)
  • AuthenticationFailureHandler : 발동됨

만약, 인증에 성공했을 경우 (Success)

  • SessionAuthenticationStrategy
    • sessionAuthenticationStrategy.onAuthentication가 실행되며
    • 새로운 session을 만듦
  • SecurityContextHolder(인증된 사용자의 정보를 담는 곳)
    • 이곳에 Authentication(사용자 정보가 들어있음) 객체를 넣음
    • 반환된 정보가 모두 채워져서 반환된 Authentication 객체를 SecurityContext에 감싸고, 또 SecurityContextHolder로 감싼다.
      → 그럼 Spring Security는 SecurityContextHolder에 값이 있으면 인증된 유저로 인식하고, Set-Cookie 헤더에 JSESSINID를 넣어서 응답
  • RememberMeServices(로그인 유지 기능) : 이곳에 성공 로직이 발동됨 (단, 이 기능을 설정해놨을 때만 발동)
  • ApplicationEventPublisher : 이곳에서 InteractiveAuthenticationSuccessEvent를 발행
  • AuthenticationSuccessHandler : 발동됨 (일반적으로는 SimpleUrlAuthenticationSuccessHandler이다)
    • 로그인 성공 시 커스텀 로직을 넣는 것은 SimpleUrlAuthenticationSuccessHandler를 커스터마이징 하면 됨.
      (예시 : 최근 로그인 날짜를 DB에 집어넣는 로직 등...)

최초 인증이 끝난 유저가 JSESSINID를 Cookie 헤더에 넣어서 요청을 보내면, 서버에서는 JSESSINID가 세션에 있는지 확인하여 인증을 진행

번호 9 中
SecurityContextHolder

인증된 객체는 이런 구조로 감싸져있다.

  1. SecurityContextHolder
    1) SecurityContext를 감싼다.
    2) spring security로 인증된 유저에 대한 정보를 저장하고 있는 객체 (spring security의 인증에 핵심 모델)
    3) SecurityContextHolder가 값을 가지고만 있어도 spring security에서는 인증된 유저라고 가정한다.
    4) 프로젝트 내 어디에서든 인증이 완료된 사용자 정보를 호출할 수 있도록 해주는 클래스

  2. SecurityContext
    1) SecurityContextHolder와 Authentication 객체를 이어주는 역할
    2) SecurityContextHolder 로 접근 가능
    3) Authentication 객체를 가지고 있다.

  3. Authentication
    1) 정의
    - AuthenticationManager에 들어가기 위해
    - 현재 입증된 유저를 대표하는 객체(현재 인증된 사용자를 나타냄)
    - SecurityContext 에서 가져올 수 있다.
    - Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스
    - 인증객체를 만드는데 사용됨
    2) 구성요소
    - principal : 사용자를 식별. username/password로 인증할때, 이 요소는 UserDetail의 인스턴스
    - credentials : 암호(비밀번호)가 들어감. (대부분의 경우) 유출 방지를 위해, 인증한 후 삭제됨
    - authorities : 유저의 역할(일반 유저, 관리자...)을 나타냄. 부여한 권한을 GrantedAuthority 로 추상화하여 사용

3. 예외 발생

1) 정의

AccessDeniedException과 AuthenticationException을 http response로 바꿔주기 위해, ExceptionTranslationFilter 를 활용
(ExceptionTranslationFilter : SecurityFilterChain에 들어있는 필터 中중 하나)

2) 사용법

  1. 예시
try {
	// 1. 다음 필터로 넘어가도록 하는데
		// ExceptionTranslationFilter는 FilterChain.doFilter(request, response)를 호출하여, 다음 filter로 넘어가게 한다.
	filterChain.doFilter(request, response);
} catch (AccessDeniedException | AuthenticationException ex) {
	// 2. 예외가 발생하면, 인증을 시작
		// 만약, 유저가 인증이 안되었거나 AuthenticationException을 발생 시키면, Authentication(인증)을 시작
	if (!authenticated || ex instanceof AuthenticationException) {
		startAuthentication();
	// 3. 인증에 실패하면(예외가 발생하지 않으면), 예외를 핸들링하는 로직이 실행(ExceptionTranslationFilter는 그냥 넘어감)
		// 인증이 실패하여 AccessDeniedException이 발생하면, AccessDeniedHandler를 호출
	} else {
		accessDenied();
	}
}
  1. 예시
    CustomAccessDeniedHandle
@Component
public class CustomAccessDeniedHandle implements AccessDeniedHandler {      //CustomAccessDeniedHandler?

    //객체 생성
    private static final SecurityExceptionDto exceptionDto =
            new SecurityExceptionDto(HttpStatus.FORBIDDEN.value(), HttpStatus.FORBIDDEN.getReasonPhrase());

    @Override
    //handle() 함수를 통해, Client 쪽으로 반환
    //401, 403 에러가 발생하면, handle() 함수 실행
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException{

        //response 에 ContentType, Status 를 넣음
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setStatus(HttpStatus.FORBIDDEN.value());

        //ObjectMapper 를 사용해서, String 값으로 변환 --> Client 쪽으로 반환됨
        try (OutputStream os = response.getOutputStream()) {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.writeValue(os, exceptionDto);
            os.flush();
        }
    }
}

4. WebSecurityConfigurerAdapter

1) 정의

spring security는 WebSecurityConfigurerAdapter를 상속받아, 관련 설정을 관리

2) 사용법

configure를 오버라이딩해서 여러가지 설정 가능

참고: 오버로딩, 오버라이딩

@Configuration
@EnableWebSecurity // 스프링 시큐리티 필터가 스프링 필터체인에 등록됨
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // secured 어노테이션 활성화, preAuthorize 어노테이션 활성화
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final LoginSuccessHandler loginSuccessHandler;

    //해당 메서드의 리턴되는 오브젝트를 ioc로 등록해줌.
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
//            .antMatchers("/user/**").authenticated()
            .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
            .anyRequest().permitAll()
        .and()
            .formLogin()
            .loginPage("/member/loginForm")
            .usernameParameter("email")
            .loginProcessingUrl(
                "/member/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해줌. -> Controller에 로그인을 안만들어됨.
            .defaultSuccessUrl("/")
            .successHandler(loginSuccessHandler)
        .and()
            .sessionManagement()
            .maximumSessions(1)
            .expiredUrl("/member/loginForm");
    }
}

5. 비밀번호 암호화

1) 정의

  • Spring Security 는 적응형 단방향 함수를 쉽게 사용할 수 있도록, 미리 만들어 제공
    → bCrypt를 사용하여 비밀번호를 암호화
    (bCrypt : Spring Security 가 제공하는 적응형 단방향 함수)

  • 적응형 단방향 함수 : 내부적으로 리소스의 낭비가 매우 大 때문에, API 요청마다 사용자의 이름과 비밀번호를 검증하면 어플리케이션 성능 ↓
    → 따라서, Session/cookie, Token 과 같은 인증방식을 사용하여 검증하는 것이 속도/보안 측면에 유리

2) 검증 흐름

  1. 사용자는 회원가입을 진행

  2. 사용자의 정보 저장 시, 비밀번호를 암호화하여 저장

  3. 사용자는 로그인을 진행

  4. 사용자가 입력한 정보를 통해, 저장된 암호화된 비밀번호를 가져와서 사용자가 입력한 암호와 비교 (즉, 회원가입 정보 & 로그인 정보를 비교)

  5. 사용자 인증이 성공하면, 사용자의 정보를 사용하여 JWT 토큰을 생성하여 Header에 추가하여 반환하고
    Client 는 이를 쿠키저장소에 저장

  6. 사용자는 요청(게시글 작성 같은)을 진행할 때 발급받은 JWT 토큰을 같이 보내고
    서버는 이를 빠르게 인증하고 사용자의 요청을 수행

3) 양방향, 단방향

(1) 양방향 암호 알고리즘

(2) 단방향 암호 알고리즘

Password 확인절차
1. 사용자가 로그인을 위해 "아이디, 패스워드(평문)" 입력 → 서버에 로그인 요청
2. 서버에서 패스워드(평문)을 암호화
3. DB 에 저장된 "아이디, 패스워드 (암호문)"와 일치 여부 확인

4) Password Matching

Spring Security에서는 비밀번호를 암호화하는 함수를 제공할 뿐만 아니라,
사용자가 입력한 비밀번호를 저장된 비밀번호와 비교하여 일치여부를 확인해주는 함수도 제공

// boolean matches(CharSequence rawPassword, String encodedPassword);
if(!passwordEncoder.matches("사용자가 입력한 비밀번호", "암호화되어 DB 에 저장된 비밀번호")) {
		   throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
 }

6. 권한(Authority) 설정

1) 사용법

  1. 로그인을 시도하면, 인증 관리자에게 username, password 정보를 HTTP body 로 전달 (POST 요청)

  2. UserDetailsService에게 username 을 전달하고 회원상세 정보를 요청

  3. DB에 회원 정보가 존재하지 않으면 Error를 발생시키고,
    존재시 조회된 회원 정보를 UserDetails로 변환

  4. UserDetails를 인증관리자에게 전달한 후
    client가 보낸 username, password 와 UserDetails의 username, password를 비교하여
    인증 성공 시, 세션에 로그인 정보를 저장하고
    인증 실패 시, Error 를 발생시킨다.

2) @EnableGlobalMethodSecurity

  • GlobalMethodSecurity 사용 활성화 (= @Secured, @PreAuthorize, @PostAuthorize 활성화)
  • MethodSecurity 는 특정 메소드마다 권한을 제어할 수 있도록 해준다.
  • 단순히 허용, 거부 하는 것 보다 더 복잡한 규칙을 적용할 수 있음

3) 표현식

hasRole([role]) : 현재 사용자의 권한이 파라미터의 권한과 동일한 경우 true
hasAnyRole([role1,role2]) : 현재 사용자의 권한디 파라미터의 권한 중 일치하는 것이 있는 경우 true

principal : 사용자를 증명하는 주요객체(User)를 직접 접근할 수 있다.
authentication : SecurityContext에 있는 authentication 객체에 접근 할 수 있다.

permitAll : 모든 접근 허용
denyAll : 모든 접근 비허용

isAnonymous() : 현재 사용자가 익명(비로그인)인 상태인 경우 true
isRememberMe() : 현재 사용자가 RememberMe 사용자라면 true
isAuthenticated() : 현재 사용자가 익명이 아니라면 (로그인 상태라면) true
isFullyAuthenticated() : 현재 사용자가 익명이거나 RememberMe 사용자가 아니라면 true

4) 어노테이션

(1) @Secured

① 정의

  • 특정 권한만 접근이 가능하다는 것을 나타냄
  • 표현식을 사용할 수 없고 OR문만 표현 가능
  • 스프링 표현 언어 (SpEL을)를 지원하지 X → 표현식 사용 X
  • 권한이 필요한 부분에 선언 가능 → Class나 Method 단위까지 지정 가능
  • 역할 List을 지정하는 데 사용 → 따라서 사용자는 지정된 역할 中 하나 이상이 있는 경우에만 해당 방법에 액세스 가능

단, 단순하게 특정 권한을 가진 사람이 아닌 다양한 조건이 들어가야 되는 경우에는 @PreAuthorize, @PostAuthorize를 이용

② 사용법

  1. 접근 가능한 권한이 하나 일 때
    MethodSecurityConfig.class
@Configuration
@EnableGlobalMethodSecurity(
        prePostEnabled = true,		// Spring Security의 @PreAuthorize, @PreFilter / @PostAuthorize, @PostFilter 활성화 여부
        securedEnabled = true,		// @Secured 활성화 여부
        jsr250Enabled = true)		// @RoleAllowed 사용 활성화 여부
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

enum 클래스
메소드에 권한 적용하기

@Secured({UserRole.ROLES.ADMIN})	// 또는 @PreAuthorize("hasRole('" + UserRole.ROLES.ADMIN + "')")
    public static class ROLES {
        public static final String USER = "ROLE_USER";
        public static final String ADMIN = "ROLE_ADMIN";
    }
@Secured("ROLE_ADMIN")
public String encoding(String str){
	return encoder.encodePassword(str,null);
}
  1. 접근 가능한 권한이 두개 이상 일 때
@RequestMapping(value = "/checkAuth", method = RequestMethod.GET)
@Secured({"ROLE_ADMIN","ROLE_USER"})	// ROLE_USER 또는 ROLE_ADMIN 중 하나의 권한을 가지고 있다면 접근 가능
public String checkAuth(Locale locale, Model model, Authentication auth) {
	UserDetailsVO vo = (UserDetailsVO) auth.getPrincipal();
	logger.info("Welcome checkAuth! Authentication is {}.", auth);
	logger.info("UserDetailsVO == {}.", vo);
	model.addAttribute("auth", auth );
	model.addAttribute("vo", vo );
	return "checkAuth";
}

(2) @PreAuthorize

① 정의

  • Spring EL(SpEL, Spring Expression Language, 표현식)을 사용 → AND나 OR 같은 표현식 사용 가능
  • 해당 로직을 수행 전 권한을 검사

② 사용법

// ROLE_USER와 ROLE_ADMIN 두 개의 권한을 가져야 접근 가능
@PreAuthorize("hasRole('ROLE_USER') and hasRole('ROLE_ADMIN')")
@PreAuthorize("hasRole('ROLE_VIEWER')")
public String getUsernameInUpperCase() {
    return getUsername().toUpperCase();
}

@Secured({"ROLE_VIEWER","ROLE_EDITOR"}) = @PreAuthorize("hasRole('ROLE_VIEWER') 또는 hasRole('ROLE_EDITOR')")

@PreAuthorize("isAuthenticated() and (( #user.name == principal.name ) or hasRole('ROLE_ADMIN'))")
@RequestMapping( value = "", method = RequestMethod.PUT)
public ResponseEntity<Project> updateProject( User user ){
    updateProject.update( user );
    return new ResponseEntity<Project>( new Project(), HttpStatus.OK );
}

#user.name : Annotation이 붙은 Method의 Param값을 위와 같이 활용

(3) @PostAuthorize

① 정의

  • Spring EL(SpEL, Spring Expression Language, 표현식)을 사용 → AND나 OR 같은 표현식 사용 가능
  • 해당 로직을 수행하기 후 권한을 검사

② 사용법

@PostAuthorize("isAuthenticated() and (( returnObject.name == principal.name ) or hasRole('ROLE_ADMIN'))")
@RequestMapping( value = "/{id}", method = RequestMethod.GET )
public Project getProject( @PathVariable("id") long id ){
    return service.findOne(id);
}

returnObject.name : Method가 실행된 이후의 return값을 활용할 수 있다.


참고: Spring Security에 대해서 알아보자(동작 과정편)
참고: Spring Security에 대해서 알아보자(로그인 인증 구조)
참고: Spring Security Form Login
참고: @Secured, @PreAuthorized를 이용한 메소드 수준의 권한 적용
참고: 09. spring security @Secured 어노테이션을 사용하여 접근 권한 부여
참고: 스프링 메소드 Security 소개
참고: [Spring] Spring Security Annotation (@PreAuthorize, @PostAuthorize, @Secure)
참고: Spring Security @PreAuthorize, @PostAuthorize 를 사용하는 신박한 전처리 후처리 기법
참고: [프로젝트 2] Spring Security를 활용한 인증, 인가 처리 로직 구현
참고: Spring security - csrf란?
참고: 항해99 ) 4주차 회고록
참고: Spring security 동작 원리 (인증,인가)

profile
개발자로 거듭나기!

0개의 댓글