본문 바로가기
Backend boot camp/Session4

[Spring Security] JWT 인증

by orioncsy 2022. 12. 9.

JWT(JSON Web Token)

JWT 기본 개념

토큰 기반 인증

  • 이전 세션 기반 인증은 서버에 유저 정보를 담는 방식
    • 매번 요청할 때마다 DB를 확인하는 방식
  • 이것을 개선하기 위한 방식이 토큰 기반 인증

토큰

  • 클라이언트에서 인증 정보를 보관하는 방식
  • 토큰은 유저 정보를 암호화한 상태로 담는다.

JWT

  • JSON 포맷으로 사용자에 대한 속성을 저장하는 웹 토큰

토큰 기반 자격 증명

세션 기반 자격 증명 방식

  • HTTP 프토토콜은 request, response를 거치면 연결을 끊는 비연결성(Conectionless)의 특성과 상태를 저장하지 않는 비상태성(Stateless)의 특성을 가지고 있다.
  • 사용자의 인증을 매 번 수행하지 않도록 인증된 사용자의 request 상태를 유지하기 위한 수단이 세션이다.
  • 세션은 사용자의 정보를 세션 형태로 세션 저장소에 저장하고 클라이언트가 요청하면 저장된 세션 정보와 사용자가 제공하는 정보가 일치하는지 확인하는 방식

세션 기반 자격 증명 특징

  • 인증된 사용자 정보를 서버 측 세션 저장소에 관리
  • 세션 고유 ID(세션 ID)는 클라이언트 쿠키에 저장되어 request 전송 시, 증명 수단으로 사용
  • 세션 ID는 클라이언트에서 사용하므로 상대적으로 적은 네트워크 트래픽 사용
  • 서버 측에서 세션 정보를 관리하여 보안성 유리
  • 서버 확장성 측면에서 세션 불일치 문제 발생 가능성 높다
  • 세션 데이터가 많아지면 서버 부담 증가
  • SSR 방식 애플리케이션에 적합

토큰 기반 자격 증명 방식

  • 토큰에 포함된 인증된 사용자 정보를 서버에서 관리하지 않는다.
  • 토큰을 헤더에 포함하여 request를 요청하면, 인증된 사용자인지 증명하는 수단으로 사용
  • 토큰 내 인증된 사용자 정보를 포함하여 세션에 비해 네트워크 트래픽을 많이 사용
  • 토큰을 서버에서 관리하지 않아 보안성 불리
  • 인증된 사용자의 상태를 서버가 유지할 필요 없어, 서버 확장성이 유리하고 세션 불일치 문제가 발생하지 않는다.
  • 토큰에 포함된 사용자 정보는 암호화가 되지 않기 때문에 민감 정보는 포함시키지 말아야한다.
  • 토큰 만료 전까지 토큰 무효화 불가능
  • CSR 방식 애플리케이션에 적합

JWT(JSON Web Token)

방식

  • 데이터를 안전하고 간결하게 전송하기 위해 고안된 인터넷 표준 인증 방식
  • 토큰 인증에 가장 범용적
  • JSON 포맷의 토큰 정보를 인코딩하고 이것을 Secret Key로 서명한 메시지를 Web Token으로 인증 과정에 사용

종류

  • Access Token
    • 보호된 정보에 접근할 수 있는 권한 부여에 사용
    • 클라이언트가 처음 로그인하면 Access Token, Refresh Token 둘 다 받지만 실제 권한을 얻는 데 사용하는 토큰은 access token이다.
  • Refresh Token
    • access token이 탈취당할 수 있기 때문에 짧은 유효 기간을 주어서 탈취돼도 오래 사용할 수 없도록 한다.
    • Refresh Token을 통해 새로운 access token 발급 가능

JWT 구조

  • aaaaaaaaaa.bbbbbb.ccccccc
    • header, payload, signature를 .으로 구분
  1. Header
    • 토큰 종류, 암호화 알고리즘 정의
    • {”alg” : “HS256”.”typ”:”JWT”}
    • 이렇게 만들어진 JSON 객체를 base64로 인코딩하면 JWT 첫 부분 구성
  2. Payload
    • 유저 정보, 권한, 기타 정보
    • {”sub” : “someInformation”, “name” : “…”, “iat”: 23525}
    • 이렇게 만들어진 JSON 객체를 base64로 인코딩하면 JWT 두 번째 구성
  3. Signature
    • Header, Payload를 base64인코딩한 값을 header에서 지정한 알고리즘과 secret key를 사용하여 단방향 암호화를 수행
    • 암호화된 메시지는 토큰 위변조 유무 검증
    • HMACSHA256(base64UrlEncode(header) + “.”+base64UrlEncode(payload), secret)

토큰 기반 인증 절차

  1. POST /login (서버에 username, password로 로그인을 요청)
  2. 서버는 DB에서 아이디 비밀번호 확인하고 암호화된 JWT 생성 및 클라이언트에게 전송
    • access token, refresh token 모두 생성
    • payload는 사용자 식별 정보, 사용자 권한 등
  3. 클라이언트는 토큰을 local storage, cookie 등 다양한 곳에 저장
  4. GET /somInfo (클라이언트가 HTTP 헤더에 토큰 정보를 담아서 서버에게 요청)
    • Authorization : “Bearer authentication”
  5. 서버는 JWT 토큰 해독하고 발급한 토큰이 맞다면 응답을 작성하고 보낸다.

JWT 장단점

토큰 기반 인증 장점

  • Statelessness & Scalability(무상태성, 확장성)
    • 서버가 클라이언트에 대한 정보를 저장할 필요가 없어 부담이 덜하다.
    • 토큰을 헤더에 추가함으로써 인증 완료
      • 여러 서버에서 하나의 토큰으로 인증 가능
      • 세션 방식이라면 모든 서버가 해당 사용자 세션 정보 공유하고 있어야 한다.
  • 클라이언트가 request할 때마다 자격 증명 정보 전송 필요가 없다.
    • JWT의 경우 토큰이 만료되기 전 한 번의 인증만 수행
  • 인증 담당 시스템 분산 용이
    • 자격 증명 정보를 직접 관리하지 않고 다른 플랫폼의 자격 증명 정보로 인증 가능
    • 토큰 생성용 서버나 타사에 토큰 작업을 맡기는 활용 가능
  • 권한 부여 용이
    • 토큰 payload 안에 접근 권한 부여 가능

토큰 기반 인증 단점

  • payload 디코딩이 용이
    • payload는 base64로 인코딩 되어 토큰 탈취해서 디코딩하면 데이터 확인 가능하여 민감정보를 payload에 포함하면 안 된다.
  • 토큰 길이가 길어지면 네트워크 부하
    • 토큰에 저장하는 정보 양이 많아질수록 request 전송을 할 때 네트워크에 부하를 준다.
  • 토큰은 자동 삭제 불가
    • 한 번 생성된 토큰은 만료 기간 전까지 삭제가 되지 않는다.
    • 토큰 탈취되면 만료 시간이 길 경우 오래 탈취당하기 때문에 만료 시간을 길게 설정하면 안 된다.

JWT 생성 및 검증 테스트

프로젝트 설정

  • Spring Boot 기반 템플릿 프로젝트 생성 및 라이브러리 추가
  • dependencies에 아래 라이브러리 추가
    • implementation 'org.springframework.boot:spring-boot-starter-security'
    • implementation 'org.springframework.boot:spring-boot-starter-web'
    • testImplementation 'org.springframework.boot:spring-boot-starter-test'
    • testImplementation 'org.springframework.security:spring-security-test'
    • implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    • runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    • runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

JWT 생성 기능 구현

  • JWT 생성을 위해 JwtTokenizer라는 클래스 생성
  • encodeBase64SecretKey() 메서드를 선언하여 String 타입의 SecretKey를 byte[] 형태로 바꾸고 Base64 형식 문자열로 인코딩한다.
    • Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8))
  • generateAccessToken() 메서드를 선언하여 사용자에게 JWT를 최초 발급해준다.
    • 매개변수로 Map<String, Object>claims, String subject, Date expiration, String base64EncodedSecretKey를 받는다.
    • getKeyFromBase64EncodedKey() 메서드를 따로 선언하고 이것을 이용해 매개변수로 받은base64EncodedSecretKey로 부터 key를 가져온다.
    • Jwts.builder()를 통해 JWT에 포함 시킬 사용자정보(claims), JWT 제목(subject), JWT 발행 일자(Calendar.getInstance().getTime()), JWT 만료일시(expiration)를 set메서드를 통해 적용시킨다.
    • 서명을 위한 Key를 signWith() 메서드를 통해 전달하고 compact()를 사용하여 JWT를 생성하고 직렬화한다.
  • getKeyFromBase64EncodedKey()메서드를 선언
    • base64EncodedSecretKey를 String 타입으로 입력받아서 Key를 출력
    • Decoders.BASE64.decode(base64EncodedSecretKey) 로 byte[] 형태로 가져와서
    • Keys.hmacShaKeyFor() 를 사용해서 Key 객체로 가져와 반환한다.
  • generateRefreshToken()를 선언
    • accessToken을 생성했던 것처럼 claims(사용자 정보)만 제외하고 똑같이 key를 가져와서 Jwts.builder()를 사용하고 정보를 set메서드를 통해 적용하고 compact()로 JWT를 생성한다.

JWT 생성 기능 테스트

  • @TestInstance(TestInstance.LifeCycle.PER_CLASS) 애너테이션을 붙여서 테스트 클래스 선언
    • 필드 멤버로 JwtTokenizer 객체, String secretKey, String base64EncodedSecretKey 선언
    • @BeforeAll 애너테이션으로 jwtTonkenizer를 생성하고 secretKey에 값을 넣고 base64EncodedSecretKey에 encodeBase64SecretKey메서드 사용해서 할당하는 메서드를 작성
    • @Test로 메서드 선언하고, assertThat으로 secretkey하고 Decoders로 base64EncodedSecretKey를 풀었을 때 값이 같은지 비교
      • ssertThat(secretKey, is(new String(Decoders.BASE64.decode(base64EncodedSecretKey))))
    • @Test로 메서드 선언하고 claims를 map으로 선언하고
      • memberId는 1/ roles는 USER 값을 담은 리스트를 put 해준다.
      • subject는 “test / Calender 객체를 선언해서 만료 시간을 지정
      • accessToken을 generateAccessToken을 통해 생성하고 assertThat으로 null 값이 아닌지 확인
    • @Test로 메서드 선언하고 위와 같이 생성하고 generateRefreshToken 메서드를 검증한다.

JWT 검증 기능 구현

  • JwtTokenizer 클래스에서 JWT 검증을 위한 verifySignature() 메서드 추가
  • String 타입으로 jws(signature가 포함된 JWT)와 base64EncodedSecretKey를 매개변수로 받고
  • base64EncodedSecretKey로 부터 key값을 가져온다.
  • Jwts.parserBuilder()를 사용하여 .setSigining(key)를 통해 서명에 사용된 SecretKey를 설정
  • .build() 후 .parseClainsJws(jws)를 통해 검증한다.

JWT 검증 기능 테스트

  • getAccessToken() 메서드를 선언하여 jwtTokenizer를 통해 AccessToken을 하나 생성하는 코드를 작성한다.
  • jwtTokenizer에서 verifySignature 메서드를 실행하였을 때 잘 작동하는지 테스트 진행
  • 만료시간이 지나서(TimeUnit.MILLISECONDS.sleep()) 테스트를 하였을 때 검증이 안되는지 확인하는 테스트 진행

Spring Security에서 JWT 인증

JWT 적용 사전 작업

의존 라이브러리 추가

  • dependencies에 아래 추가
    • implementation 'org.springframework.boot:spring-boot-starter-security’
    • implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    • runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    • runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

SecurityConfiguration 추가

  • @Configuration 애너테이션을 붙이고 SecurityConfiguration 클래스를 생성
    • @Bean으로 filterChain 메서드를 선언하고 매개변수는 HttpSecurity http로 받고 SecurityFilterChain을 반환형으로 가진다.
      • 메서드 내부에 http에 .headers().frameOptions().sameOrigine()을 추가해서 동일 출처로 들어오는 request만 페이지 렌더링 허용
      • .and().csrf().disable()을 통해 CSRF 공격에 대한 설정을 비활성화
      • .cors(withDefaults())일 경우 corsConfigurationSource라는 이름으로 등록된 Bean을 활용한다.
      • formLogin().disable()로 폼 로그인 방식을 비활성화하여 JSON 포맷 방식을 사용
      • httpBasic().disable()을 통해 request를 전송할 때마다 HTTP Header에 username/password 정보를 실어서 인증하는 방식을 비활성화한다.
      • authorizeHttpRequest(authorize→ authorize.anyRequest().permitAll())을 통해 모든 요청을 허용
      • http를 .build()하고 반환
    • PasswordEncoder를 반환형으로 하는 passwordEncoder() 메서드를 @Bean으로 등록
      • return PasswordEncoderFactories.createDelegatingPasswordEncoder()를 통해 객체 반환
    • CorsConfigurationSource를 반환형으로 하는 corsConfigurationSource() 메서드를 선언
      • CorsConfiguration 객체를 선언
      • 객체에 .setAllowedOrigins(Arrays.asList(”*”))를 통해 모든 출처에 대해 HTTP 통신 허용
      • 객체에 .setAllowedMethods(Arrays.asList(”GET”,”POST”,”PATCH”,”DELETE”))를 통해 HTTP Method에 대한 HTTP 통신 허용
      • UrlBasedCorsConfigurationSource 객체를 선언한다.
      • 객체에 .registerCorsConfiguration(”/**”, CorsConfigurationSource 객체명)을 적용하고 반환(모든 URL에 앞에서 정의한 CORS 정책 적용)

회원 가입 로직 수정

  • MemberDto,post
    • 필드 멤버에 @NotBlank를 붙여 String 타입의 password 변수 선언
  • Member 엔티티 클래스
    • @Column 애너테이션을 붙여서 nullable=false로 설정하고 length 옵션을 사용해 길이 지정하고 String 타입으로 password 선언
    • @ElementCollectioin(fetch=FetchType.EAGER)을 붙여서 List<String> 타입의 roles를 선언
  • MemberService 클래스
    • PasswordEncoder 객체, CustomAutorityUtils 객체를 선언하여 생성자 DI 설정
    • createMember 메서드에서
      • passwordEncoder.encode()를 통해 비밀번호를 암호화하여 member 객체에 저장
      • CustomAutorityUtils 객체를 이용해서 member 객체 생성
    • CustomAutorityUtils는 이전 학습에서 진행했던 것처럼 authority 리스트를 반환하도록 작성

JWT 자격 증명을 위한 로그인 인증 구현

JWT가 클라이언트에게 전달되는 과정

  1. 클라이언트가 서버 쪽에 로그인 요청(Username/password)
  2. Security Filter(JwtAuthenticationFilter)가 로그인 인증 정보 수신
  3. Security Filter가 AuthenticationManager에 전달해 인증 처리 위임
  4. AuthenticationManager가 UserDetailsService에게 사용자의 UserDetails 조회 위임
  5. UserDetailsService가 사용자 크리덴셜을 DB에서 조회 후 AuthenticationManager에게 UserDetails 전달
  6. AuthenticationManager가 로그인 정보와 UserDetails 정보 비교 후 인증 처리
  7. JWT 생성 후 클라이언트 응답으로 전달

Custom UserDetailsService

  • @Component로 클래스를 선언하고 UserDetailsService를 implements한다.
    • 필드 멤버로 MemberRepository, CustomAuthorityUtils 객체를 선언하고 DI 받는다.
    • loadUserByUsername() 메서드에서 member를 MemberRepository 객체에서 찾아서 MemberDetails 객체로 변환해서 반환
    • MemberDetails 클래스를 extends Member implements UserDetails 하여 선언
      • Member의 필드 멤버를 정의하고 getAuthorities() 메서드를 CustomAuthorityUtils 객체를 통해서 정의

로그인 인증 정보 역직렬화를 위한 LoginDTO 클래스 생성

  • 클라이언트가 전송한 Username/password를 Security Filter에서 사용할 수 있도록 역직렬화하기 위한 DTO 클래스
  • @Getter를 붙여 username, password를 선언

JWT를 생성하는 JwtTokenizer 구현

  • 로그인 인증에 성공한 클라이언트에게 JWT를 생성, 발급하고 클라이언트 요청이 들어올 때마다 전달된 JWT를 검증하는 역할
  • 이전 JwtTokenizer 구현과 거의 유사
  • secretKey, accessTokenExpirationMinutes, refreshTokenExpirationMinutes를 선언하고 application.yml에 정의하도록 @Value를 사용한다. @Getter를 각 필드에 붙여 getter 추가
  • getTokenExpiration() 메서드를 추가하여 반환형으로 Date를 선언하여 만료 일시를 지정하도록 구현
  • 민감 정보는 환경변수로 표현 &{JWT_SECRET_KEY}로 환경 변수에서 설정 가능

로그인 인증 요청 처리하는 Custom Security Filter 구현

  • UsernamePasswordAuthenticationFilter 를 extends한 JwtAuthenticationFilter 클래스 선언
  • AuthenticationManager, JwtTokenizer 객체를 필드 멤버로 선언하고 생성자 DI로 받는다.
  • 오버라이딩으로 attemptAuthentication 를 받아서 ObjectMeapper를 선언
    • objectMapper.readValue(request.getInputStream(), LoginDto.class)를 통해 loginDto를 생성
    • UsernamePasswordAuthenticationToken 를 생성하여 생성자 매개변수에 loginDto.getUsername(),loginDto.getPassword()를 넣어 생성
    • authenticationManager.authenticate(authenticationToken) 을 반환한다.
  • 오버라이딩으로 successfulAuthentication 를 받아서 Authentication 객체를 (Member) authResult.getPrincipal() 를 작성하여 member 객체로 가져온다.
    • delegateAccessToken, delegateRefreshToken 메서드를 각자 사용하여 String 타입의 token을 생성
    • response.setHeader("Authorization", "Bearer "+accessToken)
    • response.setHeader("Refresh", refreshToken)
    • response의 헤더를 위와 같이 변경해준다.
  • delegateAccessToken 메서드는 Member 객체를 매개변수로 받아 String을 반환한다.
    • Map<String,Object> claims로 선언하고 username, roles를 각각 member로부터 가져와서 claims에 넣는다.
    • subject는 member의 email을 사용하고 expiration은 jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes()) 을 통해 가져온다.
    • base64EncodedSecretKey 는 jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()) 를 통해 가져온다.
    • 마지막으로 jwtTokenizer.generateAccessToken(claims,subject,expiration,base64EncodedSecretKey) 을 통해 가져온 String 값을 반환
  • delegateRefreshToken메서드는 Member 객체를 매개변수로 받아 String을 반환한다.
    • 위의 과정에서 claims만 빼고 똑같이 구현한다.

Custom Security Filter 추가를 위한 SecurityConfiguration 설정 추가

  • JwtTokenizer를 필드 멤버로 추가하고 생성자를 추가한다
  • filterChain 메서드에
    • httpBasic().disable() 다음에 .apply(new CustomFilterConfigurer()).and()를 추가한다.
  • CustomFilterConfigurer는 extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> 하여 선언
    • configure() 메서드를 오버라이딩한다.
    • AuthenticationManager 객체를 선언하고 builder.getSharedObject(AuthenticationManager.class) 하여 할당
    • JwtAuthenticationFilter 객체를 선언하여 new JwtAuthenticationFilter(authenticationManager, jwtTokenizer) 로 할당
    • jwtAuthenticationFilter.setFilterProcessesUrl("/v11/auth/login") 로 디폴트 request URL인 “/login”을 “/v11/auth/login”으로 변경
    • builder.addFilter(jwtAuthenticationFilter) 를 통해 JwtAuthenticationFilter를 추가

로그인 인증 성공 및 실패에 따른 추가 처리

AuthenticationSuccessHandler 구현

  • AuthenticationSuccessHandler 를 implements하는 클래스 생성, @Component로 등록
  • 오버라이딩으로 onAuthenticationSuccess 메서드를 받아서 log.info()로 로그 기록을 남긴다.

AuthenticationFailureHandler 구현

  • AuthenticationFailureHandler 를 implements하는 클래스 생성, @Component로 등록
  • 오버라이딩으로 onAuthenticationFailure 메서드를 받아서 log.error(”faile: {}”, exception.getMessage())를 통해서 로그를 기록
  • sendErrorResponse()라는 메서드를 만들고 response를 매개변수로 넣는다.
    • 매개변수로 HttpServletResponse response 를 받는다.
    • Gson gson으로 선언
    • ErrorResponse 객체를 선언하고 ErrorResponse.of(HttpStatus.UNAUTHORIZED) 로 할당
    • response에 .setContentType(MediaType.APPLICATION_JSON_VALUE) 와 .setStatus(HttpStatus.UNAUTHORIZED.value()) 를 통해 contenttype을 JSON으로 하고 status를 401로 HTTP Header에 추가한다.
    • response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class)) 를 통해 errorResonse를 JSON 포맷으로 변경하고 출력 스트림 생성

AuthenticationSuccessHandler 와 AuthenticationFailureHandler 추가

  • SecurityConfiguration 클래스에 추가
    • CustomFilterConfigurer 클래스에 configure 메서드에서
    • jwtAuthenticationFilter.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler()) 와 jwtAuthenticationFilter.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler()) 통해서 핸들러 추가

AuthenticationSuccessHandler 호출

  • JwtAuthenticationFilter 클래스에서 successfulAuthentication 메서드 마지막에 this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult) 를 통해 메서드 호출 가능
  • 로그인 인증이 실패하면 MemberAuthenticationFailureHandler 객체의 onAuthenticationFailure() 메서드가 자동으로 호출

JWT를 이용한 자격 증명 및 검증 구현

JWT 검증 필터 구현

  • JWT를 검증하는 전용 Security Filter를 구현
  • OncePerRequestFilter 를 extends하는 JwtVerificationFilter 클래스 생성
    • 필드 멤버로 JwtTokenizer, CustomAuthorityUtils 객체를 생성하고 생성자 생성
    • doFilterInternal 메서드를 오버라이딩 받는다.
      • Map으로 claims를 선언하여 verifyJws 메서드를 구현하고 request를 넣어 할당한다.
      • setAuthenticationToContext 메서드를 구현하고 claims를 할당한다.
      • filterChain.doFilter(request, response) 를 통해 다음 filter를 호출한다.
    • verifyJws
      • HttpServletRequest request를 매개변수로 받고 Map<String, Object>를 반환하는 메서드
      • request.getHeader("Authorization").replace("Bearer ", "") 를 통해 request의 헤더에서 서명된 jwt인 jws(JSON Web Token Signed)를 가져와서 String 변수에 담는다.
      • jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()) 를 통해 검증 Secret Key를 얻는다.
      • jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody() 를 통해 JWT를 파싱한다.
        • claims가 정상적으로 파싱이 되면 서명 검증 역시 성공했다는 뜻이다.
      • 위의 결과를 반환
    • setAuthenticationToContext
      • 매개변수로 claims를 받는다.
      • (String) claims.get("username") 을 통해 해당 username을 얻는다.
      • authorityUtils.createAuthorities((List)claims.get("roles")) 를 통해 권한 리스트를 얻는다.
      • new UsernamePasswordAuthenticationToken(username, null, authorities) 를 통해 Authentication 객체를 생성해서 할당
      • SecurityContextHolder.getContext().setAuthentication(authentication) 를 통해 Authenticaiton 객체 저장

SecurityConfiguration 설정 업데이트

  • SecurityConfiguration 클래스에서
    • filterChain 메서드에서
      • http에 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() 추가
      • 참고(SessionCreationPolicy)
        • ALWAYS(항상 세션 생성), NEVER(세션 생성하지 않지만 이미 있다면 사용), IF_REQUIRED(필요한 경우에만 세션 생성), STATELESS(세션 생성하지 않고 SecurityContext 정보를 얻기 위해 세션 사용 안함)
    • CustomFilterConfigurer 클래스에서
      • configure 메서드에서 JwtVerificationFilter 객체를 생성하고 객체들을 생성자 DI한다.
      • builder에 .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class) 를 추가해서 jwtAuthenticationFilter 다음에 jwtVerificationFilter 가 실행되도록 구현

서버 측 리소스에 role 기반 권한 적용

  • 기존에 SecurityConfiguration 클래스 filterChain 메서드에서 http에
    • .authorizeHttpRequests(authroize→ authorize.antMatchers(HttpMethod.POST, "//members").permitAll()를 통해 ㅡ //members URL에 대해 접근 권한에 상관없이 post를 허용한다.(회원 등록은 누구나 할 수 있다)
    • .antMatchers(HttpMethod.PATCH, "/*/members/**").hasRole("USER")를 통해 회원정보 수정은 USER만 가진 사용자만 접근 가능
    • .antMatchers(HttpMethod.GET, "/*/members").hasRole("ADMIN")를 통해 모든 회원 목록은 관리자만 접근 가능
    • .antMatchers(HttpMethod.GET, "/*/members/**").hasAnyRole("USER", "ADMIN")를 통해 특정 회원에 대한 조회는 사용자, 관리자 모두 접근 가능
    • .antMatchers(HttpMethod.DELETE, "/*/members/**").hasRole("USER")를 통해 특정 회원 삭제 요청은 사용자 권한만 가진 사용자 접근 허용

예외처리

JwtVerificationFilter에 예외 처리 로직 추가

  • doFilterInternal 메서드에서 검증하고 claims를 securityContext에 넣는 작업을 try로 묶는다
  • catch로 ```SignatureException,````ExpiredJwtException, Exception` 에 대해서 request.setAttribute(”exception”, 예외 변수명)을 통해 HttpServletRequest attribut에 추가
  • Exception을 throw 하여 처리하지 않고 위와 같이 처리하면 Authentication이 저장되지 않은 상태로 다음 Security Filter를 수행하고 이렇게 되면 AuthenticationException이 발생된다.

AuthenticationEntryPoint 구현

  • SignatureException, ExpiredJwtException 등의 Exception이 발생하여 SecurityContext에 Authentication이 저장되지 않을 때 AuthenticationException이 발생하고 이때 호출되는 핸들러
  • AuthenticationEntryPoint를 implements하는 MemberAuthenticationEntryPoint 클래스 구현
    • commence() 메서드를 오버라이딩하여 AuthetnciationException이 발생할 경우 request.getAttribute(”exception”)을 통해 예외를 가져와서 ErrorResponder라는 클래스를 생성하고 메서드를 정의해서 response와 HttpStatus.UNAUTHORIZED를 전송
    • authenticationException 객체나 request에서 가져온 exception 객체 중 null이 아닌 값을 통해 로그 발생
  • ErrorResponder 클래스
    • sendErrorResonse 메서드를 선언해서 HttpServletResponse response, HttpStatus status 를 매개 변수로 받는다.
    • response의 contentType을 JSON으로, Status를 status.value()로 설정
    • response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class)) 통해 출력 스트림 형성

AccessDeniedHandler 구현

  • 인증을 성공했지만 권한이 없는 경우 호출
  • AccessDeniedHandler를 implements한 MemberAccessDeniedHandler 클래스 구현
    • handle() 메서드를 오버라이딩하여 ErrorResponder.sendErrorResponse(response, HttpStatus.FORBIDDEN) 를 통해 errorResponse를 클라이언트에게 전송
    • log.warn("Forbidden error happened: {}", accessDeniedException.getMessage()) 통해 로그 생성

SecurityConfiguration에 예외처리 로직 추가

  • SecurityConfiguration 클래스
    • filterChain 메서드
      • http에 .exceptionHandling() 를 붙인다.
      • .authenticationEntryPoint(new MemberAuthenticationEntryPoint()) 를 통해 인증 예외를 처리
      • .accessDeniedHandler(new MemberAccessDeniedHandler()) 를 통해 접근 권한 예외 처리
      • .and() 로 연결

'Backend boot camp > Session4' 카테고리의 다른 글

[Spring WebFlux] Reactor  (0) 2022.12.09
[Spring WebFlux] 리액티브 프로그래밍  (0) 2022.12.09
[Spring Security] OAuth 2  (0) 2022.12.09
[Spring Security] Spring Security 기본  (0) 2022.11.19
[인증/보안] 기초  (0) 2022.11.19