[Spring Security] 스프링 시큐리티의 개념과 구조

Intro

스프링 시큐리티에 대해서 정리하고자 한다.

스프링 시큐리티 개념

스프링 시큐리티란, 스프링을 기반으로 애플리케이션의 보안(인증, 권한, 인가)를 담당하는 프레임워크이다.
즉, 인증과 인가를 담당하는 프레임워크라고 생각하면 된다.(스프링 공식페이지에서 소개한 문장)

스프링 시큐리티는 Dispatcher Servlet의 앞에 위치하는 필터와 필터 체인으로 구성된 모델이다. 그렇기에 스프링 영역에 들어가기 전 작업을 처리할 수 있다는 특징이 있다.

img

스프링 시큐리티는 다양한 필터들을 제공하는데, 이렇게 제공되는 필터들을 시큐리티 필터 체인이라 한다.

스프링 시큐리티 기본용어

AOP에는 인증(Authentication), 인가(Authorization), 권한, 접근 주체(principal)라는 개념이 있다.

  1. 접근 주체(principal) : 보호된 대상에 접근하는 주체를 의미한다.
  2. 인증(Authentication) : 해당 사용자가 본인이 맞는지를 확인하는 절차를 의미한다.
  3. 인가(Authorization) : 인증된 주체가 요청한 자원에 권한을 갖고 있는 지 검사하는 과정이다.
  4. 권한(Authorize) : 인증된 사용자가 어떤 것을 할 수 있는지를 의미한다.

스프링 시큐리티 의존성 설정(maven)

1
2
3
4
5
<!-- Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

pom.xml에 직접 작성해도 되며, 프로젝트를 생성할 때 설정해도 된다.


스프링 시큐리티 초기 화면

img

시큐리티 의존성을 설정하게 되면 어떤 페이지를 가더라도 위 사진과 같은 창이 뜨게 된다.
기본적으로 Spring Security는 인증되지 않은 사용자는 서비스를 사용하지 못하도록 설정되어있다.
그렇기에 별도로 SecurityConfig.java 파일을 통해서 시큐리티 설정을 해줘야 한다.


SecurityConfig 설정(SecurityConfig.java)

해당 파일은 Spring : 2.7.12 , java: 11으로 작성된 파일입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.cos.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import com.cos.security.oauth.PrincipalOauth2UserService;

import lombok.AllArgsConstructor;


@Configuration // IoC 빈(bean)을 등록
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // Secured 어노테이션 활성화
@AllArgsConstructor
public class SecurityConfig {
	@Autowired
	private PrincipalOauth2UserService principalOauth2UserService;

	@Autowired
    BCryptPasswordEncoder encoder;

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http.csrf().disable();
		http.authorizeRequests()
				.antMatchers("/user/**").authenticated() // authenticated : /user의 주소는 인증되야만 한다.
				.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") // access : /admin/* 주소는 인증 + Role을 가져야 함.
				.anyRequest().permitAll() //admin, user를 제외한 주소는 허용 된다.
				.and()
				.formLogin()
				.loginPage("/loginForm")
				.loginProcessingUrl("/login")// login 주소가 호출되면 시큐리티가 낚아채서 로그인 진행함.
				.defaultSuccessUrl("/")
				.and()
				.oauth2Login()
				.loginPage("/loginForm")
				.userInfoEndpoint()
				.userService(principalOauth2UserService);

		return http.build();
	}
}

  1. authorizeRequest() : 인증된 경우에만 접근을 허용하고, 아닌 경우는 접근을 차단하는 기능이다.
    • 위 코드 중에서 Authorization을 설정하는 부분이 authorizeRequests() 이후의 코드들이다.
    • antMatchers를 통해 여러 요청에 대해서 역할에 따라 경로 패턴을 설정할 수 있다.
      • 위 코드에서는 user의 경우는 인증을 받도록 설정하였고, admin의 경우는 인증 + ROLE_ADMIN이라는 역할을 가진 경우에만 접근하도록 설정하였다.
      • 그리고 그 외의 요청들은 permitAll()을 통해 다른 주소들의 접근은 허용하였음.
    • 주의할 점으로는 permitAll()으로 설정한 경로가 앞단에 위치하게 되면 뒤에 작성되는 패턴들은 적용되지 않는다. 그렇기에 permitAll()을 작성할 때 구체적인 경로(“/user/”, “/admin/“)를 먼저 작성하고, 뒤에 큰 범위의 경로(permitAll)가 오도록 해야 한다.
  2. formLoginUrl().loginPage("로그인 페이지"). : 일반 로그인 시 커스텀 로그인 페이지를 지정함.
  3. loginProcessingUrl("주소) : 로그인 요청 주소
  4. oauth2Login() : OAuth 로그인 진행 시 페이지

SecurityConfig 설정 후

gif 원래는 /user요청을 하면 user로 이동해야 하지만 /loginForm으로 경로가 변경되는 것을 확인할 수 있다.

위 파일에서 loginPage(“/loginForm”)으로 설정하였기 때문이다.

인증되지 않은 사용자가 user와 admin에 접속할 경우 loginPage에 지정된 페이지로 포워딩된다.

그리고 loginForm에서 /login 요청을 진행하면 loginProcessingUrl(“/login) 설정했기에 시큐리티 필터가 가로채서 작업을 수행한다. 그렇기에 컨트롤러에 /login에 대한 매핑을 진행하지 않아도 된다.

로그인 원리로는 /login 요청이 들어오면 AuthenticationManager가 토큰과 username, password를 가지고 userDetailsService에서 사용자 정보를 userDetails 객체에 저장한다. 그리고 AuthenticationProvider가 비교한다. 비교한 후 인증이 완료되면 Authentication객체를 반환하고 SecurityHolder에 저장하여 로그인이 진행된다. (구체적인 코드와 설명은 하단에서 설명합니다.)


Spring Security 구조

img

Spring Security 구조 처리과정

  1. 사용자의 요청(Http Request)
  2. AuthenticationFilter(필터)
    • AuthenticationFilter는 요청이 들어오면 가로채는 역할을 한다.
    • 가로챈 정보를 통해서 UsernamePasswordAuthenticationToken을 생성한다.
  3. AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.
  4. AuthenticationManager는 등록된 AuthenticationProvider(들)을 조회하여 인증을 요구한다.
  5. 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.
  6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만든다.
  7. AuthenticationProvider(들)은 UserDetails를 넘겨받고 사용자 정보를 비교한다.
  8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.
  9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.
  10. Authenticaton 객체를 SecurityContext에 저장한다.

Spring Security 모듈

  1. AuthenticationFilter : 인증되지 않은 사용자인 경우 loginPage() 메서드에 설정 해둔 주소로 요청을 가로챈다.

  2. UsernamePasswordAuthenticationToken : 사용자의 인증을 한 후 유저네임과 패스워드를 담고 있는 토큰이다.
    • UsernamePasswordAuthenticationToken에 유저네임, 패스워드를 담겨있기에 이 토큰을 AuthenticationManager에게 전달하여 인증을 수행한다.
    • 수행 후 인증이 완료되면 Authentication 객체로 반환된다.
    • AuthenticationProvider에 의해 처리된다.
  3. AuthenticationManager : 인증을 처리하는 클래스로, Authentication에 등록된 AuthenticationProvider에 의해서 처리된다.
    • 인증이 성공적으로 이뤄지면 Authentication 객체를 SecurityContext에 저장한다.
  4. SecurityHolder : 인증이 완료된 사용자 정보를 호출할 수 있는 클래스.
    • SecurityContext 저장소라고 생각하면 된다.
    • Holder 객체를 이용해서 위치와 상관없이 전역적으로 SecurityContext에 접근이 가능하다
  5. SecurityContext : Authentication 객체를 보관하는 저장소이다.
    • Authentication 객체를 언제든지 꺼내서 쓸 수 있도록 제공되는 클래스이다.
    • 인증이 완료되면 세션 영역에 저장되기에 전역적인 참조가 가능하다.
  6. Authentication : 사용자의 인증 정보를 저장하는 곳이다.
    • 요청이 들어올 때 인증된 고객의 정보를 담고 있다고 생각하면 된다.
    • SecurityContext에 저장된다.
    • Authentication 안에 유저 정보를 담을 때는 UserDetails, OAuth2User 타입만 들어갈 수 있다.
  7. UserDetails
    • 사용자의 정보를 담는 인터페이스.
    • implements + 오버라이드를 진행한 후에 사용하면 된다.
  8. UserDetailsService
    • UserDetails 를 리턴하는 loadUserByUsername 메서드를 갖고 있는 인터페이스.
    • implements + 오버라이드를 진행한 후에 사용하면 된다.
    • login 요청이 들어오면 UserDetailsService에서 loadUserByUsername를 실행하여 UserDetails 객체를 리턴한다.
  9. GranedAuthority
    • 현재 사용자가 갖고 있는 권한을 담은 인터페이스.
    • ROLE_* 형태로 사용된다. (ex: ROLE_USER, ROLE_ADMIN)
    • UserDetailsService를 통해서 불러올 수 있다.

PrincipalDetails.java 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package test.security.test_security.auth;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import test.security.test_security.dto.Users;

public class PrincipalDetails implements UserDetails {

    private Users users;

    public PrincipalDetails(Users users) {
        this.users = users;
    }

    // 해당 유저의 권한을 리턴하는 메서드.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // Collection 타입으로 지정했기에 타입을 맞춰준다.
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {

            @Override
            public String getAuthority() {
                return users.getRole();
            }

        });
        return collect;
    }

    @Override
    public String getPassword() {
        return users.getPassword();
    }

    @Override
    public String getUsername() {
        return users.getUsername();
    }

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

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

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

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

}

PrincipalDetailsService.java 파일

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package test.security.test_security.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import test.security.test_security.dto.Users;
import test.security.test_security.repository.UserRepository;

@Service
public class PrincipalDetailsService implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // findByUsername 메서드는 직접 제작함. (username에 일치하는 데이터가 있는 지 확인.)
        Users userEntity = userRepository.findByUsername(username);

        if (userEntity != null) {
            return new PrincipalDetails(userEntity);
        }

        return null;
    }

}

참고 사이트

Security 참고 사이트1
Security 참고 사이트2
Security 참고 영상1