무상태(stateless : 비 연결적인 특성)는 연결이 해제되면 동시에 server 및 clinet는 clinet가 이전에 요청한 결과에 대해서 잊기 때문에 다른 요청 시 서버와의 연결이 필요하다.Http의 이러한 특성 때문에 stateless protocol 이라고 불리기도 하며 웹 사이트는 매 페이지에서 로그인이 되어 있는지 상태를 확인하는 인증 방식이 필요하게 된다.Authentication (인증) : 웹 사이트에 로그인을 하는 것Authorization (인가) : 한번 로그인을 하고 나면 로그인 상태가 유지되는 것Session(세션) 및 JWT 이다.브라우저에 웹 서버가 연결되어서 해당 브라우저의 종료시점 까지를 의미session id를 부여하고 같은 브라우저인지 구별한다.CORS(Cross-Origin Resource Sharing) 제약으로 인해 여러 도메인에서 세션을 공유하기 어렵다.토큰을 사용한다는 것은 요청과 응답에 토큰을 함께 보내 이 사용자가 유효한 사용자인지를 검색하는 방법이다.
이때, JWT(Json Web Token) 토큰을 사용해서 전달
우리가 로그인을 성공하면 Authentication(인증)이 일어나고 서버는 jwt를 제공합니다. 그리고 우리는 매 요청마다 jwt와 함께 서버에 요청을 보내면 서버는 jwt만 확인하여 authorization(인가) 하게 됩니다.
JWT 인증 과정

사용자의 인증 정보가 담겨있는 토큰을 클라이언트에 저장하기 때문에 서버에서 별도의 저장소가 필요 없어, 완전한 무상태(stateless)를 가질 수 있습니다. 그리고 이로 인해 서버를 확장할 때 용이합니다.
토큰 기반 인증을 사용하는 다른 시스템에 접근이 가능
HMAC(Hash-based Message Authentication) 기법이라고도 불리며, 발급 후의 토큰의 정보를 변경하는 행위가 불가능합니다. 즉, 토큰이 변조되면 바로 알아차릴 수 있습니다.
클라이언트가 서버에 요청을 보낼 때, 쿠키를 전달하지 않기 때문에 쿠키의 취약점은 사라집니다.

헤더는 토큰의 타입과 해싱 알고리즘을 지정하는 정보를 포함합니다.
{
"typ": "JWT",
"alg": "HS256" // HS256: 해상 알고리즘
}
토큰에 담을 정보가 들어갑니다. 정보의 한 덩어리를 클레임(claim)이라고 부르며, 클레임은 key-value의 한 쌍으로 이루어져 있습니다. 클레임의 종류는 세 종류로 나눌 수 있습니다.
{
"iss": "***", // 등록된(registered) 클레임
"iat": -, // 등록된(registered) 클레임
"exp": -, // 등록된(registered) 클레임
"https://**.com/jwt_claims/is_admin": true, // 공개(public) 클레임
"email": "***", // 비공개(private) 클레임
"hello": "안녕하세요" // 비공개(private) 클레임
}해당 토큰이 조작되었거나 변경되지 않았음을 확인하는 용도로 사용하며, 헤더(header)의 인코딩 값과 정보(payload)의 인코딩값을 합친 후에 주어진 비밀키를 통해 해쉬값을 생성합니다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
jsonwebtoken을 사용하기 위해 의존성을 추가해 줍니다.
implementation 'io.jsonwebtoken:jjwt:0.9.1'
public String makeJwtToken() {
Date now = new Date();
return Jwts.builder()
.setHeaderParam(Header.TYPE, Header.JWT_TYPE) // (1)
.setIssuer("fresh") // (2)
.setIssuedAt(now) // (3)
.setExpiration(new Date(now.getTime() + Duration.ofMinutes(30).toMillis())) // (4)
.claim("id", "아이디") // (5)
.claim("email", "ajufresh@gmail.com")
.signWith(SignatureAlgorithm.HS256, "secret") // (6)
.compact();
}
더 많은 기능이 있지만, 제가 사용해 본 메서드 위주로 설명을 적어두었습니다. 모든 설정이 끝나면 compact()를 통해 JWT 토큰을 만들 수 있습니다.
그 이후에 실행하면,
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmcmVzaCIsImlhdCI6MTYyMjkwNjg0NSwiZXhwIjoxNjIyOTA4NjQ1LCJpZCI6IuyVhOydtOuUlCIsImVtYWlsIjoiYWp1ZnJlc2hAZ21haWwuY29tIn0.ucTS9OgA7Z751a6aNzttcEXRfEhG_hsZPzZZTHhbUrA
위와 같은 토큰을 획득할 수 있습니다.
Authorization : Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJmcmVzaCIsImlhdCI6MTYyMjkwNjg0NSwiZXhwIjoxNjIyOTA4NjQ1LCJpZCI6IuyVhOydtOuUlCIsImVtYWlsIjoiYWp1ZnJlc2hAZ21haWwuY29tIn0.ucTS9OgA7Z751a6aNzttcEXRfEhG_hsZPzZZTHhbUrA
전달받은 토큰을 해석해서 유효한 토큰인지 확인이 가능합니다. 👀
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
Claims claims = jwtTokenProvider.parseJwtToken(authorizationHeader);
filterChain.doFilter(request, response);
}
public Claims parseJwtToken(String authorizationHeader) {
validationAuthorizationHeader(authorizationHeader);// (1)String token = extractToken(authorizationHeader);// (2)return Jwts.parser()
.setSigningKey("secret")// (3)
.parseClaimsJws(token)// (4)
.getBody();
}
private void validationAuthorizationHeader(String header) {
if (header == null || !header.startsWith("Bearer ")) {
throw new IllegalArgumentException();
}
}
private String extractToken(String authorizationHeader) {
return authorizationHeader.substring("Bearer ".length());
}
위와 같은 정보를 넣어준 후에 getBody()를 호출하게 되면, Claim 타입의 결과 객체를 반환하게 되는데, 여기에서 저장된 클레임 정보들을 확인할 수 있습니다.

발생할 수 있는 예외는 다음과 같으며 적절한 처리를 해주는 것이 좋다
UnsupportedJwtException : 예상하는 형식과 다른 형식이거나 구성의 JWT일 때MalformedJwtException : JWT가 올바르게 구서오디지 않았을 때ExpiredJwtException : JWT를 생성할 때 지정한 유효기간이 초과되었을 때SignatureException : JWT의 기존 서명을 확인하지 못했을 때IllegalArgumentException