질문, 피드백 등 모든 댓글 환영합니다.
프로젝트를 진행하며 가장 신경쓰이고 아쉬웠던 부분이 보안 부분이었습니다.
지금까진 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'
}
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()
는 그 반대
hasRole()
등)formLogin()
으로 login 관련 설정이 가능합니다. formLogin()
을 사용하면 "/login"
경로에 스프링 시큐리티가 제공하는 로그인 페이지가 제공됩니다.
저는 이전에 로그인페이지를 만들어 두었으므로 loginPage("/login")
를 사용하여 제작한 로그인 페이지를 사용하겠습니다.
logout()
으로 logout 관련 설정이 가능합니다. logout()
을 사용하면 "logout"
경로에 Post 요청이 오면 스프링 시큐리티가 세션을 제거하여 로그아웃 처리를 합니다. logoutUrl("...")
을 사용하여 경로를 직접 지정할 수도 있습니다.
기존 방식인 configure(WebSecurity web)
대신 WebSecurityCustomizer
을 스프링 빈으로 등록하여 사용합니다.
WebSecurity
의 ignoring()
을 사용하여 스프링 시큐리티가 제공하는 필터를 거치 않도록 설정할 수 있습니다.
WebSecurity
가HttpSecurity
보다 먼저 작용하므로WebSecurity
의ignoring()
을 설정한 경로는 이후HttpSecurity
의 설정이 적용되지 않습니다.
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
에서 조회한 사용자 정보를 비교하여 인증 작업을 진행합니다.
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
를 반환합니다.
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 : 만료
본격적으로 스프링 시큐리티를 서비스에 적용시키겠습니다.