Spring/팀스파르타

30. Spring Security 로그인

열심히 해 2024. 11. 20. 18:42

Spring Security를 사용한다면 Client 의 요청은 모두 Spring Security 를 거치게 됩니다.


Spring Security 역할

  • 인증/인가
    1. 성공 시: Controller 로 Client 요청 전달
      1. Client 요청 + 사용자 정보 (UserDetails)
    2. 실패 시: Controller 로 Client 요청 전달되지 않음
      1. Client 에게 Error Response 보냄

 

 

로그인 처리 과정

 

Client

  1. 로그인 시도
  2. 로그인 시도할 username, password 정보를 HTTP body 로 전달 (POST 요청)
  3. 로그인 시도 URL 은 WebSecurityConfig 클래스에서 변경 가능
    • 아래와 같이 설정 시 "POST /api/user/login" 로 설정됩니다.

 

인증 관리자 (Authentication Manager)

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

 

 UserDetailsService

  • 회원 DB 에서 회원 조회
    1. 조회된 회원 정보(user) 를 UserDetails 로 변환
    2. 회원 정보가 존재하지 않을 시 → Error 발생

 

**인증 관리자**가 인증 처리

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

 

 


로그인 구현 

 

1. 로그인 처리 URL 설정

package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@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를 사용하면 이러한 인가 처리가 굉장히 편해집니다.
    • requestMatchers("/api/user/**").permitAll()
      • 이 요청들은 로그인, 회원가입 관련 요청이기 때문에 비회원/회원 상관없이 누구나 접근이 가능해야합니다.
      • 이렇게 인증이 필요 없는 URL들을 간편하게 허가할 수 있습니다.
    • anyRequest().authenticated()
      • 인증이 필요한 URL들도 간편하게 처리할 수 있습니다.

 

 

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

 

2-1. UserDetailsService 인터페이스 → UserDetailsServiceImpl

package com.sparta.springauth.security;

import com.sparta.springauth.entity.User;
import com.sparta.springauth.repository.UserRepository;
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;

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

 

 

2-2. UserDetails 인터페이스 → UserDetailsImpl

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

 

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

 

 

UserDetailsImpl에 담긴 회원 정보(user)를 Controller에서 가져다 사용할 수 있습니다.

 

@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 객체를 사용할 수 있습니다.

 

'Spring > 팀스파르타' 카테고리의 다른 글

33. RestTemplate이란 무엇일까?  (0) 2024.11.25
31. Spring Security: JWT 로그인  (0) 2024.11.21
28. 필터란 무엇일까?  (0) 2024.11.16
27. JWT 다루기  (1) 2024.11.15
26. 쿠키와 세션이란 무엇일까?  (0) 2024.11.13