추석 연휴동안 지친 몸과 마음을 회복하고 이전 프로젝트에서 상용했던 Spring Security와 JWT를 정리해보고자 한다.
스프링 기반의 APP의 보안을 담당하는 스프링 하위 프레임 워크이다. Spring Security는 강력한 사용자 인증 및 Access를 제어할 수 있다. Spring Security는 다양한 필터를 사용하여 내 마음대로 설정이 가능하다!
Spring Security는 인증, 권한에 대한 부분을 Filter의 흐름에 따라 처리하게 된다. 위에서 말한것과 동일하게 여러가지 필터를 사용해 내마음대로 설정이 가능하기때문에 개발자 입장에서는 일일이 보안관련 로직을 짤 필요가 없어진다는 큰 메리트가 있다.
Security가 담당하는 부분은 인증(Authentication), 인가(Authorize), 권한(Admission) 이다.
Spring Security는 기본적으로 세션 & 쿠키방식으로 인증이 진행된다. 이렇게 사진처럼 인증관리자와 접근 결정 관리자를 통해 사용자의 리소스 접근을 관리한다.
SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 로드하고 저장하는 일을 담당한다.
LogoutFilter : 로그아웃 URL로 지정된 가상 URL에 대한 요청을 감시하고 매칭되는 요청이 있으면 사용자를 로그아웃시킨다.
UsernamePasswordAuthenticationFilter : 사용자명과 비밀번호로 이뤄진 폼기반 인증에 사용하는 가상 URL요청을 감시하고 요청이 있으면 사용자의 인증을 진행한다.
DefaultLoginPageGeneratingFilter : 폼기반 또는 OpenID 기반 인증에 사용하는 가상 URL에 대한 요청을 감시하고 로그인 폼 기능을 수행하는데 필요한 HTML을 생성한다.
BasicAuthenticationFilter : HTTP 기본 인증 헤더를 감시하고 이를 처리한다.
RequestCacheAwareFilter : 로그인 성공 이후 인증 요청에 의해 가로채어진 사용자의 원래 요청을 재구성하는데 사용된다.
AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자가 아직 인증을 받지 못했다면 요청 관련 인증 토큰에서 사용자가 익명 사용자로 나타나게 된다.
SessionManagementFilter : 인증된 주제를 바탕으로 세션 트래킹을 처리해 단일 주체와 관련한 모든 세션들이 트래킹되도록 돕는다.
ExceptionTranslationFilter : 이 필터는 보호된 요청을 처리하는 동안 발생할 수 있는 기대한 예외의 기본 라우팅과 위임을 처리한다.
FilterSecurityInterceptor : 이 필터는 권한부여와 관련한 결정을 AcessDecisionManager에게 위임해 권한부여 결정 및 접근 제어 결정을 쉽게 만들어준다.
이렇게 많은 Filter들이 존재한다. Spring Security를 이용해 로그인 기능을 구현하는데 사용되는 Filter들이다.
JWT란 Json 포멧을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다. 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다.
JWT는 Header, Payload, Signature로 구분이 된다. 보통 Payload부분에 여러가지 데이터를 담아 Cookie나 Session에 저장하게 된다. BE에서 설정한 시크릿 키값으로 여러가지 데이터를 암호화해 FE와 데이터를 주고받는 형태이다.
토큰의 해더는 Typ와 alg 두 정보로 구성된다.
코튼에서 사용할 정보의 조각들인 클레임이 담겨있다. Payload는 등록된 클레임, 공개 클레임, 비공개 클레임으로 구분되어있다.
public static String createJwt(String nickname, String secretKey) {
Claims claims = Jwts.claims();
claims.put("nickname", nickname); //비공개 클레임
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expireMs))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
//등록된 클레임
}
이렇게 사용이 가능하다.
서명은 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다. 서명은 헤더와 페이로드의 값을 각각 BASE64Url로 인코등하고, 인코딩한 값을 비밀 키를 이용해 해더에서 정의한 알고리즘으로 해싱을 하고, 이 값을 다시 BASE64Url로 인코딩하여 생성한다.
public static long getUserId(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().get("userid", Long.class);
}
public static String getNickname(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().get("nickname", String.class);
}
public static boolean isExpired(String token, String secretKey) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
.getBody().getExpiration().before(new Date());
}
이렇게 생성된 JWT토큰을 SecretKey를 이용해 Payload값을 읽어오는 부분과 토큰의 유효성을 검사하는 부분이다.
Spring Security와 결합해 permit되지 않은 페이지에 접속할때마다 Token의 값의 유효성을 검사하고, 만약 유효하지 않다면 접속 거부 하도록 구현하게되었다.
https://devuna.tistory.com/55
https://mangkyu.tistory.com/56