Spring Security 기본

김병수·2022년 11월 23일
0
post-thumbnail

Spring Security

Spring MVC 기반 애플리케이션의 인증과 인가 기능을 지원하는 보안 프레임워크. 다양한 유형의 사용자 인증 기능을 적용, 사용자 역할에 따른 권한 지정, 리소스 접근 제어, 데이터 암호화 등의 보안 강화 기능을 제공한다.

Spring Security 웹 요청 처리 흐름

일반적인 처리 흐름


웹 페이지 접속 시 리소스를 요청하면 인증 관리자가 비밀번호를 요청한다. 이를 크리덴셜이라고 하는데 이를 비교 검증을 수행한다. 접근 결정 관리자가 권한을 검증하여 접근 가능성을 판단하여 리소스 접근을 허용하게 된다.

서블릿 필터와 필터 체인의 역할

클라이언트로 부터 서버로 요청이 들어오기 전에 서블릿을 걸쳐 필터를 하는 것을 서플릿 필터라고 한다. 하나 이상의 필터들을 연결해 구성한 것을 필터 체인이라 부른다. 각각의 필터들이 doFilter() 메서드를 구현하여, 해당 메서드 호출을 통해 필터 체인을 형성한다. 서블릿 필터의 모든 필터 작업을 수행 후, HttpServlet을 거쳐 DispatcherServlet에 요청이 전달된다.

Spring Security 기본 구조

의존 라이브러리 추가

dependencies {

	implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

}

Spring Security를 적용하기위해 의존 라이브러리를 추가한다.

user 인증

@Configuration
public class SecurityConfiguration {
    @Bean
    public UserDetailsManager userDetailsService() {
        UserDetails userDetails =
                User.withDefaultPasswordEncoder()    // 패스워드 1111 암호화 
                //(테스트 환경에서만 사용)
                        .username("kevin@gmail.com") 
                        .password("1111")            
                        .roles("USER")               
                        .build();
        //객체를 Bean으로 등록하면 해당 인증정보가 클라이언트 요청으로 넘어오면 정상적인 인증 프로세스 수행
        return new InMemoryUserDetailsManager(userDetails); 
    }
}

커스텀 로그인 페이지 지정하기

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()                 // csrf공격 비활성화
            .formLogin()                      // 기본 인증 방법을 폼 로그인으로 지정
            .loginPage("/auths/login-form")   // 로그인 페이지 URL
            .loginProcessingUrl("/process_login")    // 로그인 인증 요청을 수행할 요청 URL지정
            .failureUrl("/auths/login-form?error")   // 로그인 실패 시 리다이렉트 화면
            .and()                                   // Spring Security 보안 설정을 메서드 체인 형태로 구성
            .authorizeHttpRequests()      //접권 권한을 확인하겠다고 정의    
            .anyRequest()                            
            .permitAll();                 // 클라이언트의 모든 요청에 대한 접근 허용        
        return http.build();
    }

    @Bean
    public InMemoryUserDetailsManager userDetailsService() {
        UserDetails user =
                User.withDefaultPasswordEncoder()
                        .username("kevin@gmail.com")
                        .password("1111")
                        .roles("USER")
                        .build();
        return new InMemoryUserDetailsManager(user);
    }
}

접근 권한 부여

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .formLogin()
            .loginPage("/auths/login-form")
            .loginProcessingUrl("/process_login")
            .failureUrl("/auths/login-form?error")
            .and()
            .exceptionHandling().accessDeniedPage("/auths/access-denied")   // 권한이 없는 사용자에 403에러
            .and()
            .authorizeHttpRequests(authorize -> authorize                  // request URI에 대한 접근 권한 부여
             		// ADMIN만 접근 가능
                    // /orders로 시작하는 모든 URL에 접근 가능
                    .antMatchers("/orders/**").hasRole("ADMIN")        
                    .antMatchers("/members/my-page").hasRole("USER")   
                    // 앞에 지정한 URL 이외에 모든 URL은 Role에 상관없이 접근 가능
                    .antMatchers("⁄**").permitAll()                    
            );
        return http.build();
    }
}

로그아웃

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .formLogin()
            .loginPage("/auths/login-form")
            .loginProcessingUrl("/process_login")
            .failureUrl("/auths/login-form?error")
            .and()
            .logout()                        // 로그아웃 설정을 위해 호출
            .logoutUrl("/logout")            // 로그아웃을 수행하기 위한 request URL
            .logoutSuccessUrl("/")  // 로그아웃 후 리다이렉트 URL
            .and()
            .exceptionHandling().accessDeniedPage("/auths/access-denied")
            .and()
            .authorizeHttpRequests(authorize -> authorize
                    .antMatchers("/orders/**").hasRole("ADMIN")
                    .antMatchers("/members/my-page").hasRole("USER")
                    .antMatchers("⁄**").permitAll()
            );
        return http.build();
    }

}

회원가입

// MemberService를 구현하여 메모리에 멤버 정보 저장, @Service가 아닌  JavaConfiguration을 이용해 Bean을 등록한다
public class InMemoryMemberService implements MemberService {  
    private final UserDetailsManager userDetailsManager;
    private final PasswordEncoder passwordEncoder;

    public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
        this.userDetailsManager = userDetailsManager;
        this.passwordEncoder = passwordEncoder;
    }

    public Member createMember(Member member) {
        // 권한 지정
        List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());

        // 패스워드 암호화
        String encryptedPassword = passwordEncoder.encode(member.getPassword());

        // user로 등록하기 위한 UserDetails 생성
        UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);

        // 유저 등록
        userDetailsManager.createUser(userDetails);

        return member;
    }

    private List<GrantedAuthority> createAuthorities(String... roles) {
      
        return Arrays.stream(roles)
                .map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }
}

DB연동을 통한 인증 처리

@Component
//UserDetailsService 구현체
public class HelloUserDetailsService implements UserDetailsService {   
    private final MemberRepository memberRepository;
    private final HelloAuthorityUtils authorityUtils;

    // DB에서 User를 조회하고 권한 정보를 생성하기 위한 DI
    public HelloUserDetailsServiceV1(MemberRepository memberRepository, HelloAuthorityUtils authorityUtils) {
        this.memberRepository = memberRepository;
        this.authorityUtils = authorityUtils;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> optionalMember = memberRepository.findByEmail(username);
        Member findMember = optionalMember.orElseThrow(() -> new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));

        // 이메일 정보를 이용해 Role 기반의 권한 정보(GrantedAuthority) 컬렉션
        Collection<? extends GrantedAuthority> authorities = authorityUtils.createAuthorities(findMember.getEmail());

        // DB에서 조회한 User 클래스의 객체를 리턴하여 Spring Security가 인증 절차 수행
        return new User(findMember.getEmail(), findMember.getPassword(), authorities);
    }
}

User 정보를 로드하는 핵심 인터페이스인 UserDetailsService를 사용한다.

profile
BE 개발자를 꿈꾸는 대학생

0개의 댓글