- 인증(Authentication)
- 해당 유저가 실제 유저인지 인증하는 개념
- 사용자의 신원을 검증하는 프로세스
- ex) 로그인하는 행위
- 인가(Authorization)
- 인증 이후의 프로세스로, 인증된 유저가 어떠한 자원에 접근할 수 있는지 확인하는 절차
- ex) 관리자 페이지 - 관리자 권한이 있어야 접근 가능
인증 방식 차이점
항목 | 세션 인증방식 | JWT 인증 방식 |
방식 | 세션에 사용자 정보를 담은 뒤 쿠키에 세션ID를 담아서 넘기는 방식 |
사용자에게 암호화된 토큰을 넘기는 방식 (액세스 토큰, 리프레쉬 토큰) 이때 넘기는 곳은 개발자가 정함 (쿠키, 로컬스토리지) |
장점 | - 사용자의 로그인 정보를 주고 받지 않기 때문에 상대적으로 안전하다. - 사용자마다 고유한 세션 ID가 발급되기 때문에, 요청이 들어올 때마다 회원DB를 찾지 않아도 된다 |
- 동시 접속자가 많을 때 서버 부하를 낮춘다. - 클라이언트, 서버가 다른 도메인을 사용할 때 사용 가능하다. - 서버의 Stateless 특성이 유지된다. |
단점 | - 사용자를 식별할 수 있는 값인 세션 ID를 생성하고, 서버에 저장해야하는 작업이 생긴다. - 서버 세션 저장소를 사용하므로 요청이 많아지면 서버 부하가 심해진다. |
- 구현 복잡도가 증가한다. - JWT에 담는 내용이 커질수록 네트워크 비용이 증가한다. - 이미 생성된 JWT를 일부만 만료시킬 방법이 없다. (토큰의 유효기간을 너무 길게 잡으면 안된다.) - Secret Key 유출 시 JWT 조작이 가능하다. - Payload 자체는 암호화되지 않기 때문에 사용자의 중요한 정보는 담을 수 없다. |
구현
Filter
package com.trioshop.filter;
import com.trioshop.SessionConst;
import com.trioshop.model.dto.user.HeaderModel;
import com.trioshop.model.dto.user.UserInfoBySession;
import com.trioshop.repository.redis.RedisRepository;
import com.trioshop.service.user.UserLoginService;
import com.trioshop.utils.service.JwtTokenUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.net.URLEncoder;
import static com.trioshop.JWTConst.*;
import static java.nio.charset.StandardCharsets.UTF_8;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserLoginService userLoginService;
private final RedisRepository redisRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
// 요청 쿠키에서 JWT 토큰 추출
if (REQUEST_LOGOUT.equals(request.getRequestURI())) {
chain.doFilter(request, response);
return;
}
String token = null;
String username = null;
String nickname = null;
Long gradeCode = null;
Long userCode = null;
String refreshToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (ACCESS_TOKEN.equals(cookie.getName())) {
String header = cookie.getValue();
if (header != null && (header.startsWith(ACCESS_TOKEN_START1) || header.startsWith(ACCESS_TOKEN_START2))) {
token = header.substring(7);
try {
username = jwtTokenUtil.getUsername(token); // 토큰에서 사용자 이름 추출
if (jwtTokenUtil.validateToken(token) && !redisRepository.isTokenBlacklisted(token)) {
userCode = jwtTokenUtil.getUserCode(token);
gradeCode = jwtTokenUtil.getGradeCode(token); // 토큰에서 사용자 등급 추출
nickname = jwtTokenUtil.getUserNickname(token); // 토큰에서 사용자 닉네임 추출
request.setAttribute(SessionConst.LOGIN_MEMBER, new HeaderModel(userCode, nickname, gradeCode));
}
} catch (Exception e) {
token = null; // 토큰이 유효하지 않은 경우 null로 설정하여 refresh 토큰을 사용하도록 유도
}
}
} else if (REFRESH_TOKEN.equals(cookie.getName())) {
refreshToken = cookie.getValue();
}
}
}
if (token == null && refreshToken != null && jwtTokenUtil.validateRefreshToken(refreshToken)) {
// refresh 토큰이 유효한 경우 새로운 access 토큰 발급
try {
String newAccessToken = jwtTokenUtil.refreshToken(refreshToken);
response.addCookie(createAccessTokenCookie(newAccessToken));
response.sendRedirect(request.getRequestURI());
return;
} catch (Exception e) {
// refresh 토큰이 유효하지 않은 경우 처리
}
}
// 유저는 존재하지만, 권한이 없을 때
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserInfoBySession user = userLoginService.loadUserByUsername(username);
if (token != null && jwtTokenUtil.validateToken(token) && !redisRepository.isTokenBlacklisted(token)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
chain.doFilter(request, response);
}
private Cookie createAccessTokenCookie(String token) {
Cookie cookie = new Cookie(ACCESS_TOKEN, URLEncoder.encode(ACCESS_TOKEN_START1 + token, UTF_8));
cookie.setHttpOnly(true);
// cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(60*5); // 5분
return cookie;
}
}
Utils
package com.trioshop.utils.service;
import com.trioshop.model.dto.user.UserInfoBySession;
import com.trioshop.repository.redis.RedisRepository;
import com.trioshop.service.user.UserLoginService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static com.trioshop.JWTConst.*;
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtTokenUtil {
private final RedisRepository redisRepository;
private final UserLoginService userLoginService;
//JWT 비밀키 (예를 들면, 솔트값 같은 느낌)
@Value("${jwt.secret}")
private String secretKey;
private static final long ACCESS_EXPIRATION_TIME = 1000*60*5; //5분
private static final long REFRESH_EXPIRATION_TIME = 1000*60*60*24*7; //7일
// 만료시간 설정 1000 * 60 * 60 * 24; 1일
//사용자 정보를 기반으로 JWT를 생성해주는 메서드
public String generateToken(UserInfoBySession user) {
//JWT에 저장할 정보 추가부분
String userId = user.getUserId();
Map<String, Object> claims = new HashMap<>();
claims.put(ROLE, user.getRole());
claims.put(GRADE_CODE, user.getGradeCode());
claims.put(USER_NICKNAME, user.getUserNickname());
claims.put(USER_CODE, user.getUserCode());
return Jwts.builder()
.setClaims(claims)
.setSubject(userId) //토큰 사용 주체
.setIssuedAt(new Date()) //토큰 발급 시간
.setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXPIRATION_TIME)) //만료시간
.signWith(SignatureAlgorithm.HS512, secretKey) //비밀키 + 서명 알고리즘(HS512)를 이용해 암호화
.compact();
}
public String generateRefreshToken(UserInfoBySession user) {
String userId = user.getUserId();
String refreshToken = Jwts.builder()
.setSubject(userId)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + REFRESH_EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
redisRepository.save(userId,refreshToken);
return refreshToken;
}
public String refreshToken(String refreshToken) {
try {
Claims claims = Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(refreshToken)
.getBody();
String userId = claims.getSubject();
String storedRefreshToken = redisRepository.findById(userId);
if (refreshToken.equals(storedRefreshToken)) {
UserInfoBySession user = userLoginService.loadUserByUsername(userId);
return generateToken(user);
} else {
throw new RuntimeException("Invalid refresh token");
}
} catch (ExpiredJwtException e) {
throw new RuntimeException("Expired refresh token", e);
} catch (Exception e) {
throw new RuntimeException("Invalid refresh token", e);
}
}
public void logout(String userId, String token) {
if (userId != null) {
redisRepository.deleteToken(userId);
}
if (token != null) {
long expirationTime = getExpirationDateFromToken(token).getTime() - System.currentTimeMillis();
if (expirationTime > 0) {
redisRepository.addTokenToBlacklist(token, expirationTime);
}
}
}
//토큰에서 만료시간 추출
public Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token).getExpiration();
}
//토큰에서 사용자 이름 추출
public String getUsername(String token){
return getClaimsFromToken(token).getSubject();
}
//토큰에서 사용자 닉네임 추출
public String getUserNickname(String token){
return getClaimsFromToken(token).get(USER_NICKNAME, String.class);
}
//토큰에서 사용자 그레이드코드 추출
public Long getGradeCode(String token){
return getClaimsFromToken(token).get(GRADE_CODE, Long.class);
}
// 토큰에서 사용자 권한 추출
public Role getRoles(String token) {
return getClaimsFromToken(token).get(ROLE, Role.class);
}
//토큰에서 유저 코드 추출
public Long getUserCode(String token){
return getClaimsFromToken(token).get(USER_CODE, Long.class);
}
// 토큰에서 클레임 추출
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
}
// JWT 토큰 검증 메서드
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
return true;
} catch (ExpiredJwtException e) {
log.error("Expired JWT token: {}", e.getMessage());
return false;
} catch (Exception e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
public boolean validateRefreshToken(String token) {
try {
String username = getUsername(token);
String storedToken = redisRepository.findById(username);
return token.equals(storedToken);
} catch (ExpiredJwtException e) {
log.error("Expired Refresh JWT token: {}", e.getMessage());
return false;
} catch (Exception e) {
log.error("Invalid Refresh JWT token: {}", e.getMessage());
return false;
}
}
}
LoginHandler
package com.trioshop.utils.handler;
import com.trioshop.model.dto.user.UserInfoBySession;
import com.trioshop.utils.service.JwtTokenUtil;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import java.io.IOException;
import java.net.URLEncoder;
import static com.trioshop.JWTConst.*;
import static java.nio.charset.StandardCharsets.*;
/**
* 로그인 성공시 호출 되는 클래스(핸들러)
*/
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenUtil jwtTokenUtil;
public LoginSuccessHandler(JwtTokenUtil jwtTokenUtil) {
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//Jwt 토큰 발급 시작
UserInfoBySession user = (UserInfoBySession) authentication.getPrincipal();
loginSuccess(response, user);
if (user.getGradeCode()==4) {
response.sendRedirect("/trioAdmin");
} else {
response.sendRedirect("/");
}
}
public void loginSuccess(HttpServletResponse response, UserInfoBySession user) {
String accessToken = jwtTokenUtil.generateToken(user);
String refreshToken= jwtTokenUtil.generateRefreshToken(user);
String encodedAccessToken = URLEncoder.encode(ACCESS_TOKEN_START1 + accessToken, UTF_8);
String encodedRefreshToken = URLEncoder.encode(refreshToken, UTF_8);
Cookie jwtCookie = new Cookie(ACCESS_TOKEN, encodedAccessToken);
jwtCookie.setHttpOnly(true); // 보안을 위해 HttpOnly 플래그 설정
// jwtCookie.setSecure(true); // 애플리케이션이 HTTPS를 사용하는 경우 Secure 플래그 설정
jwtCookie.setPath("/"); // 쿠키의 유효 경로 설정
jwtCookie.setMaxAge(60 * 5); // 쿠키의 만료 시간 설정 (예: 5분)
Cookie jwtRefreshCookie = new Cookie(REFRESH_TOKEN, encodedRefreshToken);
jwtRefreshCookie.setHttpOnly(true); // 보안을 위해 HttpOnly 플래그 설정
// jwtRefreshCookie.setSecure(true); // 애플리케이션이 HTTPS를 사용하는 경우 Secure 플래그 설정
jwtRefreshCookie.setPath("/"); // 쿠키의 유효 경로 설정
jwtRefreshCookie.setMaxAge(60 * 60 * 24 * 7); ///7일
response.addCookie(jwtCookie);
response.addCookie(jwtRefreshCookie);
}
}
LogoutHandler
package com.trioshop.utils.handler;
import com.trioshop.utils.service.JwtTokenUtil;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import java.io.IOException;
@RequiredArgsConstructor
public class LogoutCustomHandler implements LogoutHandler {
private final JwtTokenUtil jwtTokenUtil;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = null;
String username = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("Authorization".equals(cookie.getName())) {
String header = cookie.getValue();
if (header != null && (header.startsWith("Bearer+") || header.startsWith("Bearer "))) {
token = header.substring(7);
username = jwtTokenUtil.getUsername(token); // 토큰에서 사용자 이름 추출
}
break;
}
}
}
jwtTokenUtil.logout(username, token); //RefreshToken 제거 및 AccessToken 블랙리스트 추가
// 쿠키 삭제
// Cookie authCookie = new Cookie("Authorization", null);
// authCookie.setPath("/");
// authCookie.setMaxAge(0);
// response.addCookie(authCookie);
//
// Cookie refreshCookie = new Cookie("refreshCookie", null);
// refreshCookie.setPath("/");
// refreshCookie.setMaxAge(0);
// response.addCookie(refreshCookie);
try {
response.sendRedirect("/");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}