목표: 시큐리티 파트2
Stateful과 Stateless
Stateless기반의 JWT에 대해 알아보기 전에 Stateful과 Stateless방식의 차이점을 알아보자
Stateful
- Session 사용
- Session Cluster필요(수평확장 어렵)
- 단일 사용자의 다중 로그인 컨트롤, 사용자 유효성 체크, 강제 로그아웃 기능 구현 쉬움
Stateless
- Session 사용 x, 근데 서버는 사용자를 식별할 수 있어야 함
- Session Cluster필요x(수평확장 쉬움)
- 단일 사용자의 다중 로그인 컨트롤, 사용자 유효성 체크, 강제 로그아웃 기능 구현 어려움
JWT (Json Web Token)
Stateless 상태를 유지하며, 서버에서 사용자를 식별할 수 있는 수단을 제공
서버에서 사용자 인증되면 JWT 반환
클라이언트는 JWT를 로컬 영역에 저장하고, 이후 서버에 요청을 보낼 때 JWT를 HTTP 헤더에 포함
구조
- Header: JWT를 검증하는데 필요한 정보(토큰 타입, 사용된 알고리즘)
- Payload: JWT를 통해 전달하고자 하는 데이터(Claim-Set:Key-Value 데이터 쌍)
- Signature: 비밀키를 이용해 헤더에 정의된 알고리즘으로 서명
Session과의 차이
- 자체적으로 필요한 모든 정보를 지님
- Session을 사용할 경우 Active User 수 만큼 Session을 저장->JWT사용이 유리
- 유효기간이 남아 있는 정상적인 토큰에 대해 강제 만료 처리 어려움
REST API with JWT
Spring Security 설정: csrf, headers, formLogin, http-basic, rememberMe, logout filter 비활성화
xml설정
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.18.1</version> </dependency>
yml 설정
jwt: header: token #HTTP Header 이름 issuer: prgrms #토큰 발급자 client-secret: EENY5W0eegTf1naQB2eDeyCLl5kRS2b8xa5c4qLdS0hmVjtbvo8tOyhPMcAmtPuQ #HS512 알고리즘으로 서명 expiry-seconds: 60 #토큰 만료 시간(1분)
JwtConfigure class
@Component @ConfigurationProperties(prefix = "jwt") public class JwtConfigure { private String header; private String issuer; private String clientSecret; private int expirySeconds; }
Jwt class
- JWT 발행: sign(Claims claims)
- JWT 검증: verify(String token)
Bean 등록
@Bean public Jwt jwt(JwtConfigure jwtConfigure) { return new Jwt( jwtConfigure.getIssuer(), jwtConfigure.getClientSecret(), jwtConfigure.getExpirySeconds() ); }
username으로 사용자정보를 가져와서 JWT 토큰을 만들고 반환
HTTP 헤더를 통해 JWT 토큰을 전달, 토큰의 Claims을 Map으로 변환하여 반환
JwtAuthenticationFilter
필터 구현
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; //인증된 사용자 인지 확인 if (SecurityContextHolder.getContext().getAuthentication() == null) { //HTTP 헤더에서 JWT 토큰 가져옴 String token = getToken(request); if (token != null) { try { //JWT 토큰 검증하고 디코딩 Jwt.Claims claims = verify(token); String username = claims.username; List<GrantedAuthority> authorities = getAuthorities(claims); if (isNotEmpty(username) && authorities.size() > 0) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, null, authorities); //loginId authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //UsernamePasswordAuthenticationToken 객체 참조 전달 SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { log.warn("Jwt processing failed: {}", e.getMessage()); } } } else { log.debug("SecurityContextHolder not populated with security token, as it already contained: '{}'", SecurityContextHolder.getContext().getAuthentication()); } chain.doFilter(request, response); }
필터 추가(SecurityContextPersistenceFilter 필터 바로 뒤)
public JwtAuthenticationFilter jwtAuthenticationFilter() { Jwt jwt = getApplicationContext().getBean(Jwt.class); return new JwtAuthenticationFilter(jwtConfigure.getHeader(), jwt); } @Override protected void configure(HttpSecurity http) throws Exception { http .addFilterAfter(jwtAuthenticationFilter(), SecurityContextPersistenceFilter.class); }
인증 API 추가 & 코드 리팩토링
- UsernamePasswordAuthenticationToken -> JwtAuthenticationToken
- org.springframework.security.core.userdetails.User -> JwtAuthentication
- JwtAuthenticationToken 타입을 처리할 수 있는 JwtAuthenticationProvider
- UserService 클래스를 이용해 로그인을 처리하고, JWT 토큰 생성
- 인증 요청은 JwtAuthenticationToken 객체를 만들어 AuthenticationManager를 통해 처리
- 내정보 조회 API 구현 — @AuthenticationPrincipal
//AuthenticationPrincipalArgumentResolver 통해 처리 @GetMapping(path = "/user/me") //인증필요 public UserDto me(@AuthenticationPrincipal JwtAuthentication authentication) { return userService.findByLoginId(authentication.username) .map(user -> new UserDto(authentication.token, authentication.username, user.getGroup().getName()) ) .orElseThrow(() -> new IllegalArgumentException("Could not found user for " + authentication.username)); }
SecurityContextRepository(JwtAuthenticationFilter 대체)
- JwtAuthenticationFilter의 핵심 역할: HTTP 요청헤더에서 JWT 토큰을 확인, 검증하여 SecurityContext 생성
- SecurityContextPersistenceFilter 이미 존재
- SecurityContextRepository에서 SecurityContext을 읽어옴
등록
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.securityContext()
.securityContextRepository(securityContextRepository())
}
@프로그래머스 미니 데브코스 & CNU SW Academy 강의 내용 정리
'모각코' 카테고리의 다른 글
[모각코 10회차] 네이버 지역검색 API 활용 맛집 리스트 만들기 (0) | 2022.09.08 |
---|---|
[모각코 9회차] 상품관리 REST API 클론 프로젝트(feat.React) (0) | 2022.08.30 |
[모각코 7회차] Spring Security Internals (0) | 2022.08.19 |
[모각코 6회차] 주문관리 API (0) | 2022.08.11 |
[모각코 5회차] SPA와 CORS (0) | 2022.08.04 |