기존 인증과 토큰 인증
JsessionID
- 톰캣에서 세션을 유지하기 위해 발급하는 키
- 쿠키를 통해서 세션을 유지함
- 쿠키에 저장하기 때문에 악용가능성이 있다
- 무작위 데이터이기 때문에 사용자 정보를 가지지 않는다
- 톰캣 컨테이너를 2개 이상 사용하면 세션 클러스터가 필요함
인증기반 토큰의 장점
- 인증과 인가에 토큰을 사용
- 실제 자격증명은 최초 로그인시에만 보냄
- 즉, 실제 자격증명을 가지고 계속 인증할 필요가 없음
- 토큰은 해킹시 해당 토큰만 무효화 하면 됨
- 짧은 인증기간을 가지는 토큰을 생성 가능
- 권한, 역할 같은 정보 포함 가능
- 재사용이 가능함 / 여러 서비스에서 사용 가능
- Stateless
JWT(JSON Web Token)
JWT란?
- 데이터를 JSON 형식으로 유지
- 가장 많이 사용함
- 내부에 유저의 데이터를 저장하고 유지
- Stateless가 가능하며 마이크로 서비스에서 유리
- 최초로 생성한 것과 계속 동일해야함
- 최초 생성 후 캐시, DB에 저장 후 동일한지만 확인
- Signature을 이용하면 저장할 필요도 없음
JWT 구조
- JWT 예시(JWT.IO)

- 다음과 같이 3부분으로 구성됨
- 각 부분은 (.)을 통해서 구분
- Header / Payload / Signature
- Encode / Decode를 사용함
- 비밀번호와 같은 정보를 저장하면 안됨
- 해시 알고리즘과 토큰의 형식등을 저장
- 토큰의 정보를 저장하는 곳
Payload(필수)
- 유저에 대한 모든 정보를 저장 가능
- 이름, 이메일, 역할, 만료시간 등
Signature(Optional)
- 클라이언트를 신뢰한다면 필요없음
- 헤더와 페이로드의 값을 가지고 secret키를 이용해서 생성한다.
- 무작위 해시 문자열을 반환함
- 헤더와 페이로드에 변경이 있다면 달라진다.
- 즉, 해당 방식을 통해 토큰을 확인할 수 있다.
JWT 사용 세팅하기
dependencies
dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}
Security config 수정하기
http.sessionManagement((sessionManagement) ->
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
- JsessionID를 사용하지 않고 무상태를 유지하기 위해서 다음을 추가한다.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
return http.build();
}
- CORS() 수정하기, 다음을 추가해서 헤더에 Authorization를 포함한다.
config.setExposedHeaders(List.of("Authorization"));
JWT 필터 구현하기
토큰에 사용할 상수 선언하기
package com.chan.ssb.constants;
public interface SecurityConstants {
public static final String JWT_KEY = "jxgEQeXHuPq8VdbyYFNkANdudQ53YUn4";
public static final String JWT_HEADER = "Authorization";
}
- 비밀키로 사용할 문자열과 헤더 문자열을 정의 하였다.
- 실제 제품에서는 다음과 같은 방식으로 비밀키를 사용하지 않는 것을 권장함!
토큰 생성 필터
- JWTTokenGeneratorFilter.java
package com.chan.ssb.filter;
import com.chan.ssb.constants.SecurityConstants;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
public class JWTTokenGeneratorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (null != authentication) {
SecretKey key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
String jwt = Jwts.builder().setIssuer("SSB").setSubject("JWT Token")
.claim("username", authentication.getName())
.claim("authorities", populateAuthorities(authentication.getAuthorities()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + 30000000))
.signWith(key).compact();
response.setHeader(SecurityConstants.JWT_HEADER, jwt);
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !request.getServletPath().equals("/user/login");
}
private String populateAuthorities(Collection<? extends GrantedAuthority> collection) {
Set<String> authoritiesSet = new HashSet<>();
for (GrantedAuthority authority : collection) {
authoritiesSet.add(authority.getAuthority());
}
return String.join(",", authoritiesSet);
}
}
- 먼저 인증정보를 가져온다.
- 다음의 코드들은 JWT를 만드는 것이다.
- claim을 통해서 원하는 내용를 추가할 수 있다.
- 권한의 경우 별도의 함수를 통해 문자열로 저장함
- shouldNotFilter()의 경우 true일 경우 필터를 실행하지 않음 따라서, /user/login의 요청의 경우만 토큰을 생성한다.
필터 추가하기
- SpringSecurityConfiguration.java
.addFilterAfter(new JWTTokenGeneratorFilter(), BasicAuthenticationFilter.class)
토큰 검증 필터 만들기
- JWTTokenValidatorFilter.java
package com.chan.ssb.filter;
import com.chan.ssb.constants.SecurityConstants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class JWTTokenValidatorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String jwt = request.getHeader(SecurityConstants.JWT_HEADER);
if (null != jwt) {
jwt = jwt.replace("Bearer ", "");
try {
SecretKey key = Keys.hmacShaKeyFor(
SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwt)
.getBody();
String username = String.valueOf(claims.get("username"));
String authorities = (String) claims.get("authorities");
Authentication auth = new UsernamePasswordAuthenticationToken(username, null,
AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
throw new BadCredentialsException("Invalid Token received!");
}
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getServletPath().equals("/user/login");
}
}
- 헤더의 내용을 불러와서 처리한다.
- Bearer의 형식으로 오는 토큰도 처리가능하게 치환한다.
- setSigningKey(): 다음에서 검증을 진행한다.
- shouldNotFilter()을 통해서 /user/login이 아닌 모든 요청에 대해서 필터를 적용한다.
필터 추가하기
- SpringSecurityConfiguration.java
.addFilterBefore(new JWTTokenValidatorFilter(), BasicAuthenticationFilter.class);
Swagger 설정하기
package com.chan.ssb.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(new Components().addSecuritySchemes("bearer-jwt", new SecurityScheme()
.name("bearer-jwt")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")
.in(SecurityScheme.In.HEADER)
.description("JWT Authorization header using the Bearer scheme.")))
.info(apiInfo())
.addSecurityItem(new SecurityRequirement().addList("bearer-jwt"));
}
private Info apiInfo() {
return new Info()
.title("API Test")
.description("Let's practice Swagger UI")
.version("1.0.0");
}
}
- 다음과 같은 설정으로 인증을 추가할 수 있다.
사용하기
Token 발급 확인하기
- Postman을 통해서 요청을 보낸다.

- 헤더에서 토큰을 확인할 수 있다.

Token으로 인증 받기

- 다음과 같이 요청에 토큰을 추가한다.

- Swagger도 다음과 같은 버튼이 추가된다.