Just Do IT!

[Spring] Spring Security 기본 개념 정리 & 예시 본문

개발 공부/Spring

[Spring] Spring Security 기본 개념 정리 & 예시

MOON달 2024. 8. 23. 12:25
728x90

이제 단순 CRUD가 아니라 로그인, 회원가입 기능을 배우고 있는데...

spring security에 들어가니까 너무 복잡하고 강의 내용을 그대로 따라치고 있는 것 같아서 내 나름대로 정리를 해보려고 한다.

기본 개념부터 정리하기로....

 

 

 

Spring Security란?

  • 스프링 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크
  • 즉, 인증(Authenticate)과 인가(Authorize)를 담당하는 프레임워크
  • 주로 서블릿 필터(filter)와 이들로 구성된 필터체인으로의 구성된 위임 모델을 사용한다.

 

간단 용어 정리

  • 인증(Authentication) : 사용자가 누구인지 확인하는 과정
  • 인가(Authorization) : 특정 부분에 접근할 수 있는 권한을 확인하는 과정
  • 접근 주체(Principla) : 보호되노 대상에 접근하는 유저
  • 권한 : 인증된 주체가 애플리케이션의 동작을 수행할 수 있도록 허락되었는지를 결정할 때 사용

 

 

스프링 시큐리티는 기본적으로 세션&쿠키 방식으로 인증한다.
인증 관리자(Authentication Manager)와 접근 결정 관리자 (Access Decision Manager)를 통해 사용자의 리소스 접근을 관리한다.
여기서 인증 관리자는 UsernamePasswordAuthenticationFilter, 접근 결정 관리자는 FilterSecurityInterceptor가 수행한다.

 

 

스프링 시큐리티 필터

클라이언트(보통 브라우저)는 요청을 보내게 되고, 그 요청을 서블릿이나 JSP등이 처리한다.
스프링 MVC 에서는 요청을 먼저 받는 것이 DispatchServlet이다.
DispatchServlet이 요청을 받기 전에 다양한 필터들이 존재할 수 있다.
필터는 클라이언트와 자원 사이에서 요청과 응답 정보를 이용해 다양한 처리를 하는데 목적이 있다.

 

 

위처럼 스프링 시큐리티는 다양한 기능을 가진 필터들을 기본적으로 제공하고, 이를 시큐리티 필터 체인이라고 말한다.

 

그렇지만 위 그림은 너무 복잡하므로, 아래에 조금 더 정리 잘된 그림을 가져왔다.

그래도 복잡함...

  • SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할
  • LogoutFilter : 설정된 로그아웃 URL로 오는 요청을 감시하며, 해당 유저를 로그아웃 처리하는 역할
  • UsernamePasswordAuthenticationFilter : 설정된 URL로 오는 요청을 감시하며 유저 인증 처리하는 역할 (아이디와 비밀번호를 사용하는 form 기반 인증이다)
    • AuthenticationManager를 통한 인증 실행
    • 인증 성공 시, Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
    • 인증 실패 시, AuthenticationFailureHandler 실행
  • DefaultLoginPageGeneratingFilter : form 기반 혹은 OpenID 기반 인증에 사용하는 가상 URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML을 생성하는 역할
  • BasicAuthenticationFilter : HTTP 기본 인증 헤더를 감시하여 처리한다.
  • RequestCatchAwareFilter : 로그인 성공 이후, 원래 인증 요청에 의해 가로채어진 사용자의 원래 요청을 재구성하는데 사용하는 역할
  • SecurityContextHolderAwareRequestFilter : HttpServletRequestWrapper를 상속한 SecurityContextHolderAwareRequestWrapper 클래스로 HttpServletRequest 정보를 감싸는 역할
    • SecurityContextHolderAwareRequestWrapper : 필터 체인상의 다음 필터들에게 부가 정보 제공
  • AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면, 인증 토큰에 사용자가 익명으로 나타난다.
  • SessionManagementFilter : 인증된 주체를 바탕으로 세션 트레킹을 처리해 단일 주체와 관련한 모든 세션들이 트레킹 되도록 도움을 준다
  • ExceptionTranslationFilter : 보호된 요청을 처리하는 도중에 발생할 수 있는 예외의 기본 라우팅과 위임, 전달하는 역할
  • FilterSecurityInterceptor : AccessDecisionManager로, 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해준다.

 

 

스프링 시큐리티 동작

  1. 클라이언트가 로그인 시도
    • HttpServletRequest에 아이디, 비밀번호 정보가 전달
    • AuthenticationFilter가 넘어온 아이디와 비밀번호 유효성 검사 실시
  2. 유효성 검사 실시 후, 실제 구현체인 UsernamePasswordAuthenticationToken을 만들어 넘겨준다
  3. 인증용 객체인 UsernamePasswordAuthenticatonToken을 AuthenticationManager에 전달
  4. UsernamePasswordAuthenticationToken AuthenticationProvider에게 전달
  5. 사용자 아이디를 UserDetailsService로 보낸다. UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 UserDetails 객체로 만들어 AuthenticationProvider에게 전달한다.
    • UserDetailsService (Interface)
      • 해당 인터페이스를 구현한 빈(Bean)을 생성하여 시큐리티는 해당 빈을 사용한다.
      • 어떤 데이터베이스부터 읽어들일 지 개발자가 결정할 수 있게 한다.
  6.  DB에 존재하는 사용자 정보 가져오기
  7. 입력 정보와 UserDetails의 정보를 비교해 실제 인증 처리를 진행한다

 

8 ~ 10. 인증이 완료되면 SecurityContextHolder Authentication을 저장한다.

인증 성공 시 AuthenticationSuccessHandler, 실패 시 AuthenticationFailureHandler 실행한다.

 

 

 

 


 

[예제] 로그인, 회원가입, 로그아웃 기능 구현해보기

1. 의존성 추가

    // 스프링 시큐리티를 사용
    implementation 'org.springframework.boot:spring-boot-starter-security'
    // 타임리프에서 스프링 시큐리티를 사용
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    // 스프링 시큐리티 테스트 사용
    testImplementation 'org.springframework.security:spring-security-test'

 

build.gradle에 의존성을 추가해줘야 한다.

의존성을 추가한 경우 gradle > Refresh Gradle Project를 해줘야 제대로 추가가 된다.

 

2. User Entity 생성

@Entity
@EntityListeners(AuditingEntityListener.class)
@RequiredArgsConstructor
@Data
public class User implements UserDetails {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(updatable = false)
	private Long id;

	@Column(nullable = false, unique = true)
	private String email;

	@Column(nullable = false)
	private String password;

	@Column(name = "login_attempts")
	private Long loginAttempts = 0L;

	@CreatedDate
	@Column(name = "craeted_at")
	private LocalDateTime createdAt;

	@LastModifiedDate
	@Column(name = "updated_at")
	private LocalDateTime updatedAt;

	@Builder
	public User(String email, String password) {
		this.email = email;
		this.password = password;
	}

	// 사용자가 가질 수 있는 권한 목록 반환
	// '사용자' 권한만 있기 때문에 '사용자' 권한만 부여
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return List.of(new SimpleGrantedAuthority("user"));
	}

	// 사용자 식별값 반환
	// 사용자의 식별 가능한 이름 = email
	@Override
	public String getUsername() {
		return email;
	}

	// 계정 만료 여부
	// true : 만료 아님 | false : 만료
	@Override
	public boolean isAccountNonExpired() {
		return true;
	}

	// 계정 잠금 여부
	// true : 열림 | false : 잠김
	@Override
	public boolean isAccountNonLocked() {
		return UserDetails.super.isAccountNonLocked();
	}

	// 비밀번호 만료 여부
	// true: 만료되지 않음 | false : 만료
	@Override
	public boolean isCredentialsNonExpired() {
		return UserDetails.super.isCredentialsNonExpired();
	}

	// 계정 사용 가능 여부
	@Override
	public boolean isEnabled() {
		return UserDetails.super.isEnabled();
	}

}

 

Spring Security의 UserDetails 인터페이스를 구현하여, Spring Security에서 사용자의 정보를 제공하는 역할을 한다.

 

메소드 오버라이드를 해줄 수 있는데, 처음이라 전부 오버라이드 하긴 했지만 모든 기능을 다 사용하지는 않았다.

프로젝트에 따라 필요한 부분만 오버라이드 해주면 된다.

 

각 메소드마다 설명은 주석으로 다 적어두었다.

 

3. WebSecurityConfig 파일 생성

스프링 시큐리티 3.0 이상은 시큐리티 설정을 해야 한다고 한다.

config 폴더 안에 WebSecurityConfig 파일을 생성해주었고, 필요한 @Bean을 추가해 사용할 수 있다.

 

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
	private final UserDetailsService userDetailsService;
	private final LoginSuccessHandler loginSuccessHandler;
    
    // 특정 부분에 스프링 시큐리티 기능 비활성화
  	@Bean
  	WebSecurityCustomizer configure() {
  		return (web) -> web.ignoring()
  			.requestMatchers(
  					new AntPathRequestMatcher("/static/**")
  			);
  	}

	// HTTP 요청에 따른 보안 구성
	@Bean
	SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		return http.authorizeHttpRequests(auth -> auth.requestMatchers(
				// 인증, 인가 설정
				new AntPathRequestMatcher("/login"), new AntPathRequestMatcher("/join"),
				new AntPathRequestMatcher("/static/**")).permitAll()
				// 나머지는 인증이 필요
				.anyRequest().authenticated()
		// 폼 기반 로그인 설정
		).formLogin(
				form -> form.loginPage("/login").successHandler(loginSuccessHandler).defaultSuccessUrl("/blog/list"))
				// 로그아웃 설정
				.logout(logout -> logout.logoutSuccessUrl("/login").invalidateHttpSession(true))
				// CSRF 공격 방지 설정
				.csrf(AbstractHttpConfigurer::disable)
				// CORS 비활성화
				.cors(AbstractHttpConfigurer::disable).build();
	}

	// 인증 관리자 설정
	@Bean
	AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCrypt, UserDetailsService uds)
			throws Exception {
		DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
		authProvider.setUserDetailsService(userDetailsService);
		authProvider.setPasswordEncoder(bCrypt);
		return new ProviderManager(authProvider);
	}

	// 비밀번호 암호화를 위한 사용 설정
	@Bean
	BCryptPasswordEncoder bCryptPasswordEncoder() {
		return new BCryptPasswordEncoder();
	}

}

 

 

필요한 설명은 주석으로 적어놓았지만, 적용하지 않은 것들도 포함해서 정리해본다.

 

SecurityFilterChain :  특정 Http 요청에 대해 웹 기반 보안 구성. (인증/인가 및 로그아웃 설정)

  • Configure() : 스프링 시큐리티의 모든 기능을 사용하지 않게 설정
    • requestMatchers() : 특정 요청과 일치하는 url에 대한 액세스 설정
    • ignoring() : requestMatchers에 적힌 url에 대해 인증, 인가를 하지 않는다는 의미
    • URL의 /**/, ** 사용은 ** 위치에 어떤 값이 들어와도 적용 가능하다는 의미이다.
  • authenticationHttpRequests() : 인증, 인가가 필요한 URL 지정
    • AntPathRequestMatcher : 사용자가 요청한 요청 정보를 확인하여 요청 정보 url이 맞는지 확인
    • permitAll() : 지정된 URL은 인증, 인가 없이도 접근 허용
    • anyRequest() : 지정된 URL 외의 요청에 대한 설정
    • authenticated() : 해당 URL에 진입하기 위해서는 인증이 필요하다
    • hasAuthority() : 해당 URL에 진입하기 위해서 Authorization(인가)이 필요하다
      • 예를 들어, ADMIN만 진입 가능한 경우
    • formLogin() : form login방식 적용
      • loginPage() : 로그인 페이지 URL
      • defaultSuccessURL() : 로그인 성공 시 이동할 URL
      • successHandler() : 로그인 성공 시 실행된 핸들러를 지정
      • failureURL() : 로그인 실패 시 이동할 URL
    • logout() : 로그아웃에 대한 정보
      • logoutSuccessUrl() : 로그아웃 이후 리다이렉트될 URL을 지정 
      • invalidateHttpSession() : 로그아웃 이후 전체 세션 삭제 여부
    • csrf (cross site request forgery) : 공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것 (즉, 정상적인 사용자가 의도치 않은 위조 요청을 보내는 것을 의미)
      • REST API 기반 환경은 session 기반 인증과 다르게 서버에 인증 정보를 보관하지 않고 권한 요청 시 필요한 인증정보 (OAuth, Jwt 토큰 등) 요청을 포함하기 때문에 활성화 할 필요 없으므로 disable 처리하면 된다.
    • sessionManagement() : 세션 생성 및 사용 여부에 대한 정책 설정
      • SessionCreationPolicy() : 세션 정책 설정
      • SessionCreationPolicy.Stateless : 4가지 정책 중 하나로, 스프링 시큐리티가 생성하지 않고 존재해도 사용하지 않는다
    • cors (cross-site resouce sharing)  

AuthenticationManager : 인증을 관리하는 컴포넌트

  • DaoAuthenticationProvider : 데이터베이스에서 사용자 정보를 가져오고, 비밀번호를 확인
  • bCryptPasswordEncoder : 비밀번호 암호화 설정
  • ProviderManager(authProvider) : AuthenticationManager를 생성하고 사용자 정보를 제공하는 authProvider 설정

BCryptPasswordEncoder : 비밀번호를 암호화하는데 사용

 

4. UserRepository 생성

package com.blog.repository;

import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.blog.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
	Optional<User> findByEmail(String email);
}


이메일을 통해 사용자가 존재하는지 확인할 것이므로 해당 메소드를 추가해주었다.

 

5. UserDetailsServiceImpl 생성

package com.blog.service;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import com.blog.entity.User;
import com.blog.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
	private final UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String email) {
		User user = userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException(email));
		return user;
	}

}

 

Spring Security의 UserDetailsService 인터페이스를 구현하는 서비스 클래스이다.

UserDetailsService에 정의된 loadUserByUsername 메서드롤 오버라이드 해서 사용한다.

여기서 email이 User 엔티티의 식별자로 사용된다.

 

orElseThrow()를 통해 사용자가 존재하지 않을 경우 예외 처리를 해준다.

예외 발생 시, 이메일을 메세지로 포함시켜 어떤 이메일로 조회했는지 확인할 수 있게 한다.

 

user 객체로 반환하여 스프링 시큐리티가 이 객체를 사용하여 사용자 인증을 처리한다.

 

6. UserService, UserServiceImpl 생성

@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
	private final UserRepository userRepository;
	private final BCryptPasswordEncoder bCryptPasswordEncoder;

	@Override
	public Long save(User user) {
		user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
		return userRepository.save(user).getId();
	}

}

 

UserService는 인터페이스라 따로 코드를 적지 않고, 위의 코드는 UserServieImple이다.

회원가입 할 때, 비밀번호를 bcrpt를 통해 암호화해서 저장하도록 구현했다.

 

7. UserController 생성

@Controller
@RequiredArgsConstructor
public class UserController {
	private final UserService userService;

	// 로그인, 회원가입 화면
	@GetMapping("/login")
	public String loginPage() {
		return "login";
	}

	@GetMapping("/join")
	public String joinPage() {
		return "join";
	}

	// 회원가입 동작
	@PostMapping("/join")
	public String join(User user) {
		userService.save(user);
		return "redirect:/login";
	}

	// 로그아웃 동작
	@GetMapping("/logout")
	public String logout(HttpServletRequest req, HttpServletResponse res) {
		new SecurityContextLogoutHandler().logout(req, res, SecurityContextHolder.getContext().getAuthentication());
		return "redirect:/login";
	}

}

 

로그인은 따로 적지 않았고, 회원가입과 로그아웃 동작을 추가해주었다.

로그아웃은 그냥 동작하는 것이기 때문에 GetMapping을 사용했다.

 

 

 

 

 

 


 

생각보다 너무 길어졌다...

로그인, 회원가입, 로그아웃 기능 구현하는게 이렇게 어려울줄 몰랐다.

코드 하나하나 뜯어보면서 이해하려고 했는데 아직은 잘 모르겠다. 그래도 해야겠지만...

이제 이걸 잘 활용해서 다른 프로젝트를 만들거나 할 때 사용해야겠다. 아직도 시큐리티는 어렵구나...ㅎ

 

참고한 블로그:

https://velog.io/@dh1010a/Spring-Spring-Security%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-3.X-%EB%B2%84%EC%A0%84-1

 

[Spring] Spring Security를 이용한 로그인 구현 (스프링부트 3.X 버전) [1] - 동작 원리 및 Config 설정

스프링 부트 3.0 이상 버전의 시큐리티 사용법 및 바뀐 Config 작성법을 다루고 있습니다.

velog.io

https://devhan.tistory.com/310?category=1111057

 

스프링부트 3.X 스프링 시큐리티 사용해서 회원가입, 로그인, 로그아웃 구현하기

스프링 시큐리티? 스프링 시큐리티는 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크이다. 인증(Authentication)? 인증은 사용자의 신원을 입증하는 과정이

devhan.tistory.com