Spring Example: ToDo List #11 Spring Security

함형주·2022년 11월 25일
0

Spring Example: ToDo

목록 보기
12/16

질문, 피드백 등 모든 댓글 환영합니다.

프로젝트를 진행하며 가장 신경쓰이고 아쉬웠던 부분이 보안 부분이었습니다.

지금까진 HttpSession을 통해 로그인 상태를 유지하고 직접 구현한 Interceptor를 추가하여 사용자 인증 기능을 만들었지만 스프링에서 제공하는 Spring Security 프레임워크를 이용하여 보안 수준을 높여보도록 하겠습니다.

스프링 시큐리티 설정

참고 : 기존의 WebSecurityConfigurerAdapter는 스프링부트 2.7, 스프링 시큐리티 5.71 부터 deprecated 되었습니다. 해당 기사 참고해주세요.

의존관계 추가

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
	testImplementation 'org.springframework.security:spring-security-test'
}

SecurityFilterChain

Configurer

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable().authorizeRequests()
                .antMatchers("/", "/login", "/add", "/error").permitAll()
                .antMatchers("/todo/**").authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/todo")
                .and()
                .logout()
                .logoutSuccessUrl("/");

        return http.build();
        
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/css/**", "/js/**");
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

SecurityFilterChain을 스프링 빈으로 등록시켜 스프링 시큐리티 관련 설정을 할 수 있습니다.

csrf 관련 설정은 disable()로 잠시 내려두었습니다. 이 부분은 추후에 업데이트하도록 하겠습니다.

  • permitAll()로 스프링 시큐리티의 인증을 적용하지 않을 url을 지정할 수 있습니다.
    authenticated()는 그 반대

    • 참고로 enum 값을 기반으로 인증하는 기능도 존재합니다.(hasRole() 등)
  • formLogin()으로 login 관련 설정이 가능합니다. formLogin()을 사용하면 "/login" 경로에 스프링 시큐리티가 제공하는 로그인 페이지가 제공됩니다.
    저는 이전에 로그인페이지를 만들어 두었으므로 loginPage("/login")를 사용하여 제작한 로그인 페이지를 사용하겠습니다.

  • logout()으로 logout 관련 설정이 가능합니다. logout()을 사용하면 "logout" 경로에 Post 요청이 오면 스프링 시큐리티가 세션을 제거하여 로그아웃 처리를 합니다. logoutUrl("...") 을 사용하여 경로를 직접 지정할 수도 있습니다.

  • 기존 방식인 configure(WebSecurity web) 대신 WebSecurityCustomizer 을 스프링 빈으로 등록하여 사용합니다.

  • WebSecurityignoring()을 사용하여 스프링 시큐리티가 제공하는 필터를 거치 않도록 설정할 수 있습니다.

WebSecurityHttpSecurity보다 먼저 작용하므로 WebSecurityignoring()을 설정한 경로는 이후 HttpSecurity의 설정이 적용되지 않습니다.

  • BCryptPasswordEncoder : 스프링이 제공하는 암호화 객체입니다.

AuthenticationProvider

AuthenticationProviderImpl

@Component
@RequiredArgsConstructor
public class AuthenticationProviderImpl implements AuthenticationProvider {

    private final UserDetailsService service;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
        String username = token.getName();
        String password = (String) token.getCredentials();

        UserDetailsImpl loginMember = (UserDetailsImpl) service.loadUserByUsername(username);

        if (!bCryptPasswordEncoder.matches(password, loginMember.getPassword())) {
            throw new BadCredentialsException(loginMember.getUsername() + " 비밀번호를 확인해주세요.");
        }

        return new UsernamePasswordAuthenticationToken(loginMember, password, loginMember.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

AuthenticationProvider을 상속 받은 객체로 인증 작업이 이루어지는 부분입니다. @Component 사용하여 스프링 빈으로 등록하여 사용합니다.

토큰(Authentication, 더 정확히는 UsernamePasswordAuthenticationToken)에 저장 된 정보와 UserDetailsService에서 조회한 사용자 정보를 비교하여 인증 작업을 진행합니다.

UserDetailsService

UserDetailsServiceImpl

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository repository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new UserDetailsImpl(repository.findByLoginId(username).orElseThrow(() ->
                new UsernameNotFoundException("해당 사용자가 존재하지 않습니다. : " + username)));
    }

}

UserDetailsService를 상속 받은 객체로 이 서비스 객체를 통해 DB에서 loginId(username) 기반으로 사용자를 조회하고 UserDetails를 반환합니다.

UserDetails

UserDetailsImpl

@AllArgsConstructor
@Getter
public class UserDetailsImpl implements UserDetails {

    private final Member member;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return new ArrayList<>();
    }

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

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

    @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() : 유저의 권한 목록

  • isAccountNonExpired() : 계정 만료 여부, true : 만료 안됨, false : 만료

  • isAccountNonLocked() : 계정 잠김 여부, true : 잠기지 않음, false : 잠김

  • isCredentialsNonExpired() : 비밀번호 만료 여부, true : 만료 안됨, false : 만료

  • isEnabled() : 사용자 활성화 여부, true : 만료 안됨, false : 만료


다음으로

본격적으로 스프링 시큐리티를 서비스에 적용시키겠습니다.

github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글