diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/app/jwt/JwtTokenRequest.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/app/jwt/JwtTokenRequest.java new file mode 100644 index 0000000..10fbebf --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/app/jwt/JwtTokenRequest.java @@ -0,0 +1,30 @@ +package com.bpgroup.poc.admin.app.jwt; + +import com.bpgroup.poc.admin.domain.base.admin.entity.TokenState; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class JwtTokenRequest { + private final String ip; + private final String accessToken; + private final String refreshToken; + private final LocalDateTime expiredRefreshToken; + private final TokenState state; + private final String loginId; + + public static JwtTokenRequest of( + String ip, + String accessToken, + String refreshToken, + LocalDateTime expiredRefreshToken, + TokenState state, + String loginId + ) { + return new JwtTokenRequest(ip, accessToken, refreshToken, expiredRefreshToken, state, loginId); + } +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/app/jwt/JwtTokenService.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/app/jwt/JwtTokenService.java new file mode 100644 index 0000000..ee74098 --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/app/jwt/JwtTokenService.java @@ -0,0 +1,35 @@ +package com.bpgroup.poc.admin.app.jwt; + +import com.bpgroup.poc.admin.domain.base.admin.entity.Admin; +import com.bpgroup.poc.admin.domain.base.admin.service.AdminService; +import com.bpgroup.poc.admin.domain.base.admin.service.AdminTokenCreateCommand; +import com.bpgroup.poc.admin.domain.base.admin.service.AdminTokenService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +public class JwtTokenService { + + private final AdminService adminService; + private final AdminTokenService adminTokenService; + + public void save(JwtTokenRequest request) { + Admin findAdmin = adminService.find(request.getLoginId()).orElseThrow(() -> new IllegalArgumentException("Not found admin")); + adminTokenService.expire(findAdmin.getId()); + + adminTokenService.create( + AdminTokenCreateCommand.of( + request.getIp(), + request.getAccessToken(), + request.getRefreshToken(), + request.getExpiredRefreshToken(), + request.getState(), + findAdmin + ) + ); + } + +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java index 69a328d..2d870dc 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminToken.java @@ -1,5 +1,6 @@ package com.bpgroup.poc.admin.domain.base.admin.entity; +import com.bpgroup.poc.admin.domain.base.BaseEntity; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -9,23 +10,53 @@ import java.time.LocalDateTime; @Entity @Table(name = "admin_token") @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class AdminToken { +public class AdminToken extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Column(name = "ip", nullable = false) + private String ip; + @Column(name = "access_token", nullable = false) private String accessToken; - @Column(name = "refresh_token", length = 32, nullable = false) + @Column(name = "refresh_token", nullable = false) private String refreshToken; - @Column(name = "expired_refresh_token", nullable = false) - private LocalDateTime expiredRefreshToken; + @Column(name = "expired_refresh_token_date_time", nullable = false) + private LocalDateTime expiredRefreshTokenDateTime; + + @Column(name = "state", nullable = false) + @Enumerated(EnumType.STRING) + private TokenState state; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "admin_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT), nullable = false) private Admin admin; + private AdminToken(String ip, String accessToken, String refreshToken, LocalDateTime expiredRefreshTokenDateTime, TokenState state, Admin admin) { + this.ip = ip; + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiredRefreshTokenDateTime = expiredRefreshTokenDateTime; + this.state = state; + this.admin = admin; + } + + public static AdminToken createOf( + String ip, + String accessToken, + String refreshToken, + LocalDateTime expiredRefreshTokenDateTime, + TokenState state, + Admin admin + ) { + return new AdminToken(ip, accessToken, refreshToken, expiredRefreshTokenDateTime, state, admin); + } + + public void expire(TokenState state) { + this.state = state; + } } diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java index 51314d8..8e95336 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/AdminTokenRepository.java @@ -2,5 +2,8 @@ package com.bpgroup.poc.admin.domain.base.admin.entity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface AdminTokenRepository extends JpaRepository { + List findByAdminIdAndState(Long adminId, TokenState state); } diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/TokenState.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/TokenState.java new file mode 100644 index 0000000..d21be6c --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/entity/TokenState.java @@ -0,0 +1,5 @@ +package com.bpgroup.poc.admin.domain.base.admin.entity; + +public enum TokenState { + NORMAL, EXPIRED +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/service/AdminTokenCreateCommand.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/service/AdminTokenCreateCommand.java new file mode 100644 index 0000000..a877d49 --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/service/AdminTokenCreateCommand.java @@ -0,0 +1,45 @@ +package com.bpgroup.poc.admin.domain.base.admin.service; + +import com.bpgroup.poc.admin.domain.base.admin.entity.Admin; +import com.bpgroup.poc.admin.domain.base.admin.entity.AdminToken; +import com.bpgroup.poc.admin.domain.base.admin.entity.TokenState; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class AdminTokenCreateCommand { + @NotBlank + private final String ip; + @NotBlank + private final String accessToken; + @NotBlank + private final String refreshToken; + @NotNull + private final LocalDateTime expiredRefreshTokenDateTime; + @NotNull + private final TokenState state; + @NotNull + private final Admin admin; + + public static AdminTokenCreateCommand of( + String ip, + String accessToken, + String refreshToken, + LocalDateTime expiredRefreshTokenDateTime, + TokenState state, + Admin admin + ) { + return new AdminTokenCreateCommand(ip, accessToken, refreshToken, expiredRefreshTokenDateTime, state, admin); + } + + public AdminToken toEntity() { + return AdminToken.createOf(ip, accessToken, refreshToken, expiredRefreshTokenDateTime, state, admin); + } + +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/service/AdminTokenService.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/service/AdminTokenService.java new file mode 100644 index 0000000..d57e2c6 --- /dev/null +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/domain/base/admin/service/AdminTokenService.java @@ -0,0 +1,32 @@ +package com.bpgroup.poc.admin.domain.base.admin.service; + +import com.bpgroup.poc.admin.domain.base.admin.entity.*; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Validated +@Transactional +public class AdminTokenService { + + private final AdminTokenRepository adminTokenRepository; + + public void create(@NotNull @Valid AdminTokenCreateCommand command) { + adminTokenRepository.save(command.toEntity()); + } + + public void expire(@NotNull Long adminId) { + List adminTokens = adminTokenRepository.findByAdminIdAndState(adminId, TokenState.NORMAL); + adminTokens.forEach( + adminToken -> adminToken.expire(TokenState.EXPIRED) + ); + } + +} diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java index de6ab08..be33e24 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/SecurityConfig.java @@ -1,5 +1,6 @@ package com.bpgroup.poc.admin.security; +import com.bpgroup.poc.admin.app.jwt.JwtTokenService; import com.bpgroup.poc.admin.security.authentication.*; import com.bpgroup.poc.admin.app.authentication.AuthenticationService; import com.bpgroup.poc.admin.security.authorization.CustomAccessDeniedHandler; @@ -37,6 +38,7 @@ public class SecurityConfig { private final AuthenticationService authenticationService; private final AuthorizationService authorizationService; + private final JwtTokenService jwtTokenService; private final JwtTokenProvider jwtTokenProvider; diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java index 5db7640..219a742 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/authentication/CustomAuthenticationSuccessHandler.java @@ -3,7 +3,6 @@ package com.bpgroup.poc.admin.security.authentication; import com.bpgroup.poc.admin.app.authentication.AuthenticationResponse; import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider; import com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -23,11 +22,7 @@ public class CustomAuthenticationSuccessHandler implements AuthenticationSuccess public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { String username = (String) authentication.getPrincipal(); - Cookie accessTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS); - response.addCookie(accessTokenCookie); - - Cookie refreshTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH); - response.addCookie(refreshTokenCookie); + jwtTokenProvider.generateToken(request, response, username); response.setStatus(HttpServletResponse.SC_OK); response.setContentType(MediaType.APPLICATION_JSON_VALUE); diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java index dec1dc3..3f32bb0 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenProvider.java @@ -1,27 +1,56 @@ package com.bpgroup.poc.admin.security.jwt; +import com.bpgroup.poc.admin.app.jwt.JwtTokenRequest; +import com.bpgroup.poc.admin.app.jwt.JwtTokenService; +import com.bpgroup.poc.admin.domain.base.admin.entity.TokenState; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import javax.crypto.SecretKey; import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; import java.util.Date; @Component +@RequiredArgsConstructor public class JwtTokenProvider { + private final JwtTokenService jwtTokenService; + // TODO: 추후 Key 별도 관리 필요 private static final SecretKey JWT_KEY = Keys.hmacShaKeyFor(JwtTokenConstants.KEY.getBytes(StandardCharsets.UTF_8)); + public void generateToken(HttpServletRequest request, HttpServletResponse response, String username) { + Cookie accessTokenCookie = createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS); + response.addCookie(accessTokenCookie); + + Cookie refreshTokenCookie = createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH); + response.addCookie(refreshTokenCookie); + + jwtTokenService.save( + JwtTokenRequest.of( + request.getRemoteAddr(), + accessTokenCookie.getValue(), + refreshTokenCookie.getValue(), + LocalDateTime.now().plusSeconds(refreshTokenCookie.getMaxAge()), + TokenState.NORMAL, + username + ) + ); + } + public enum JwtTokenType { ACCESS, REFRESH } - public Cookie createCookieWithToken(String username, JwtTokenType type) { + private Cookie createCookieWithToken(String username, JwtTokenType type) { String tokenName; String subject; long expirationTime; @@ -45,16 +74,6 @@ public class JwtTokenProvider { return tokenCookie; } - private Cookie createCookieWithToken(String tokenName, String subject, String username, long expirationTime) { - String token = generateToken(JwtTokenConstants.ISSUER, subject, username, expirationTime); - - Cookie tokenCookie = new Cookie(tokenName, token); - tokenCookie.setHttpOnly(true); - tokenCookie.setPath("/"); - - return tokenCookie; - } - private String generateToken(String issuer, String subject, String username, long expirationTime) { return Jwts.builder() .issuer(issuer) diff --git a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java index 0102217..df789da 100644 --- a/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java +++ b/poc/admin/src/main/java/com/bpgroup/poc/admin/security/jwt/JwtTokenValidateFilter.java @@ -56,11 +56,7 @@ public class JwtTokenValidateFilter extends OncePerRequestFilter { Claims claims = jwtTokenProvider.getClaims(refreshToken); String username = claims.get("username", String.class); - Cookie accessTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS); - response.addCookie(accessTokenCookie); - - Cookie refreshTokenCookie = jwtTokenProvider.createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH); - response.addCookie(refreshTokenCookie); + jwtTokenProvider.generateToken(request, response, username); setSecurityContext(username); } catch (JwtTokenExpiredException e) {