[Spring] Spring Security : 로그인

thingzoo·2023년 6월 28일
0

Spring

목록 보기
39/54
post-thumbnail

로그인 처리 과정 이해

스프링 시큐리티 사용 전

스프링 시큐리티 사용 후

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

로그인 처리 과정

1. Client

  1. 로그인 시도
  2. 로그인 시도할 username, password 정보를 HTTP body 로 전달 (POST 요청)
  3. 로그인 시도 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();
}

2. 인증 관리자 (Authentication Manager)

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

3. UserDetailsService

  1. 회원 DB 에서 회원 조회
    • 회원 정보가 존재하지 않을 시 → Error 발생
User user = userRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
  1. 조회된 회원 정보(user) 를 UserDetails 로 변환
UserDetails userDetails = new UserDetailsImpl(user)
  1. UserDetails 를 "인증 관리자"에게 전달

4. "인증 관리자"가 인증 처리

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

로그인 구현

1. 로그인 처리 URL 설정

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 사용
        http.formLogin((formLogin) ->
                formLogin
                        // 로그인 View 제공 (GET /api/user/login-page)
                        .loginPage("/api/user/login-page")
                        // 로그인 처리 (POST /api/user/login)
                        .loginProcessingUrl("/api/user/login")
                        // 로그인 처리 후 성공 시 URL
                        .defaultSuccessUrl("/")
                        // 로그인 처리 후 실패 시 URL
                        .failureUrl("/api/user/login-page?error")
                        .permitAll()
        );

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

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

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

UserDetailsService 구현

UserDetailsService 인터페이스 → UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        return new UserDetailsImpl(user);
    }
}

UserDetails 구현

UserDetails 인터페이스 → UserDetailsImpl

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;
    }
}

@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개의 댓글