Spring Security

ys·2024년 3월 16일
  • 스프링 시큐리티(Spring Security) : 스프링 기반의 애플리케이션 보안을 담당하는 스프링 하위 프레임워크이다
  • 인증 (authentication) : 사용자의 신원을 입증하는 과정
    • 로그임을 할 때 누구인지 확인하는 과정
  • 인가 (authorization): 사이트의 특정 부분에 접근할 수 있는 권한을 확인하는 과정
    • 일반 사용자는 관리자 페이지에 접근 할 수 없다

인증,인과 관련 코드를 아무런 도움 없이 작성하면 어렵고, 시간도 많이 든다
스프링 시큐리티를`사용하면 쉽게 처리할 수 있다

Spring Security

  • 인증, 인가 기능을 제공하는 스프링 하위 프레임 워크
  • 보안 관련 옵션을 많이 제공 (csrf)
  • ✅필터 기반으로 동작
  • 눈여겨봐야 할 2개 필터
  • UsernamePasswordAuthenticationFilter : 아이디, 패스워드가 넘어오면 인증 요청을 위임하는 ✅인증 관리자 역할
  • FilterSecurityInterceptor : 권한 부여 처리를 위임해, 접근 제어 결정을 쉽게 하는 ✅접근 결정 관리자 역할

여러 Filter들

  • 여러번 보면서 눈에 익혀놓자

Spring Security 인증 처리 절차

  1. 먼저 http 요청이 들어오면 아까 봤던 UsernamePasswordAuthenticationFilter에서 요청에 들어온 아이디,비밀번호의 유효성 검사를 한다
  2. 유효성 검사가 끝나면, ✅인증용 객체구현체 UsernamePasswordAuthenticationToken을 만들어 넘겨준다
  3. 인증용 객체인 UsernamePasswordAuthenticationTokenProviderManager을 구현한 AuthenticationManager (인증 메니저)로 보내준다
  4. UsernamePasswordAuthenticationTokenAuthenticationProvider로 다시 보내준다
  5. 이때 AuthenticationProvider는 id를 가지고 인터페이스인 UserDetailService를 이용해 db에서 해당 id를 가진 객체를 찾는다

이때, 해당 entity의 service는 UserDetailService를 구현하고 있다

  1. 해당 엔티티인 User는 UserDetails라는 인터페이스를 구현하고 있고, Solid의 LSP원칙을 지켜서 UserDetail객체를 AuthenticationProvider로 전달한다

    이 때, 전달되는 UserDetailService를 구현한 해당 엔티티의 Service 계층의 findById를 이용해, 찾은 객체이다

7.이때 입력받은 정보와 UserDetail의 ✅정보를 비교해, 인증 처리를 완료한다
8. 인증 성공 -> AuthenticationSuccessHandler
9. 인증 실패 -> AuthenticationFailureHandler 를 실행

의존성 추가

// 스프링 시큐리티를 사용하기 위한 스타터 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
// 타임리프에 스프링 시큐리티를 사용하기 위한 의존성 추가
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurit6'
// 스프링 시큐리티를 테스트하기 위한 의존성 추가
testImplementation 'org.springframework.security:spring-security-test'

엔티티에 ✅UserDetail 구현

public class User implements UserDetails {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id",updatable = false)
    private Long id;

    private String email;
    private String password;
    ...
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("user"));
    }

    @Override
    public String getUsername() {
        return email;
    }

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

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

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

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

}
    
  • getAuthorities : Collection<? extends GrantedAuthority>타입 반환
    • 사용자가 가지고 있는 권한의 목록을 반환
  • getUsername : String 반환
    • 사용자가 식별할 수 있는 사용자 이름 반환
    • 반듯이 unique속성이어야 할 것
  • isAccountNonExpired : boolean타입 반환
    • 계정이 만료되었는지 확인하는 메서드
    • 만료되지 않으면 true
  • isCredentialsNonExpired : boolena타입 반환
    • 비밀번호가 만료되었는지 확인하는 메서드
    • 만료되지 않으면 true
  • isEnabled() : boolean타입 반환
    • 계정이 사용가능 한지 확인하는 메서드
    • 사용 가능하다면 true

UserRepository

public interface UserRepository extends JpaRepository<User,Long> {

    public Optional<User> findByEmail(String email);
}
  • 아이디를 가지고 db에서 entity를 가져오기 위한
  • 메서드를 하나 구현한다, 여기서는 email을 이용함

✅UserDetailService를 구현하는 UserDetailService

@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new ApiException(UserError.NOT_FOUND_USER));
    }
}
  • 실제 service가 아닌, spring security에서
  • UsernamePasswordAuthenticationToken를 확인하기 위해서 사용하는 service
  • UserDetailsService를 구현하고,
  • UserDetails를 반환하는 loadUserByUsername 메서드를 오버라이드 한다

Security Config

@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {

    private final UserDetailService userDetailsService;

    @Bean
    public WebSecurityCustomizer configure(){
        return web -> web.ignoring()
                .requestMatchers("/static/**");
    }
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests()
                .requestMatchers("/login", "/signup", "/user").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin((login)->login
                        .loginPage("/login")
                        .defaultSuccessUrl("/articles"))
                .logout((logout)->logout
                        .logoutSuccessUrl("/login")
                        .invalidateHttpSession(true))
                .csrf(AbstractHttpConfigurer::disable) // csrf 비활성화
                .build();
    }

    // 패스워드 인코더로 사용할 빈 등록
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //인증 관리자 관련 설정

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();

        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());

        return daoAuthenticationProvider;
    }


    @Bean
    public AuthenticationManager authenticationManager() throws Exception {//2 - AuthenticationManager 등록
        DaoAuthenticationProvider provider = daoAuthenticationProvider();//DaoAuthenticationProvider 사용
        return new ProviderManager(provider);
    }
}
  • 먼저 예제 코드는 security 6.1버전부터 많은 부분이 바뀌었다
  • 메서드 체이닝 방식보다는 ✅람다식 방식으로 바뀌어서 처음 예제 코드를 작성하면 오류가 많이 났다...
  • 먼저 static에 대해서는 스프링 시큐리티 기능을 비활성화 한다
  • ✅filterChain이 부분에 대해서 자세히 봐보자
.requestMatchers("/login", "/signup", "/user").permitAll()
                .anyRequest().authenticated()
  • 다음 부분을은 누구나 접근을 가능(permitAll)하게 한다
  • 위에서 설정한 url제외(anyRequest())는 별도의 인가는 필요하지 않지만 인증을 해서 접근 할 수 있다(authenticated())
.formLogin((login)->login
                        .loginPage("/login")
                        .defaultSuccessUrl("/articles"))
  • 이부분 부터 람다식 형태로 바뀌었다
  • loginpage는 "/login"이고
  • 성공기 "/articles"로 이동한다
  • 지금은 화면을 보기 위해서 thymeleaf를 이용한 html 페이지를 주었지만
  • jwt토큰까지 완성하고, 전달 dto를 만든 후에는 🤔json즉 Api형식으로 바꿔볼 예정이다
.logout((logout)->logout
                        .logoutSuccessUrl("/login")
                        .invalidateHttpSession(true))
  • 로그아웃도 완료시 "/login"으로 이동하고
  • 로그 아웃후 세션을 전체 삭제한다고 설정하였다
.csrf(AbstractHttpConfigurer::disable)
  • csrf공격을 방지하는 기능을 비활성화 해두었다
  • 패스워드 인코더를 Bean으로 등록하고, bCryptPasswordEncoder를 이용한다

인증 관리자

  • 인증 관리를 해주는 AuthenicationProvider는 daoAuthenticationProvider를 사용하고
  • 아까 만들어둔 ✅userDetailsService로 token의 유효성 검사를 진행한다
  • 비밀번호 인코더도 빈으로 등록해둔 bCryptPasswordEncoder를 사용한다

인증 매니저

  • 인증 매니저인 authenticationManager 아까 만든 인증 관리자인 AuthenicationProvider 객체를 가져와서 사용한다

AuthenticationSuccessHandler , AuthenticationFailureHandler

  • 이렇게 인증이 성공된다면
  1. html + 템플릿 엔진 : 성공 페이지로 이동한다
  2. Rest Api : ✅성공 log + jwt token을 담아서 응답 메시지로 전달한다
  • 실패한다면
  1. html + 템플릿 엔진 : error 페이지로 이동한다
  2. Rest Api : 실패시, Exception handler로 이동해 예외 처리를 해서 해당 예외를 담은 Api를 응답 메시지로 전달한다

다음...

  • filter 기반의 serurity이므로 인증,인가 처리를 미리 할 수 있었다
  • 이제 다음엔 jwt token을 이용해
  • filter 레벨에서 인증에 성공했다면 🤔token을 받은 후 토큰을 응답 메시지에 넣을 수 있게 추가 해보자!
profile
개발 공부,정리

0개의 댓글