Spring Security는 Spring MVC 기반 애플리케이션의
인증 (Authentication)
rhk인가 (Authorization)
기능을 지원하는 보안 프레임워크이다.
Principal(주체)
Authentication(인증)
Authorization(인가 또는 권한 부여)
Access Control(접근제어)
: 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것. build.gradle 파일에 라이브러리를 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security 적용하기 위해 추가
UserDetail은 인증 정보를 만드는데 필요한 사용자 정보를 제공해 준다.
사용자 정보를 입력할 때 어떤 변수가 Username인지, 어떤 변수가 password인지, 어떤 변수가 권한 정보를 나타내는지 알아야한다.
우선 UserDetails 인터페이스를 구현해야한다. 아래와 같이 모든 값을 null과 false로 return 하는 것을 볼 수 있다.
그러면 각 메소드들이 어떤 역할을 하는지 알아보자.
public class CustomDetails implements UserDetails {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
getAuthorities
: 계정의 권한 목록 returngetPassword()
: 계정의 비밀번호 returngetUsername()
: 계정의 이름 return (PK에 해당하는 정보)isAccountNoNExcired()
: 계정이 만료되지 않았는지 return (true : 만료안됨)isAccountNonLocked()
: 계정이 잠겨있지 않았는지 return (true : 잠기지 않음)ìsCredentiakNonExpired()
: 비밀번호가 만료되지 않았는지 return (true : 만료안됨)isEnabled()
: 계정이 활성화(사용가능)인지 return (true : 활성화)🥲 처음 공부할때 제일 어려웠던 점 ...
단순히 인증정보를 위한 정보를 제공해준다고 하여, getPassword()와 getUsername() 메소드에 대해서 구현하고, 나머지 isAccountNonExpired, isCredentialsNonExpired()는 구현하지 않아 모두 false로 되어있어 적용이 안되었던 경험이 있다...
이 부분 절대 놓치지 말자 !! 그리고 검색하고 글을 꼼꼼히 자세히 읽자 !!
@Data
public class CustomDetails implements UserDetails {
private SiteUser siteUser;
public CustomDetails(SiteUser siteUser) { this.siteUser = siteUser; }
// 해당 유저의 권한을 리턴
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 특별한 권한 시스템을 사용하지 않을 경우
// retrn Collections.EMPTY_LIST;
// 를 사용하면 된다.
Collection<GrantedAuthority> collect = new ArrayList<>();
collect.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return siteUser.getRole();
}
});
return null;
}
// 비밀번호 정보 제공
@Override
public String getPassword() {
return siteUser.getPassword();
}
// ID 정보제공
@Override
public String getUsername() {
return siteUser.getUsername();
}
// 계정이 만료되지 않았는지 리턴 (true: 만료안됨)
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정이 잠겨있는지 않았는지 리턴 (true: 잠기지 않음)
@Override
public boolean isAccountNonLocked() {
return true;
}
// 비밀번호가 만료되지 않았는지 리턴한다. (true: 만료안됨)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정이 활성화(사용가능)인지 리턴 (true: 활성화)
@Override
public boolean isEnabled() {
return true;
}
}
사용자의 인증정보를 제공할 UserDetails를 만들었다면 받은 정보를 토대로 사용자를 찾고 UserDetails를 만들어서 SecurityContextHorder에 제공할 CustomUserDetailService를 만들어야한다.
✅ 즉, UserDetailService를 구현해야한다
혹시나 왜 구현체를 implements해야하는지 이해가 안된다면 참고자료를 확인하면 좋다.
해당 글은 완벽한 동작원리는 안나와있지만 inteface implements를 통해 어떻게 Custom이 가능하고, 동적으로 구현체를 선택 쉽게 설명이 되어있다고 생각된다.
public class CustomUserDetailsService implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}
loadUserByUsername()
: 사용자의 Id(PK, 스프링부트에서는 username으로 고정)d에 해당하는 사용자 정보를 받아서 알맞은 UserDetails 객체를 반환해준다. @Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private SiteUserRepository siteUserRepository;
// 시큐리티 session에는 Authentication타입 필요
// Authentication타입에서 User를 검사하기 위한 UserDetails타입 필요
// 따라서 securityconfig의 loginProcessingUrl의 경로 접근 시 loadUserByUsername이 호출되어
// UserDetails타입을 Authentication 내부에 return을해준다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SiteUser user = siteUserRepository.findByUsername(username)
.orElseThrow(() -> {
throw new UsernameNotFoundException("해당 %s를 찾을 수 없습니다. 다시 확인해주세요.".formatted(username));
});
return new CustomDetails(user);
}
}
기존에는 WebSecurityConfigurerAdapter
를 상속받은 Config Class를 만들어 Security 설정을 했다.
하지만 Spring Security 5.7.0M-2
부터 deprecated되어버렸다.
그렇기 때문에 해당 설정 방법은 Spring Boot 2.7.8
이하는 이전 방법으로 설정해야한다. 이전 설정 방법은 참고자료를 확인하면된다.
WebSecurity
는 패턴에 해당하는 리소스에 아예 Spring Security
를 적용하지 않도록 설정한다.
즉, 패턴이 적용된 리소스는 자유롭게 Access(접근)가 가능하다는 것이다.
하지만, WebSecurity는 Spring Security Filter Chain을 거치지 않기 때문에 '인증', '인가' 서비스가 모두 적용되지 않는다.
그렇기 떄문에 (XSS), content-sniffing에 대한 보호가 제공되지 않는다.
HttpSecurity
는 WebSecurity
에서 제외된 외의 부분에 Spring Security
를 설정할 수 있게 해준다.
authorizeRequests
를 통해 리소스의 인증 부분을 ignore할 수 있다고해도 그 외에 다른 security 기능들에 대해서는 영향을 받는다.
결국 WebSecurity는 HttpSecurity의 상위에 있다. 그렇기 때문에 WebSecurity 패턴에 적용되는 리소스들은 접근이 자유롭게 가능해지는 것이다.
그러므로 보안과 전혀 상관없는 페이지에는 WebSecurity를 적용하고, 그 외에는 HttpSecurity를 적용하는 것이 좋다.
조금 더 쉽게 말하면, 보안처리가 필요한 곳은 HttpSecurity를 적용하고, 그 외(정적리소스,HTML)에는 WebSecurity를 적용한다.
configure(WebSecurity web)
: WebSecurity에 대한 설정filterChain(HttpSecurity http)
: HttpSecurity에 대한 설정daoAuthenticationProvider()
: UserDetailsService 및 PasswordEncoder를 사용하여 사용자 아이디와 암호를 인증하는 AuthenticationProvide 구현한 설정passwordEncoder()
: DB 저장 시, password Encoding 설정@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomUserDetailsService customUserDetailsService;
/** WebSecurity 설정부분 **/
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**");
}
/** HttpSecurity 설정부분**/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
/** 시큐리티 4.x버전부터는 disable()하지 않는 한 자동으로 활성화된다.**/
http.csrf().disable();
/** Default는 모든 리소스에 대해 인증정보를 요구한다. **/
/** permitAll시 해당 리소스에 대한 인증정보를 요구하지 않는다. **/
/** hasAnyRole시 해당 리소스에는 특정 권한 정보를 요구한다. **/
http.authorizeHttpRequests()
.requestMatchers(
new AntPathRequestMatcher("/question/modify/**")).authenticated()
.requestMatchers(
new AntPathRequestMatcher("/question/create**")).authenticated()
.anyRequest().permitAll()
.and()
.formLogin()
.loginPage("/loginForm")
.loginProcessingUrl("/login")
.failureForwardUrl("/loginError")
.failureHandler(new CustomAuthFailureHandler())
.successHandler(new CustomSuccessHandler())
.defaultSuccessUrl("/")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/");
return http.build();
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
authenticationProvider.setHideUserNotFoundExceptions(false);
return authenticationProvider;
}
/** **/
@Bean
AuthenticationFailureHandler customAuthFailureHandler(){
return new CustomAuthFailureHandler();
}
/** DB에 저장 시, 비밀번호를 encoding 설정 부분 **/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
DaoAuthenticationProvider
는 사용자 이름과 암호를 인증하기 위해 UserDetailsService와 PasswordEncoder를 사용하는 AuthenticationProvider 구현체입니다.
해당 그림은 스프링 시큐리티 내에서 DaoAuthenticationProvider가 어떻게 작동하는지 보여줍니다.
1. 사용자 이름 및 비밀번호 읽기 섹션의 인증필터는 사용자 이름 비밀번호 인증 토큰을 ProciderManager에 의해 구현되는 AuthenticationManager로 전달합니다.
2. ProviderManager는 DaoAuthenticationProvider 유형의 AuthenticationProvider를 사용하도록 구성합니다.
3. DaoAuthenticationProvider는 UserDetailsServcie에서 UserDetails를 조회합니다.
4. DaoAuthenticationProvider는 PasswordEncoder를 사용하여 이전 단계에서 반환된 UserDetails에서 비밀번호의 유효성을 검사합니다.
5. 인증이 성공하면 반환되는 인증은 UsernamePasswordAuthenticationToken유형이며, 구성된 UserDetailsService에서 반환된 UserDetails인 주체를 갖습니다.
최종적으로 반환된 UsernamePasswordAuthenticationToken은 인증필터에 의해 SecurityContextHolder에 설정됩니다.
헷갈리는 부분을 나의 말로 바꾸어보면 3번 단계에서 UserDetailsService으로부터 반환된 UserDetails의 비밀번호를 유효성 검사한다는 말은 UserDetailsService는 받은 정보를 토대로 사용자를 찾고 UserDetails를 만들어서 SecurityContextHorder에 제공하기 때문에 사용자의 정보를 제공해주는 UserDetail객체와 PasswordEncoder를 사용하여 비밀번호 유효성 검사를 진행한다는 것이다 !!!
DaoAuthenctionProvider 참고자료1
DaoAuthenctionProvider 참고자료2
AuthenticationProvider 참고자료
Spring Security는 정말 어렵다고 느껴지지만,, 이 어려운걸 조금씩 이해하고 있는 것 같다는 생각에 매우 뿌듯해진다...ㅋㅎㅋㅎ(이해하고있다는 것은 .. 나의 착각일 수도 ..?) 그리고 점점 기록하는 습관을 들이고 있는 것 같아서 매우 뿌듯하다 😄
다음 글은 Spring Security와 JWT를 적용해보는 글을 작성해보자 !