SpringSecurity 를 이용한 로그인, 로그아웃

Hyun·2023년 9월 17일
1

Spring Security

스프링 시큐리티는 스프링 기반 애플리케이션의 인증과 권한을 담당하는 스프링의 하위 프레임워크이다.

  • 인증(Authentication)은 로그인을 의미한다.
  • 권한(Authorize)은 인증된 사용자가 어떤 것을 할 수 있는지를 의미한다.

먼저 스프링 시큐리티의 환경설정 파일인 SecurityConfig.java 를 살펴보자

@Configuration//스프링의 환경설정 파일임을 의미, 여기선 스프링 시큐리티의 설정을 위해  사용됨
@EnableWebSecurity//모든 요청 URL이 스프링 시큐리티의 제어를 받도록 함
                  // 내부적으로 SpringSecurityFilterChain이 동작하여 URL 필더가 적용됨
public class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                        .requestMatchers(new AntPathRequestMatcher("/**")).permitAll()) 
                .formLogin((formLogin)-> formLogin 
                        .loginPage("/user/login") 
                        .defaultSuccessUrl("/"))
                .logout((logout) -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true)) 
        ;
        return http.build();
    }

    @Bean
    PasswordEncoder passwordEncoder() {//PasswordEncoder는 BCryptPasswordEncoder 의 인터페이스임
        return new BCryptPasswordEncoder();
    }

    @Bean
    //스프링 시큐리티의 인증을 담당, 사용자 인증시 앞에서 작성한 UserSecurityServicePasswordEncoder 를 사용
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

스프링 시큐리티 설정 클래스의 이름은 자유롭게 지정할 수 있다. 다만 스프링 시큐리티 설정을 정의하는 클래스임을 나타내기 위해 클래스에 @Configuration 어노테이션과 @EnableWebSecurity 어노테이션을 추가해야 한다.

SecurityFilterChain

스프링 시큐리티의 세부 설정은 SecurityFilterChain 빈을 등록하여 설정한다. 만약 이 빈을 등록하지 않았다면, 기본적으로 모든 URL에 로그인이 필요하다. 따라서 어떤 URL에 접속하든 login 페이지가 나타나게 된다.

SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                        .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())

HttpSecurity 객체는 스프링 시큐리티의 주요 구성 객체 중 하나로, 웹 보안 설정을 정의한다. HttpSecurity 객체는 스프링 시큐리티 내부에서 자동으로 주입되는 객체이다.

authorizeHttpRequests 메서드는 HTTP 요청에 대한 권한 설정을 지정하는 부분이다. requestMatchers 는 특정 요청 매처(RequestMatcher)를 지정할 때 사용된다. 여기서는 'new AntPathRequestMatcher("/**")` 를 사용하여 모든 경로('/**') 에 대한 설정을 지정하고 있다.

permitAll() 은 해당 요청에 대한 모든 사용자에게 접근 권한을 부여한다는 것을 의미한다. 즉 로그인 여부와 상관없이 누구나 해당 URL 에 접근할 수 있다. *이 코드는 인증을 필요로 하지 않는 모든 요청에 대한 권한을 부여하는데 사용된다.

만약 authorizeHttpRequests 블록을 추가하지 않으면 모든 URL 에 대한 접근이 허용되며, 따라서 로그인 없이도 모든 URL 에 접근할 수 있게 된다.

정리(URL 접근에 한함)
1) SecurityFilterChain 빈 등록 X
-> 로그인 없이도 모든 URL에 대한 접근 권한 허용
2) SecurityFilterChain 빈 등록 O, authorizeHttpRequests 블록 추가 X
-> 로그인 없이도 모든 URL에 대한 접근 권한 허용
3) SecurityFilterChain 빈 등록 O, authorizeHttpRequests 블록 추가 O
-> 특정 URL 에 대해 접근 권한을 설정할 수 있게 됨

http
                
                .formLogin((formLogin)-> formLogin //formLogin 은 스프링 시큐리티의 로그인 설정을 담당하는 부분
                        .loginPage("/user/login") //로그인 페이지 URL 지정
                        .defaultSuccessUrl("/"))

formLogin 메서드는 스프링 시큐리티의 로그인 설정을 구성하는 부분이다. .logipPage 는 로그인 페이지의 URL 을 지정하는 부분이다. 사용자가 로그인을 시도할때 redirection 되는 로그인 페이지의 URL을 '/user/login' 으로 지정하고 있다. (MVC 패턴 사용 시) 해당 URL 을 Controller 에서 아래와 같이 로그인 폼 html 파일과 매핑시켜주면 된다.

defaultSuccessUrl 메서드는 로그인 성공 후 이동할 페이지의 URL 을 지정하는 부분이다.

PassEncoder

@Bean//암호화를 위한 BCryptPasswordEncoder 객체를 빈으로 등록하여 사용
    PasswordEncoder passwordEncoder() {//PasswordEncoder는 BCryptPasswordEncoder 의 인터페이스임
        return new BCryptPasswordEncoder();
    }

SpringSecurity.java 클래스에 회원가입 및 로그인 시 유저 정보 일치 확인을 위해 사용되는 PassEncoder 객체를 스프링 빈으로 등록하여 체계화하였다.

AuthenticationManager

@Bean
authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

스프링 시큐리티를 사용하면 '/user/login' 엔드포인트로 POST 요청을 보내면 스프링 시큐리티가 내부적으로 'authenticationManager' 빈을 사용하여 로그인 처리를 한다.

이때 authenticationManager 빈은 사용자 인증시 앞에서 작성한 UserSecurityServicePasswordEncoder 를 사용한다. 어떻게 사용하는지는 아래 로그인 과정을 설명하는 부분에서 다루겠다.

로그인 과정

먼저 사용자 로그인을 위한 UserSecutiryService 클래스와 login_form html 파일을 보여주겠다.

UserSecurityService.java

@Service
public class UserSecurityService implements UserDetailsService {

    private final UserRepository userRepository;

    public UserSecurityService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<SiteUser> _siteUser = this.userRepository.findByUsername(username);
        //해당 이름의 사용자 db에서 못찾은 경우
        if (_siteUser.isEmpty()) {
            throw new UsernameNotFoundException("사용자를 찾을 수 없습니다");
        }
        //해당 이름의 사용자 db에서 찾은 경우
        SiteUser siteUser = _siteUser.get();//해당 유저 엔티티를 Optional 객체에서 꺼냄
        List<GrantedAuthority> authorities = new ArrayList<>();
        if ("admin".equals(username)) {//사용자명이 admin 인 경우 ADMIN 권한 부여
            authorities.add(new SimpleGrantedAuthority(UserRole.ADMIN.getValue()));
        } else {//그 외엔 USER 권한 부여
            authorities.add(new SimpleGrantedAuthority(UserRole.USER.getValue()));
        }
        return new User(siteUser.getUsername(), siteUser.getPassword(), authorities);//SiteUser 객체 아님!!
    }
}

login_form.html

...
<form th:action="@{/user/login}" method="post">
    <div th:if="${param.error}"><!-- 시큐리티의 로그인 실패시, 로그인 페이지로 에러와 함께 redirect 된다-->
      <div class="alert alert-danger">
        사용자ID 또는 비밀번호를 확인해 주세요.
      </div>
    </div>
    <div class="mb-3">
      <label for="username" class="form-label">사용자ID</label>
      <input type="text" name="username" id="username" class="form-control">
    </div>
    <div class="mb-3">
      <label for="password" class="form-label">비밀번호</label>
      <input type="password" name="password" id="password" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">로그인</button>
  </form>
...

로그인 과정은 다음과 같이 이루어진다.

  1. 사용자가 로그인 폼에서 사용자 이름(username)과 비밀번호(password)를 입력하고 '/user/login'으로 POST 요청을 보낸다.
  2. 스프링 시큐리티는 해당 요청을 가로채고 authenticationManager 빈을 사용하여 사용자의 인증(authentication) 을 시도한다. 이때 UserSecurityService 빈에서 구현한 loadByUsername 메서드를 호출하여 사용자 정보를 검색한다.
  3. loadByUsername 메서드에서는 입력된 사용자 이름(username)을 기반으로 사용자 정보를 DB에서 조회하고, 사용자 정보와 권한 정보를 담은 User 객체를 반환한다.
    *UserDetails 인터페이스는 스프링 시큐리티에서 사용자 정보를 담는 일반적인 인터페이스이다. User 클래스는 UserDetails 인터페이스의 구현 클래스이다.
  4. 스크링 시큐리티는 PasswordEncoder 빈을 이용해 로그인 폼에서 입력된 비밀번호(password)와 SpringSecurtyServiceloadUserByUsername 메서드로부터 반환된 User 객체에 들어있는 비밀번호(DB에서 조회한 비밀번호)를 비교하여 인증 여부를 판단한다.
    *회원가입 시에 PasswordEncoder 빈을 이용해 비밀번호를 암호화했기 때문에 로그인 시 입력받은 비밀번호를 동일하게 PasswordEncoder 빈을 이용해 암호화하여 UserDetails 객체의 passsword 와 비교한다.
  5. 인증에 성공하면 로그인 세션을 설정하고 defaultSuccessUrl로 지정된 페이지로 이동하거나 기존 요청을 처리한다.

3줄 요약(핵심 정리)

  1. 로그인 폼에서 '/user/login' 으로의 POST 요청을 스프링 시큐리티가 가로채서 authenticationManage 빈을 사용해 사용자 인증을 시도한다.
  2. 이때 UserSecurityService 빈의 loadByUsername 메서드에서 POST 요청의 username 을 이용해 db에 해당 이름의 사용자를 조회한 후,
  3. PasswordEncoder 빈으로 POST 요청의 password 와 db에서 찾은 사용자의 비밀번호를 비교해 로그인 성공 유무를 판단한다.

스프링 시큐리티를 사용하면 로그인 처리와 인증 부분을 authenticationManager, UserSecurityService, 그리고 passwordEncoder 빈들을 설정함으로써 구현할 수 있습니다. 이를 통해 보안성을 향상시키고 로그인 인증과 관련된 작업을 효율적으로 처리할 수 있습니다.

세부적으로, 스프링 시큐리티는 authenticationManager를 중심으로 로그인 및 인증 처리를 수행합니다. 이 과정에서 UserSecurityService는 사용자 정보를 데이터베이스에서 가져오고, passwordEncoder는 사용자가 입력한 비밀번호를 암호화하거나 저장된 비밀번호와 비교하는 데 사용됩니다. 개발자는 컨트롤러에서 별도의 로그인 처리 코드를 작성할 필요 없으며, 스프링 시큐리티가 이러한 빈들을 자동으로 활용하여 인증 작업을 처리합니다.

요약하면, 스프링 시큐리티는 authenticationManager를 중심으로 로그인 및 인증 처리를 하며, 이 과정에서 필요한 정보와 빈들을 활용합니다. 개발자는 직접 이 빈들을 호출하지 않고, 스프링 시큐리티가 로그인 요청을 가로채고 처리하는 방식으로 보안성을 높이고 효율적으로 로그인 인증 작업을 처리할 수 있습니다.

기타)

loadUserByUsername 메서드, passwordEncoder 빈에서 form 태그의 값을 읽어들이는 방법

login_form.html

<form th:action="@{/user/login}" method="post">
    <div th:if="${param.error}"><!-- 시큐리티의 로그인 실패시, 로그인 페이지로 에러와 함께 redirect 된다-->
      <div class="alert alert-danger">
        사용자ID 또는 비밀번호를 확인해 주세요.
      </div>
    </div>
    <div class="mb-3">
      <label for="username" class="form-label">사용자ID</label>
      <input type="text" name="username" id="username" class="form-control"><!-- 이 부분을 읽어들이게 됨!-->
    </div>
    <div class="mb-3">
      <label for="password" class="form-label">비밀번호</label>
      <input type="password" name="password" id="password" class="form-control">
    </div>
    <button type="submit" class="btn btn-primary">로그인</button>
  </form>

loadUserByUsername

loadUserByUsername 메서드에서 사용자 이름(아이디)을 가져오려면 해당 메서드의 매개변수 이름을 로그인 폼의 name 속성과 일치시켜야 한다. 따라서 일치시켜주기만 하면 되기때문에 'name' 속성의 값을 꼭 "username" 으로 설정할 필요가 없고, loadUserByUsername 메서드는 설정된 'name' 속성의 값과 매개변수의 이름을 일치시켜 주기만 하면 된다(개발자가 임의로 'name' 속성의 값을 변경시킬 수 있다).
이를 통해 스프링 시큐리팅가 사용자가 로그인 폼에서 입력한 아이디를 매개변수로 전달해준다.

@Service
public class UserSecurityService implements UserDetailsService {
	...
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<SiteUser> _siteUser = this.userRepository.findByUsername(username);
        ...
    }
	...
}

passwordEncoder

passwordEncoder 는 db에서 찾은 유저 엔티티의 비밀번호와 로그인 폼에서 입력된 비밀번호를 비교하기 위해, 로그인 폼의 name 속성이 "password" 인 태그의 내용을 가져온다.
*passwordEncoder 는 폼에서 무조건 name 속성이 "password" 인 태그의 내용을 읽어오기 때문에 개발자가 임의로 name 속성의 값을 변경할 수 없다.

SecurityConfig.java

public class SecurityConfig {
    ...
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
	...
}

로그아웃 과정

public class SecurityConfig {
    @Bean//스프링 시큐리티의 세부 설정은 SecurityFilterChain 빈을 생성하여 설정
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
        http
                .logout((logout) -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true))

        ;
        return http.build();
    }

로그아웃 과정은 로그인 과정과 유사하게 로그아웃 요청을 처리할 URL 을 logoutRequestMathcer 를 통해 설정하고, 로그아웃 성공시 이동할 URL 을 logoutSuccessUrl 을 통해 설정한다. 또한 로그아웃시 기존에 생성된 사용자 세션도 invalidateHttpSession 을 통해 삭제하도록 처리하였다.

Thymeleaf 에서 로그인 상태 확인

위 포스트와 같이 Spring Security 에서 세션을 사용하는 경우, 로그인 성공시 세션 내부에 저장되는 SecurityContext 내부에 해당 유저에 대한 Authentication 객체를 저장한다. 이렇게 세션에 저장된 SecurityContext 에서 Authentication 객체를 추출하여 현재 사용자의 인증 정보를 확인할 수 있다.

'sec:authorize'는 Thymeleaf 템플릿 엔진에서 사용되는 스프링 시큐리티의 확장 기능 중 하나이다. sec:authorize는 내부적으로 SecurityContext에 접근하여 현재 사용자의 Authentication 객체를 확인하며, 이를 기반으로 로그인 상태를 판단한다.

  • isAnonymous(): 이 조건은 현재 사용자가 로그인되지 않은 경우에 참
  • isAuthenticated(): 이 조건은 현재 사용자가 로그인된 경우에 참

예시)

그렇다면 JWT 를 이용할때는?

JWT(JSON Web Token)는 주로 클라이언트 사이드 렌더링(Client-Side Rendering, CSR) 환경에서 사용된다. 타임리프와 같은 서버 사이드 렌더링 방식이 아닌 아닌 클라이언트 측에서 동적으로 화면을 업데이트하기 때문에 클라이언트 측에서 javascript 와 같은 도구를 사용하여 위와 같은 동작을 수행한다.

profile
better than yesterday

0개의 댓글