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를 .으로 구분
- Header
- 토큰 종류, 암호화 알고리즘 정의
- {”alg” : “HS256”.”typ”:”JWT”}
- 이렇게 만들어진 JSON 객체를 base64로 인코딩하면 JWT 첫 부분 구성
- Payload
- 유저 정보, 권한, 기타 정보
- {”sub” : “someInformation”, “name” : “…”, “iat”: 23525}
- 이렇게 만들어진 JSON 객체를 base64로 인코딩하면 JWT 두 번째 구성
- Signature
- Header, Payload를 base64인코딩한 값을 header에서 지정한 알고리즘과 secret key를 사용하여 단방향 암호화를 수행
- 암호화된 메시지는 토큰 위변조 유무 검증
- HMACSHA256(base64UrlEncode(header) + “.”+base64UrlEncode(payload), secret)
토큰 기반 인증 절차
- POST /login (서버에 username, password로 로그인을 요청)
- 서버는 DB에서 아이디 비밀번호 확인하고 암호화된 JWT 생성 및 클라이언트에게 전송
- access token, refresh token 모두 생성
- payload는 사용자 식별 정보, 사용자 권한 등
- 클라이언트는 토큰을 local storage, cookie 등 다양한 곳에 저장
- GET /somInfo (클라이언트가 HTTP 헤더에 토큰 정보를 담아서 서버에게 요청)
- Authorization : “Bearer authentication”
- 서버는 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 정책 적용)
- @Bean으로 filterChain 메서드를 선언하고 매개변수는 HttpSecurity http로 받고 SecurityFilterChain을 반환형으로 가진다.
회원 가입 로직 수정
- 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가 클라이언트에게 전달되는 과정
- 클라이언트가 서버 쪽에 로그인 요청(Username/password)
- Security Filter(JwtAuthenticationFilter)가 로그인 인증 정보 수신
- Security Filter가 AuthenticationManager에 전달해 인증 처리 위임
- AuthenticationManager가 UserDetailsService에게 사용자의 UserDetails 조회 위임
- UserDetailsService가 사용자 크리덴셜을 DB에서 조회 후 AuthenticationManager에게 UserDetails 전달
- AuthenticationManager가 로그인 정보와 UserDetails 정보 비교 후 인증 처리
- 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 가 실행되도록 구현
- filterChain 메서드에서
서버 측 리소스에 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() 로 연결
- filterChain 메서드
'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 |