240222 TIL #328 Spring Security + JWT

김춘복·2024년 2월 22일
0

TIL : Today I Learned

목록 보기
328/543
post-custom-banner

Today I Learned

오늘은 구현과제 대비 Spring Security에서 JWT를 적용하는 방법을 정리해봤다.


Spring Security

JWT

  • Spring Security에서 JWT를 사용한 인증/인가 흐름

    회원가입 - (해당 URI는 permitAll) 사용자 입력값으로 회원가입 진행.
    비밀번호는 Password Encoder로 암호화 후 저장.
    로그인 - (해당 URI는 permitAll) 사용자 입력값으로 회원 인증 진행.
    인증 성공시 JWT 토큰 생성해서 Header에 추가해서 반환. 사용자는 쿠키에 저장
    User 권한 필요한 요청은 JWT 토큰으로 인증.(사용자의 Header에 넣어서 전송)
    서버는 토큰을 검증하고 Custom Security Filter를 사용해 인증/인가 처리
    Custom Security Filter에서 SecurityContextHolder에 인증 완료한 사용자의 상세정보 저장. 이를 통해 Spring Security 에 인증이 완료 된 것을 알려준다.

즉, JWT 토큰과 Custom Security Filter를 통해 사용자를 로그인 된 상태로 유지시켜준다.

  • Controller에서 @GetMapping("/login")은 기본적으로 Spring Security가 사용하고 있기때문에 @GetMapping("/login-page") 처럼 피해준다. forbidden은 get post 둘다 만들어준다.
  • WebSecurityConfig의 securityFilterChain() 설정
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf().disable();
    // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
    http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

    http.authorizeRequests().antMatchers("/api/user/**").permitAll()
            .antMatchers("/api/search").permitAll()
            .antMatchers("/api/shop").permitAll()
            .anyRequest().authenticated()
            // JWT 인증/인가를 사용하기 위한 설정
            .and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

    http.formLogin().loginPage("/api/user/login-page").permitAll();

    http.exceptionHandling().accessDeniedPage("/api/user/forbidden");

    return http.build();
}
  • JwtAuthFilter (extends OncePerRequestFilter 다른 필터 상속받아도 됨)
    게시글 작성 같은 인증이 필요한 요청이 있을때 토큰을 확인.

  • 이전엔 ProductService 같은 Service 로직에서 토큰을 가져와 검증하고 유저를 검증하고 이런 절차가 있었지만, 이제는 Custom한 JwtAuthFilter에서 Controller를 오기 전에 먼저 수행이 되어 User정보를 바로 넘겨주기 때문에 코드가 간결해진다.

JWT 취약점 및 보완점

TIL #28에다 정리 해둠.


클래스 별로 정리

WebSecurityConfig

접근 허용 api 권한 설정을 하고 필터 체인을 설정.

  • passwordEncoder() : 비밀번호 암호화 메서드
    return BCryptPasswordEncoder() // bCrypt로 암호화.

  • webSecurityCustomizer() : 접근 허용 설정
    return (web) -> web.ignoring() // 아래의 코드로 들어오는 접근은 허용
    .requestMatchers(PathRequest.toH2Console())

  • securityFilterChain(HttpSecurity http) : FilterChain을 Custom
    api별로 접근 허용이나 권한 설정. 세션방식을 JWT로 바꾸는 설정. 로그인페이지 cutsom 설정
    필터에 JwtAuthFilter 추가.

JwtAuthFilter (extends OncePerRequestFilter)

request의 header에서 토큰 가져와서 유효성 검사를 한 뒤 인증객체 생성해서 다음 필터로 넘김

  • void doFilterInternal(request, response, filterchain)
    request의 header에서 토큰을 가져와서 토큰 유효성 검사를 한 뒤
    인증객체를 생성(아래 메서드 사용)해서 다음 필터로 넘긴다.

  • void setAuthentication(username) 위의 메서드에 사용된다.
    jwtUtil.createAuthentication(username)으로 인증객체를 만들고
    그걸 context에 넣고 SecurityContextHolder에 context를 넣음

  • void jwtExceptionHandler : 위의 doFilterInternal에서 토큰 오류 발생시 예외처리.

JwtUtil

JWT 토큰에 대한 여러 메서드를 정의. 토큰을 가져오고 만들고 검사하고,
토큰으로 사용자 정보 가져오고 토큰으로 인증객체토큰 만드는 메서드 포함.

  • String resolveToken(request) : request의 header에 있는 토큰 값을 가져옴

  • String createToken(username, role) : 토큰 생성. 만료날짜랑 유저명 역할 넣고 암호화

  • boolean validateToken(String token) : 토큰 유효성 검사. try catch로 토큰 이상시 예외처리.

  • Claims getUserInfoFromToken(String token) : 토큰에서 사용자 정보 가져옴

  • Authentication createAuthentication(username) : 인증객체 생성
    username으로 userdetails 만들어 UsernamePasswordAuthenticationToken을 생성
    principal, credentials(비밀번호 이미 검증했으니 null), authorities.

UserDetailsImpl implements UserDetails

user와 username을 받아서 authorities 생성

  • getAuthorities() : user의 role 받아서 authority 받아 String 값 생성.
    authority를 GrantedAuthority에 감싸서 반환.

UserDetailsServiceImpl implements UserDetailsService

username으로 UserDetails 생성

  • UserDetails loadUserByUsername(String username)
    : username으로 user 찾아서 userDetailsImpl(user, username)에 넣고 반환.

UserService

회원가입 로그인 진행

  • void signup(signupRequestDto)
    DTO에서 username 그대로 받고, password는 passwordEncoder.encode(dto.getPassword())로 받아옴. 회원 중복과 admin여부 판단 뒤 userRepository.save로 DB에 저장

  • void login(loginRequestDto, response)
    DTO에서 username password 받고 사용자 확인, 비밀번호 확인!passwordEncoder.matches()
    해서 로그인 성공시 response에 JWT토큰 넣어 보냄.

정리

Request가 오면 미리 허가한 것 제외하고 FilterChain으로 들어와 토큰의 유효성 검사를 하고 인증객체를 생성해 FilterChain들을 통과. 그 중 문제가 생기면 예외 처리.
인증을 성공적으로 통과하면 Controller에 DTO와 UserDetails가 전달.
Service에서는 UserDetails를 이용해 더이상의 검증과정 없이 서비스 코드만 실행해서 response.

profile
Backend Dev / Data Engineer
post-custom-banner

0개의 댓글