어제에 이어 오늘은 수립한 API명세에 따라 JWT를 이용한 인증인가 회원가입,로그인 기능을 구현하였다.
지난 프로젝트에서도 jwt를 사용하였지만 이번과는 다르게 하나의 AccessToken만 사용한 형태의 스프링시큐리티 인증,인가 기능을 구현하였기 때문에 이번에는 두개의 토큰(AccessToken, RefreshToken)을 사용한 스프링시큐리티 인증,인가 기능을 구현하였다.
기초적인 틀을 잡을때 고려 하였던것은 RefreshToken의 저장위치에 관한것이였다.
RefreshToken은 AccessToken과의 역할이 각기 다르다
RefreshToken : AccessToken 만료시 토큰재발급인증 만료시간 : 2주~4주
AccessToken : 일반적인 로그인중일때의 인증 만료시간 : 30분~1시간
RefreshToken은 AccessToken이 만료시 재발급을위한 인증을 위해 서버측에서도 해당 토큰을 저장하고있어야 한다. 여러가지 고민을 해본결과 단일 DB를 사용한 RTK(RefreshToken)저장소를 따로 만드는 방법을 선택하였다. 그 이유는 향후 로그아웃기능을 구현할때 해당 RTK객체의 필드 요소에 boolean값을 넣어 로그인시 true, 로그아웃시 fasle로 변경하여 해당 토큰 인증을 무효화를 하되 계속 저장되는 데이터의 생성시간을 필드요소로 하여 만료시간이 넘어선 토큰은 스케줄러로 삭제되도록 구현할 예정이다.
구체적인 저장소위치와 저장소 설계계획 다음은 ATK(AccessToken)과 RTK의 생성과 클라이언트와 요청,반환시 jwt의 저장위치에 대해 로직을 구현하였다.
// AccessToken 생성
public String createAccessToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + ACCESS_TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
// RefreshToken 생성
public String createRefreshToken(String username, UserRoleEnum role) {
Date date = new Date();
return Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + REFRESH_TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 암호화 알고리즘
.compact();
}
ATK는 토큰생성시에는 BEARER을 붙여줬고 RTK의 경우 없이 생성하였다. 그 이유는 ATK와 RTK두개의 토큰중에서 어떤것이 더 보안에 신경써야하는것은 RTK라 생각했기때문에 해당 토큰을 쿠키에 담고 ATK는 헤더에 바로 담아 클라이언트에게 전달하는 방법을 택하였다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("로그인 성공 및 JWT 생성");
String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();
String accessToken = jwtUtil.createAccessToken(username, role);
String refreshToken = jwtUtil.createRefreshToken(username, role);
// RefreshToken DB에 저장
jwtUtil.saveRefreshJwtToDB(refreshToken, username);
// RefreshToken 쿠키에 저장
jwtUtil.addJwtToCookie(refreshToken, response);
// AccessToken 헤더에 저장
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
response.setStatus(200);
response.setCharacterEncoding("utf-8");
PrintWriter writer = response.getWriter();
writer.println("200 Ok");
writer.println("로그인 성공!");
}
다음으로 ATK만료시 RTK인증을 통해 ATK를 재발급하여 클라이언트에게 전달하는 로직을 구현하였다.
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws ServletException, IOException {
String accessTokenValue = jwtUtil.getJwtFromHeader(req);
if (StringUtils.hasText(accessTokenValue)) {
log.info(accessTokenValue);
try {
if (!jwtUtil.validateToken(accessTokenValue)) {
log.error("유효하지않은 AccesToken");
res.setStatus(400);
res.setCharacterEncoding("utf-8");
PrintWriter writer = res.getWriter();
writer.println("AccessToken이 유효하지 않습니다.");
return;
}
} catch (ExpiredJwtException e) {
// 만료된 accessToken 일경우 accessToken 재발급
// 쿠키에서 리프레시 토큰가져와서 유효성 검사후 발급
String refreshToken = jwtUtil.getTokenFromRequest(req);
// refreshToken = jwtUtil.substringToken(refreshToken);
log.info(refreshToken);
// refrshToken 검증
if (!jwtUtil.validateToken(refreshToken)) {
log.error("유효하지않은 RefreshToken");
res.setStatus(400);
res.setCharacterEncoding("utf-8");
PrintWriter writer = res.getWriter();
writer.println("유효하지않은 RefreshToken 입니다. 다시 로그인 해주세요.");
return;
}
// refreshToken DB조회
if (!jwtUtil.checkTokenDB(refreshToken)) {
log.error("refreshtoken not exist");
res.setStatus(400);
res.setCharacterEncoding("utf-8");
PrintWriter writer = res.getWriter();
writer.println("등록되지않은 RefreshToken 입니다. 다시 로그인 해주세요.");
return;
}
// accessToken 재발급
Claims user = jwtUtil.getUserInfoFromToken(refreshToken);
String accessToken = jwtUtil.createAccessToken(user.getSubject(), UserRoleEnum.valueOf(user.get(AUTHORIZATION_KEY).toString()));
// AccessToken 헤더에 저장
res.addHeader(JwtUtil.AUTHORIZATION_HEADER, accessToken);
res.setStatus(200);
res.setCharacterEncoding("utf-8");
PrintWriter writer = res.getWriter();
writer.println("AccessToken이 재발급되었습니다. 다시 시도 해주세요.");
return;
}
// 정상 동작일때
Claims info = jwtUtil.getUserInfoFromToken(accessTokenValue);
try {
setAuthentication(info.getSubject());
} catch (Exception e) {
log.error(e.getMessage());
return;
}
}
filterChain.doFilter(req, res);
}