velog에서 이전한 글 입니다.
Filter
Spring의 life cycle이다.
Security
Spring Security도 많은 filter로 구성 돼있다.
Security의 목적은 controller에서 인증/인가를 분리하고 filter단에서 손쉽게 처리하기 위함이다.
인증 - 해당 유저가 실제 유저인지 인증
인가 - 해당 유저가 특정 리소스에 접근이 가능한지 허가를 확인
Authentication - 회원확인 로그인 JWT발급(Payload에 user정보)
Authorization - JWT검증 및 인가 관련 작업(Role_Blabla)
Security의 인가는 @Secured같은 어노테이션으로 기능할 수 있다.
implementation 'org.springframework.boot:spring-boot-starter-security'
gradle에 추가하면 자동으로 autoconfiguration이 동작해 라우트들이 401을 뱉는다.
@SpringBootApplication(exclude = SecurityAutoConfiguration.class)
설정하기 전에 exclude를 시켜줄 수 있다.
Authentication
(1)
(2)
(class WebSecurityConfig)
private final AuthenticationConfiguration authenticationConfiguration;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception{
return configuration.getAuthenticationManager();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception{
JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
return filter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
...
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
custom한 filter를 생성하고 AuthenticationManager를 할당하고 filter를 return하는 jwtAuthenticationFilter()를 만든다.
securityFilterChain에 Manager를 할당한 filter를 생성하고 chain의 적절한 위치에서 동작하도록 지정한다.
(3)
(public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter)
@Autowired
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
setFilterProcessesUrl("/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
...
return getAuthenticationManager().authenticate(
new UsernamePasswordAuthenticationToken(
requestDto.getUsername(), //Principal
requestDto.getPassword(), //Credentials
null // Authorities
)
);
}
attemptAuthentication 함수를 오버라이딩하고 requestDto로 로그인을 시도한다. 로그인 시도 함수가 반환하는 것은 Authentication 객체로 (3)의 자료처럼 구성되어 있다.
(public class UserDetailsServiceImpl implements UserDetailsService)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
return new UserDetailsImpl(user);
}
attemptAuthentication에서 return한 Authentication은 UserDetailsService의 구현체에 String username으로 Principal만 넘어온다. ( hmm... )
(2)의 자료처럼 UserDetailsService는 user객체를 담은 UserDetails를 반환해야 한다
(public class UserDetailsImpl implements UserDetails {)
private final User user;
public UserDetailsImpl(final User user) {
this.user = user;
}
public User getUser(){
return user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
...
...
구현할 인터페이스에 맞춰 함수들을 구현한다.
(3)의 자료에 Authorities는 권한을 GrantedAuthority로 추상화한 Collection이다.
이렇게 반환된 UserDetails는 (2)의 자료에서 보다시피 requestDto와 UserDetails를 비교하여 username과 password가 일치하는지 확인한다. (비교 로직은 직접 작성한게 아니다) 일치한다면 (1)자료의 4번으로 실패한다면 3번으로 이어진다.
(public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {)
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) {
log.info("로그인 성공 및 JWT 생성");
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String token = jwtUtil.createToken(username, role);
jwtUtil.addJwtToCookie(token, response);
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
log.info(failed.getMessage());
response.setStatus(401);
}
AuthenticationSuccessHandler는 UsernamePasswordAuthenticationFilter를 상속받고 custom한 filter의 successfulAuthentication 함수이고 AuthenticationFailureHandler는 unsuccessfulAuthentication 함수다.
두 함수를 오버라이딩하여 적절히 핸들링한다.
Authorization
(4)
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
....
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf((csrf) -> csrf.disable());
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
http.authorizeHttpRequests((authorizeHttpRequest) ->
authorizeHttpRequest
.requestMatchers("/api/user/signup").permitAll()
.anyRequest().authenticated()
);
http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
인가 필터는 JWT를 검증하고 ROLE을 인가한다. OncePerRequestFilter를 상속받고 doFilterInternal를 오버라이딩 한다.
그래서 AuthorizationFilter()를 Security Chain에 끼워넣지 않아도 한 번 동작하는 filter이고 chain에 끼워넣음으로 동작하는 타이밍을 제어할 수 있다.
(4)의 자료와 같이 security에서 인증으로 사용하는 라우트는 컨트롤러의 라우트까지 연결되지 않는다. permitAll()도 인증 라우트에는 적용되지 않는다.
.requestMatchers("/login").permitAll()
//authenticated url이 /login 이다.
위와 같이 작성해도 컨트롤러 /login이 아닌 Security의 /login에서만 처리가 된다.
+) UsernamePasswordAuthenticationFilterd를 상속받은 클래스를 생성할 때 setFilterProcessesUrl("/login")로 Security의 authenticated 라우트를 정해줬다.
결국 permitAll()은 인가 필터에만 적용된다. permitAll()이 적용된다해서 인가 필터가 동작하지 않는 건 아니다. 인가 필터의 결과와 무관하게 Security chain을 통과시키는 듯 하다.
(permitAll을 하면 Security를 생략하고 OncePerRequestFilter는 한 번은 동작하므로 동작하는건지 security chain은 그대로 진행하되 결과만 무관해지는건지?)
로그인 라우트로 들어올 때 인가/인증 2개가 모두 진행되는데 기능적으로는 인증만 동작해야 예상한대로 동작할 것이다.