Spring Security : 로그인

금은체리·2023년 11월 16일
2

Spring

목록 보기
18/49

로그인 처리 과정 이해

  • 스프링 시큐리티 사용 전

  • 스프링 시큐리티 사용 후

    • Client의 요청은 모두 Spring Security를 거치게 됨
    • Spring Security 역할
      • 인증/인가
        1. 성공 시: Controller로 Client 요청 전달
          • Client 요청 + 사용자 정보 (UserDetails)
        2. 실패 시: Controller로 Client 요청 전달되지 않음
          • Client에게 Error Response 보냄
  • 로그인 처리 과정

    1. Client
      a. 로그인 시도
      b. 로그인 시도할 username, password 정보를 HTTP body로 전달(post 요청)
      c. 로그인 시도 URL은 WebSecurityConfig 클래스에서 변경 가능
      * 아래와 같이 설정 시 "POST/api/user/login"로 설정됨

        ```
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            // CSRF 설정
            http.csrf((csrf) -> csrf.disable());
      
           http.authorizeHttpRequests((authorizeHttpRequests) ->
                    authorizeHttpRequests
                            .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                            .anyRequest().authenticated() // 그 외 모든 요청 인증처리
            );
      
           // 로그인 사용
             http.formLogin((formLogin) ->
                    formLogin
                         // 로그인 처리 (POST /api/user/login)
                        .loginProcessingUrl("/api/user/login").permitAll()
            );
      
            return http.build();
        }
        ```

      • .loginPage()
        • 우리가 만든 로그인 페이지 제공 가능
      • .loginProcessingUrl()
        • Security 앞 단 쪽에서 처리
      • .loginSuccessUrl()
        • 성공하면 메인 페이지로 이동
      • .failureUrl()
        • 실패하면 ~?error로 반환

    1. 인증 관리자(Authentication Manager)

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


    1. UserDetailsService
      a. 회원 DB에서 회원 조회
      User user = userRepository.findByUsername(username)
              .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
      • 회원 정보가 존재하지 않을 시 ➡️ Error 발생

    b. 조회된 회원 정보(user)를 UserDetails로 변화

    UserDetails userDetails = new UserDetailsImpl(user)

    c. UserDetails를 "인증 관리자"에게 전달


    1. "인증 관리자"가 인증 처리
      a. 아래 2개의 username, password 일치 여부 확인
      • Client가 로그인 시도한 username, password
      • UserDetailsService가 전달해준 UserDetails의 username, password
        b. password 비교 시
        • Client가 보낸 password는 평문이고, UserDetails의 password는 암호문
        • Client가 보낸 password를 암호화해서 비교
      c. 인증 성공 시 ➡️ 세션에 로그인 정보 저장
      d. 인증 실패 시 ➡️ Error 발생

로그인 구현

1. 로그인 구현

  • 우리가 직접 Filter를 구현해서 URL 요청에 따른 인가를 설정한다면
  • 코드가 매우 복잡해지고 유지보수 비용이 많이 들 수 있음
  • Spring Security를 사용하면 이러한 인가 처리가 매우 편해짐
    • requestMatchers("/api/user/**").permitAll()
      • 이 요청들은 로그인, 회원가입 관련 요청이기 때문에 비회원/회원 상관없이 누구나 접근 가능해야함
      • 이렇게 인증이 필요 없는 URL들을 간편하게 허가 가능
    • anyRequest().authenticated()
      • 인증이 필요한 URL들도 간편하게 처리 가능

2. DB의 회원 정보 조회 ➡️ Spring Security의 "인증 관리자"에게 전달

  • UserDetailsService 구현
    • UserDetailsService 인터페이스 ➡️ UserDetailsServiceImpl
package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

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

public class UserDetailsImpl implements UserDetails {

   private final User user;

   public UserDetailsImpl(User user) {
       this.user = user;
   }

   public User getUser() {
       return user;
   }

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

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

   @Override
   public Collection<? extends GrantedAuthority> >getAuthorities() {
       UserRoleEnum role = user.getRole();
       String authority = role.getAuthority();

       SimpleGrantedAuthority simpleGrantedAuthority = >new SimpleGrantedAuthority(authority);
       Collection<GrantedAuthority> authorities = new >ArrayList<>();
       authorities.add(simpleGrantedAuthority);

       return authorities;
   }

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

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

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

   @Override
   public boolean isEnabled() {
       return true;
   }
}
  • UserDetails 구현
    • UserDetails 인터페이스 ➡️ UserDetailsImpl

  • UserDetailsService와 UserDetails를 직접 구현해서 사용하게 되면 Security의 default 로그인 기능을 사용하지 않겠다는 설정이 됨
    • Security의 password를 더 이상 제공하지 않는 것 확인 가능!
  • POST "/api/user/login"을 로그인 인증 URL로 설정했기 때문에 이제 해당 요청이 들어오면
    • 우리가 직접 구현한 UserDetailsService를 통해 인증 확인 작업이 이뤄지고
      • 인증 객체에 직접 구현한 UserDetails가 담기게 됨

@AuthenticationPrincipal

@Controller
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/products")
    public String getProducts(@AuthenticationPrincipal UserDetailsImpl userDetails) {
        // Authentication 의 Principal 에 저장된 UserDetailsImpl 을 가져옵니다.
        User user =  userDetails.getUser();
        System.out.println("user.getUsername() = " + user.getUsername());

       return "redirect:/";
    }
}
  • @AuthenticationPrincipal
    • Authentication의 Principal에 저장된 UserDetailsImpl을 가져올 수 있음
    • UserDetailsImpl에 저장된 인증된 사용자인 User 객체 사용 가능
  • @AuthenticationPrincipal 사용해서 메인 페이지 사용자 이름 반영하기
@Controller
public class HomeController {
    @GetMapping("/")
    public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        // 페이지 동적 처리 : 사용자 이름
        model.addAttribute("username", userDetails.getUser().getUsername());

        return "index";
    }
}
profile
전 체리 알러지가 있어요!

0개의 댓글