Spring
Spring Security
장진혁
2023. 3. 15. 01:11
스프링 심화에서 Spring Security 라이브러리를 추가해서
jwt토큰을 이용한 인증/인가 기능을 추가할려고한다.
스프링 시큐리티 config설정과
커스텀한 필터를 위주로 기록했다.
WebSecurityConfig 안에 시큐리티 기능 중에 어떤것을 활성화 할것인지 또한 어떤 API를 인증 없이 통과 할 것인지 명시했다.
코드에 관한 설정은 내가 최대한 이해한 내용을 바탕으로 주석으로 작성했다.
설정에서 핵심은
비밀번호를 암호화 할때 단방향으로 설정하고 평문을 암호화 하고 다시 평문으로 되돌릴 수 없다.
로그인, 회원가입, 조회는 인증이 필요없는 부분이기 때문에 인증필터를 통과한다.
를 제외한 모든 것은 인증을 거처야한다.
@Configuration
@RequiredArgsConstructor
// 스프링 Security 지원을 가능하게 함
@EnableWebSecurity
// 메소드 보안 활성화
// @Secured 어노테이션 활성화
// 메소드 레벨에서 권한(Authority)을 설정할 수 있다.
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public PasswordEncoder passwordEncoder() {
// 비밀번호 암호화 기능 등록
// 적응형 단방향 함수를 사용
// 단방향은 평문에서 암호화가 가능하지만 다시 평문으로 돌리는 것은 불가능하다.
return new BCryptPasswordEncoder();
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
// h2-console 사용 및 resources 접근 허용 설정
// 밑에 있는 SecurityFilterChain 보다 우선적으로 걸리는 설정이다.
// h2-console 사용 및 resources 접근 허용 설정
return (web) -> web.ignoring() // ignoring 인증처리를 무시하겠다.
.requestMatchers(PathRequest.toH2Console())
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
// CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)
// 웹 애플리케이션 취약점 중 하나로, 인증된 사용자의 권한을 사용하여 악성 요청을 전송하는 공격
http.csrf().disable();
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
// 로그인, 회원가입 기능 인증없이 허용
.antMatchers("/post/user/**").permitAll()
// 게시글 전체, 단건 조회는 인증없이 허용
.antMatchers("/posts/list/**").permitAll()
// 이외의 URL요청들은 전부 다 authenticated 인증 처리를 하겠다.
.anyRequest().authenticated()
// JWT 인증/인가를 사용하기 위한 설정
// Custom Filter 등록하기
// addFilterBefore - (어떠한 Filter 이전에 Filter를 먼저 실행 및 추가하겠다.)
// 즉 JwtAuthFilter를 UsernamePasswordAuthenticationFilter 실행 전에 사용하도록 설정
// UsernamePasswordAuthenticationFilter 이것으로 인증처리가 되지만
// JWT토큰을 사용하는 시큐리티 커스텀 필터를 사용할 것이다.
.and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
// UsernamePasswordAuthenticationFilter
// Form Login 기반이 사용되고 이것은 인증이 필요한 URL요청이 들어왔을 때
// username 과 password 가 인증이 안되어 있으면 로그인페이지가 반환
// 내장 기본 로그인 사용
// http.formLogin();
// Custom 로그인 페이지 사용
// http.formLogin().loginPage("/api/user/login-page").permitAll();
// 접근 제한 페이지 이동 설정
// http.exceptionHandling().accessDeniedPage("/api/user/forbidden");
return http.build();
}
다음 코드는 커스텀한 필터인데 클라이언트에서 보낸 내용이 필터에 먼저 도달하고
wtAuthFilter(jwtUtil) 필터에 먼저 처리 된다.
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter { // 기본 필터 사용됨
private final JwtUtil jwtUtil;
// API요청이 오면 HTTP객체가 Filter를 타고서 controller까지 온다.(HTTP객체를 파라미터로 받는다.)
// filterChain은 Filter가 Chain형식으로 연결되어 Filter끼리 이동한다.
// 이 Filter가 끝나고 나면 다음 Filter로 이동을 해야한다.
// 이 Filter 마지막 줄 doFilter(request,response) 를 통해서 request와 response를 담아서 다음 필더로 이동
// 이 Filter에서 예외처리가 되면 이전 Filter로 예외가 넘어간다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.resolveToken(request);
// 로그인 회원가입하는 부분은 인증이 필요없다.
// 토큰이 헤더에 없기때문에 토큰을 검증하는 부분에서 예외가 터진다.
// 그래서 if문 분기 처리를 해준다.
// 인증이 필요없는 것은 다음 필터로 넘어간다.
if (token != null) {
if (!jwtUtil.validateToken(token)) {
// 밑에 있는 jwtExceptionHandler 를 통해서 클라이언트로 반환
jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
return;
}
Claims info = jwtUtil.getUserInfoFromToken(token);
setAuthentication(info.getSubject());
}
filterChain.doFilter(request, response);
}
// 인증 객체 생성
// 인증이 만들어지면 SecurityContextHolder 에 인증이 된다.
// SecurityContextHolder 안에 SecurityContext 인증 객체가 들어있다.
// 다음 필터로 이동했을때 이 요청은 인증 했다고 인식해서 컨트롤러로 넘어간다.
public void setAuthentication(String username) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = jwtUtil.createAuthentication(username);
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
// SecurityContextHolder 는 Spring Secutity 로 인증한
// 사용자의 상세정보를 가지고있는 컨테이너이다.
}
// 토큰에 대한 오류가 발생했을때 클라이언트로 예외 처리 값을 알려준다.
public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
response.setStatus(statusCode);
response.setContentType("application/json");
try {
String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
response.getWriter().write(json);
} catch (Exception e) {
log.error(e.getMessage());
}
}
인증 객체 생성
// 인증 객체 생성
public Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
밑에 있는 코드는 필터를 거치고 인증이 완료되면
컨트롤러에서 @AuthenticationPrincipal 어노테이션을 이용하여
인증된 사용자의 정보를 주입한다.
/**
* @AuthenticationPrincipal 컨트롤러 메서드의 파라미터로 현재 인증된 사용자의 정보를 주입
*/
// 게시글 저장 name content password
@PostMapping("/create")
public PostResponseDto createPost(
@RequestBody PostRequestDto postRequestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
return postService.createPost(postRequestDto, userDetails.getUser());
}
이 코드 이후로 서비스 계층으로 넘어가 비즈니스 로직을 수행한다.