JWT (JSON Web Token)은 클라이언트와 서버 간에 정보를 안전하게 전송하기 위해 사용되는 컴팩트하고 자가 포함적인 방식의 토큰입니다.
1. 라이브러리 세팅
dependencies {
implementation group: 'ch.simas.qlrm', name: 'qlrm', version: '1.7.1'
implementation group: 'com.auth0', name: 'java-jwt', version: '4.3.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'com.mysql:mysql-connector-j'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
- 스프링 부트 프로젝트 생성 시 라이브러리 설정
- Lombok, Spring Boot DevTools, Spring Data JPA, H2 Database, MySQL driver
- 따로 dependencies에 추가
implementation group: 'ch.simas.qlrm', name: 'qlrm', version: '1.7.1'
implementation group: 'com.auth0', name: 'java-jwt', version: '4.3.0'
implementation 'org.springframework.boot:spring-boot-starter-validation'
2. application.properties
spring.profiles.active=dev
- application-dev.properties 을 실행
server.port=8080
server.servlet.encoding.force=true
server.servlet.encoding.charset=utf-8
spring.datasource.url=jdbc:h2:mem:test;MODE=MySQL
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
# localhost:8080/h2-console
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=false
# lazy-loading test
spring.jackson.serialization.fail-on-empty-beans=false
# dummy
spring.jpa.defer-datasource-initialization=true
spring.sql.init.data-locations=classpath:db/data.sql
# in-query setting
spring.jpa.properties.hibernate.default_batch_fetch_size=100
# log
logging.level.com.metacoding.restserver=DEBUG
logging.level.org.hibernate.type=TRACE
# 로컬에서 테스트 할 때 적용, JwtUtil.java에서 사용할 파일
var.jwt.secret=metacoding
- 배포용 application-prod.properties 예시
server.port=8081
server.servlet.encoding.force=true
server.servlet.encoding.charset=utf-8
# RDS_ 로 설정된 변수는 노출 안되게 환경 변수로 설정
spring.datasource.url=jdbc:mysql://${RDS_HOST}:${RDS_PORT}/${RDS_DBNAME}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=${RDS_USERNAME}
spring.datasource.password=${RDS_PASSWORD}
# 실제 배포할 때는 DB를 내가 직접 받아야 함
spring.jpa.hibernate.ddl-auto=none
# spring.jpa.show-sql=true 로그 안 뜨게 해야 함 -> 리소스 잡아먹음
spring.jpa.open-in-view=false
# lazy-loading test
spring.jackson.serialization.fail-on-empty-beans=false
# in-query setting
spring.jpa.properties.hibernate.default_batch_fetch_size=100
# log
logging.level.com.metacoding.restserver=INFO
logging.level.org.hibernate.type=INFO
# 배포한 jar 실행 시 시스템 환경 변수에서 이 값을 찾아옴
var.jwt.secret=${JWT_SECRET}
3. FilterConfig
@RequiredArgsConstructor
@Configuration
public class FilterConfig {
private final JwtUtil jwtUtil;
@Bean
public FilterRegistrationBean<JwtAuthorizationFilter> jwtAuthorizationFilter() {
FilterRegistrationBean<JwtAuthorizationFilter> bean = new FilterRegistrationBean<>(new JwtAuthorizationFilter(jwtUtil));
bean.addUrlPatterns("/api/*"); // 인증 필요한 곳은 다 /api를 추가시키는 게 편하다.
bean.setOrder(1); // 0번인 CorsFilter랑 겹치지 않게
return bean;
}
@Bean
public FilterRegistrationBean<CorsFilter> corsFilter() {
System.out.println("debug: CorsFilter 등록됨~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
FilterRegistrationBean<CorsFilter> bean = new FilterRegistrationBean<>(new CorsFilter());
bean.addUrlPatterns("/*"); // * 하나만 써야됨.
bean.setOrder(0); // 낮은 번호부터 실행됨. -> CORS 부터 실행
return bean;
}
}
4. JwtAuthorizationFilter
@RequiredArgsConstructor
public class JwtAuthorizationFilter implements Filter {
private final JwtUtil jwtUtil;
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
System.out.println("debug: JwtAuthorizationFilter 작동됨~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
String jwt = request.getHeader("Authorization");
if (jwt == null) {
onError(response, "토큰 없음");
return;
}
if (!jwt.startsWith("Bearer ")) {
onError(response, "프로토콜 잘못됨(`Bearer `없음) 혹은 공백일 수 없다.");
return;
}
try {
// 토큰 검증
LoginUser loginUser = jwtUtil.verify(jwt);
// 세션에 저장
HttpSession session = request.getSession();
session.setAttribute("sessionUser", loginUser); // SessionUserResolver에서 설정함
} catch (JWTDecodeException jwtDecodeException){
onError(response, "토큰 검증 실패");
return;
}
chain.doFilter(request, response);
}
private void onError(HttpServletResponse response, String msg) {
try {
// 여기서 setContentType까지는 스프링의 도움을 받지 못하기 때문에 직접 JSON으로 변환해야 한다.
String responseBody = new ObjectMapper().writeValueAsString(Resp.fail(msg));
// 설정 안하면 200
response.setStatus(401);
response.setContentType("application/json; charset=utf-8");
PrintWriter out = response.getWriter();
out.println(responseBody);
} catch (Exception e){
// throw하면 안된다. 위임해서 처리할 곳이 없다.
e.printStackTrace();
}
}
}
- String jwt = request.getHeader("Authorization");
- 요청 헤더에서 Authorization 값(JWT)를 가져옵니다.
- Controller가 아니라서 스프링이 자동으로 해주는 JSON으로 변환, setContentType 등을 적어야 합니다.
5. JwtUtil
@Component
public class JwtUtil {
// 컴퍼넌트 스캔시에 @Value가 발동하고, 해당 값은 application.properties에서 가져온다.
@Value("${var.jwt.secret}")
private String secret;
// 토큰 만료 시간 설정
public final Long EXPIRATION_TIME = 1000*60*60*24*2L;
public String create(User user) {
String jwt = JWT.create()
.withSubject("title") // 토큰 이름
.withClaim("id", user.getId())
.withClaim("username", user.getUsername())
.withExpiresAt(Instant.now().plusMillis(EXPIRATION_TIME))
.sign(Algorithm.HMAC512(secret));
return "Bearer " + jwt;
}
public LoginUser verify(String jwt)
throws SignatureVerificationException, TokenExpiredException, JWTDecodeException {
// 서명안됨, 토큰만료, JWT형식이 아님 까지 예외 3개 발생 가능 -> 예외 처리하는 코드 추가 필요
jwt = jwt.replace("Bearer ", "");
// JWT를 검증한 후, 검증이 완료되면, header, payload를 base64로 복호화함.
DecodedJWT decodedJWT = JWT.require(Algorithm.HMAC512(secret))
.build().verify(jwt);
int id = decodedJWT.getClaim("id").asInt();
String username = decodedJWT.getClaim("username").asString();
return new LoginUser(id, username, jwt);
}
}
@AllArgsConstructor
@Data
public class LoginUser {
private Integer id;
private String username;
@JsonIgnore
private String jwt;
}
- @Value("${var.jwt.secret}")
- application-dev.properties 에서 정해둔 값을 가져옵니다.
- secret은 비밀 키로 관계자 외 사람에게 알려지면 안됩니다.
- create(User user)
- 로그인한 user 정보를 가지고 jwt 토큰을 생성합니다.
- verify(String jwt)
- 받은 jwt 토큰으로 검증합니다.
- LoginUser로 반환하는 이유는 password, email 같이 노출되면 안 되는 정보를 감추기 위해서 입니다.
6. UserService
@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
public LoginUser 로그인(UserRequest.@Valid LoginDTO reqDTO) {
User user = userRepository.findByUsername(reqDTO.getUsername())
.orElseThrow(() -> new Exception401("유저네임 혹은 패스워드가 틀렸습니다."));
if (!user.getPassword().equals(reqDTO.getPassword())) {
throw new Exception401("유저네임 혹은 패스워드가 틀렸습니다.");
}
// 토큰 생성
String jwt = jwtUtil.create(user);
// LoginUser 쓰는 이유 -> password 제외하게
return new LoginUser(user.getId(), user.getUsername(), jwt);
}
}
7. UserController
@RequiredArgsConstructor
@RestController
public class UserController {
private final UserService userService;
// 로그인 + jwt 생성
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody @Valid UserRequest.LoginDTO reqDTO, Errors errors) {
LoginUser loginUser = userService.로그인(reqDTO);
// 세션에 저장안함 (이유 : stateless 서버니까)
return ResponseEntity.ok()
.header("Authorization", loginUser.getJwt())
// @JsonIgnore 걸린 jwt 빼고 body에 id, username만 담아서 전송
.body(Resp.success(loginUser));
}
// 인증 받아야 하는 테스트 용 요청
@GetMapping("/api")
public ResponseEntity<?> api(@SessionUser LoginUser loginUser) {
return ResponseEntity.ok("통과함 : " + loginUser.getUsername());
}
}
- SessionUserResolver 덕분에 @SessionUser LoginUser loginUser 로 세션에 담긴 유저 정보를 가져올 수 있습니다.
8. SessionUserResolver
@RequiredArgsConstructor
@Configuration
public class SessionUserResolver implements HandlerMethodArgumentResolver {
private final HttpSession session;
// @SessionUser 어노테이션이 달려 있고 LoginUser class와 일치하면 resolveArgument 발동
@Override
public boolean supportsParameter(MethodParameter parameter) {
boolean isAnnotated = parameter.getParameterAnnotation(SessionUser.class) != null;
boolean isClass = LoginUser.class.equals(parameter.getParameterType());
return isAnnotated && isClass;
}
// @SessionUser User user와 같이 선언된 파라미터에 세션 사용자 정보가 자동으로 주입됩니다.
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
LoginUser loginUser = (LoginUser) session.getAttribute("sessionUser");
return loginUser;
}
}
- 매개변수에 @SessionUser가 붙어있고 그 타입이 LoginUser라면 세션에서 유저 정보를 불러올 수 있습니다.
@SessionUser 어노테이션 생성
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface SessionUser { }
9. Postman으로 테스트
로그인 및 JWT 토큰 생성

- 요청이 성공적으로 처리(응답코드 200)될 경우 JWT를 생성해서 헤더에 담아 응답을 보냅니다.

- 받은 토큰을 요청 헤더의 Authorization에 담아 보내면 인증이 완료됩니다.
- Authorization 탭에서 토큰 내용을 넣어도 됩니다.
Share article