본문 바로가기
Framework/Spring

[Spring / ToyProject] Spring Security 설정 - 3_1

by wo__ongii 2025. 1. 16.
728x90

[JWT를 활용하여 인증과 토큰을 관리하는 서비스 구현]

1. build.gradle 의존성 추가

/* jwt */
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'com.auth0:java-jwt:3.13.0'
 

2. entity, repository 수정

1) entity 수정

//jwt refreshToken 추가
@Column(length = 1000)
private String refreshToken;

public void updateRefreshToken(String refreshToken) {
    this.refreshToken = refreshToken;
}

public void destroyRefreshToken() {
    this.refreshToken = null;
}

2) repository 수정

//refreshToken을 통해 유저를 조회하는 기능을 추가

Optional<UserEntity> findByRefreshToken(String refreshToken);

3.JwtService

1) application-jwt.yml

jwt:
  secret: 시크릿 키
  access:
    expiration: 80 //80초
    header: Authorization

  refresh:
    expiration: 90 //90초
    header: Authorization-refresh

 

  • jwt.secret: JWT 서명에 사용되는 비밀 키로, 이 키는 JWT를 생성할 때 서명에 사용되며, 토큰의 유효성을 검증할 때도 필요하다. 키 생성은 아래와 같이 랜덤키 생성하여 넣어줬다.
//32바이트 랜덤 키 생성
openssl rand -hex 32

//64바이트 랜덤 키 생성
openssl rand -hex 64
  • jwt.access.expiration: 액세스 토큰의 만료 시간(초)이다.
  • jwt.access.header: 액세스 토큰이 포함될 HTTP 헤더의 이름이다. 클라이언트는 Authorization 헤더에 액세스 토큰을 전달해야 한다.
  • jwt.refresh.expiration: 리프레시 토큰의 만료 시간(초)이다.
  • jwt.refresh.header: 리프레시 토큰이 포함될 HTTP 헤더의 이름이다. 클라이언트는 Authorization-refresh 헤더에 리프레시 토큰을 전달해야 한다.

 

2) JwtService.java 인터페이스 구현

JwtService 인터페이스는 JWT 관련 주요 기능을 선언한 인터페이스이다. 이 인터페이스의 목적은 JWT 액세스 토큰과 리프레시 토큰을 생성, 검증, 갱신, 파기하는 기능을 정의하는 것이다.

public interface JwtService {
    String createAccessToken(String userEmail)
    String createRefreshToken();
    void updateRefreshToken(String userEmail, String refreshToken);
    void destoryRefreshToken(String userEmail);
    void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken);
    void sendAccessToken(HttpServletResponse response, String accessToken);
    Optional<String> extractAccessToken(HttpServletRequest request);
    Optional<String> extractRefreshToken(HttpServletRequest request);
    Optional<String> extractEmail(String accessToken);
    void setAccessTokenHeader(HttpServletResponse response, String accessToken);
    void setRefreshTokenHeader(HttpServletResponse response, String refreshToken);
    boolean isTokenValue(String token);
}

 

  • createAccessToken(String userEmail): 사용자의 이메일을 기반으로 액세스 토큰을 생성한다.
  • createRefreshToken(): 리프레시 토큰을 생성한다.
  • updateRefreshToken(String userEmail, String refreshToken): 특정 사용자의 리프레시 토큰을 업데이트한다.
  • destoryRefreshToken(String userEmail): 특정 사용자의 리프레시 토큰을 삭제한다.
  • sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken): 응답에 액세스 토큰과 리프레시 토큰을 헤더로 포함시킨다.
  • sendAccessToken(HttpServletResponse response, String accessToken): 응답에 액세스 토큰을 헤더로 포함시킨다.
  • extractAccessToken(HttpServletRequest request): 요청에서 액세스 토큰을 추출한다.
  • extractRefreshToken(HttpServletRequest request): 요청에서 리프레시 토큰을 추출한다.
  • extractEmail(String accessToken): 액세스 토큰에서 이메일 정보를 추출한다.
  • setAccessTokenHeader(HttpServletResponse response, String accessToken): 응답에 액세스 토큰을 헤더로 설정한다.
  • setRefreshTokenHeader(HttpServletResponse response, String refreshToken): 응답에 리프레시 토큰을 헤더로 설정한다.
  • isTokenValue(String token): 토큰이 유효한지 검증한다.

 

4) JwtServiceImpl.java 구현체 구현

JwtServiceImpl 클래스는 JwtService 인터페이스의 구현체로, JWT 생성, 검증, 토큰 갱신 등을 담당한다.

 

[필드]

@Value("${jwt.secret}")
private String secret;
@Value("${jwt.accsee.expiration}")
private long accessTokenValidityInSeconds;
@Value("${jwt.refresh.expiration}")
private long refreshTokenValidityInSeconds;
@Value("${jwt.accsee.header}")
private String accessHeader;
@Value("${jwt.refresh.header}")
private String refreshHeader;

private static final String ACCESS_TOKEN_SUBJECT = "AccessToken";
private static final String REFRESH_TOKEN_SUBJECT = "RefreshToken";
private static final String USERNAME_CLAIM = "email";
private static final String BEARER = "Bearer";

private final UserRepository userRepository;

 

  • secret: application-jwt.yml에서 정의된 JWT 비밀
  • accessTokenValidityInSeconds: application-jwt.yml에서 설정된 액세스 토큰의 유효 기간(초)
  • refreshTokenValidityInSeconds: application-jwt.yml에서 설정된 리프레시 토큰의 유효 기간(초)
  • accessHeader, refreshHeader: 각각 액세스 토큰과 리프레시 토큰을 헤더에서 추출하기 위한 설정값

[주요 메서드]

@Override
public String createAccessToken(String userEmail) {
    return JWT.create()
            .withSubject(ACCESS_TOKEN_SUBJECT)
            .withExpiresAt(new Date(System.currentTimeMillis() + accessTokenValidityInSeconds * 1000))
            .withClaim(USERNAME_CLAIM, userEmail)
            .sign(Algorithm.HMAC512(secret));
}

@Override
public String createRefreshToken() {
    return JWT.create()
            .withSubject(REFRESH_TOKEN_SUBJECT)
            .withExpiresAt(new Date(System.currentTimeMillis() + refreshTokenValidityInSeconds * 1000))
            .sign(Algorithm.HMAC512(secret));
}

@Override
public void updateRefreshToken(String userEmail, String refreshToken) {
    userRepository.findByUserEmail(userEmail)
            .ifPresentOrElse(users -> users.updateRefreshToken(refreshToken),
                    () -> new Exception("updateRefreshToken() 에러 - 조회 실패"));
}

@Override
public void destoryRefreshToken(String userEmail) {
    userRepository.findByUserEmail(userEmail)
            .ifPresentOrElse(users -> users.destroyRefreshToken(),
                    () -> new Exception("destoryRefreshToken() 에러 - 조회 실패"));
}

@Override
public void sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken) {
    response.setStatus(HttpServletResponse.SC_OK);
    setAccessTokenHeader(response, accessToken);
    setRefreshTokenHeader(response, refreshToken);

    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
    tokenMap.put(REFRESH_TOKEN_SUBJECT, refreshToken);
}

@Override
public void sendAccessToken(HttpServletResponse response, String accessToken) {
    response.setStatus(HttpServletResponse.SC_OK);
    setAccessTokenHeader(response, accessToken);

    Map<String, String> tokenMap = new HashMap<>();
    tokenMap.put(ACCESS_TOKEN_SUBJECT, accessToken);
}

@Override
public Optional<String> extractAccessToken(HttpServletRequest request) {
    return Optional.ofNullable(request.getHeader(accessHeader)).filter(
            accessToken -> accessToken.startsWith(BEARER)).map(accessToken -> accessToken.replace(BEARER, ""));
}

@Override
public Optional<String> extractRefreshToken(HttpServletRequest request) {
    return Optional.ofNullable(request.getHeader(refreshHeader)).filter(
            refreshToken -> refreshToken.startsWith(BEARER)).map(refreshToken -> refreshToken.replace(BEARER, ""));
}

@Override
public Optional<String> extractEmail(String accessToken) {
    try {
        return Optional.ofNullable(JWT.require(Algorithm.HMAC512(secret)).build().verify(accessToken)
                .getClaim(USERNAME_CLAIM).asString());
    } catch (Exception e) {
        log.error(e.getMessage());
        return Optional.empty();
    }
}

@Override
public void setAccessTokenHeader(HttpServletResponse response, String accessToken) {
    response.setHeader(accessHeader, accessToken);
}

@Override
public void setRefreshTokenHeader(HttpServletResponse response, String refreshToken) {
    response.setHeader(refreshHeader, refreshToken);
}

@Override
public boolean isTokenValue(String token) {
    try {
        JWT.require(Algorithm.HMAC512(secret)).build().verify(token);
        return true;
    } catch (Exception e) {
        log.error(e.getMessage());
        return false;
    }
}
  1. createAccessToken(String userEmail):
    • 사용자의 이메일을 포함한 액세스 토큰 생성
    • JWT.create()를 사용하여 JWT를 생성하고, withSubject()로 토큰의 주제를 설정하며, withExpiresAt()으로 만료 시간을 설정한다.
    • withClaim(USERNAME_CLAIM, userEmail)으로 사용자 이메일을 JWT의 클레임에 추가한다. (토큰에 포함된 데이터의 조각으로, 토큰의 본문(payload) 부분에 저장)
    • sign(Algorithm.HMAC512(secret))는 secret을 사용하여 JWT에 서명을 추가한다.
  2. createRefreshToken():
    • 리프레시 토큰을 생성합니다. 리프레시 토큰은 이메일 등의 사용자 정보 없이 만료 시간만 설정된다.
  3. updateRefreshToken(String userEmail, String refreshToken):
    • userRepository에서 사용자를 조회한 후, 해당 사용자의 리프레시 토큰을 갱신하고, 사용자가 없을 경우 예외를 던진다.
  4. destoryRefreshToken(String userEmail):
    • 사용자의 리프레시 토큰을 파기한다. 사용자 조회 후, destroyRefreshToken() 메서드를 호출하여 리프레시 토큰을 삭제한다.
  5. sendAccessAndRefreshToken(HttpServletResponse response, String accessToken, String refreshToken):
    • 응답에 액세스 토큰과 리프레시 토큰을 설정한다. setAccessTokenHeader()와 setRefreshTokenHeader()를 사용하여 헤더에 추가한다.
  6. sendAccessToken(HttpServletResponse response, String accessToken):
    • 응답에 액세스 토큰만을 설정한다.
  7. extractAccessToken(HttpServletRequest request):
    • 요청에서 액세스 토큰을 추출한다. Authorization 헤더에서 Bearer로 시작하는 토큰을 찾는다.
  8. extractRefreshToken(HttpServletRequest request):
    • 요청에서 리프레시 토큰을 추출한다. Authorization-refresh 헤더에서 Bearer로 시작하는 토큰을 찾는다.
  9. extractEmail(String accessToken):
    • 액세스 토큰에서 이메일을 추출한다. JWT를 검증하고, 이메일 클레임을 반환한다.
  10. setAccessTokenHeader(HttpServletResponse response, String accessToken):
    • 응답의 Authorization 헤더에 액세스 토큰을 설정한다.
  11. setRefreshTokenHeader(HttpServletResponse response, String refreshToken):
    • 응답의 Authorization-refresh 헤더에 리프레시 토큰을 설정한다.
  12. isTokenValue(String token):
    • 주어진 토큰이 유효한지 검증한다. JWT를 검증하여 예외가 발생하지 않으면 유효한 토큰으로 간주한다.

 

728x90
반응형