admin: RT 를 통한 AT 재 발급 시 IP&RT expired date time 체크 로직 추가

This commit is contained in:
geonhos 2024-05-23 15:11:15 +09:00
parent 6a1b4fe0c5
commit a3404947a0
12 changed files with 114 additions and 34 deletions

View File

@ -3,11 +3,14 @@ package com.bpgroup.poc.admin.app.jwt;
import com.bpgroup.poc.admin.domain.admin.entity.Admin; import com.bpgroup.poc.admin.domain.admin.entity.Admin;
import com.bpgroup.poc.admin.domain.admin.service.AdminService; import com.bpgroup.poc.admin.domain.admin.service.AdminService;
import com.bpgroup.poc.admin.domain.admin.service.AdminTokenCreateCommand; import com.bpgroup.poc.admin.domain.admin.service.AdminTokenCreateCommand;
import com.bpgroup.poc.admin.domain.admin.service.AdminTokenExpireCommand;
import com.bpgroup.poc.admin.domain.admin.service.AdminTokenService; import com.bpgroup.poc.admin.domain.admin.service.AdminTokenService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@Transactional @Transactional
@ -16,9 +19,30 @@ public class JwtTokenAppService {
private final AdminService adminService; private final AdminService adminService;
private final AdminTokenService adminTokenService; private final AdminTokenService adminTokenService;
public void save(JwtTokenRequest request) { public void saveNewToken(JwtTokenRequest request) {
Admin findAdmin = adminService.find(request.getLoginId()).orElseThrow(() -> new IllegalArgumentException("Not found admin")); 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
)
);
}
public void saveRegenerateToken(JwtTokenRequest request) {
Admin findAdmin = adminService.find(request.getLoginId()).orElseThrow(() -> new IllegalArgumentException("Not found admin"));
adminTokenService.expire(
AdminTokenExpireCommand.of(
findAdmin.getId(),
request.getIp(),
LocalDateTime.now()
)
);
adminTokenService.create( adminTokenService.create(
AdminTokenCreateCommand.of( AdminTokenCreateCommand.of(

View File

@ -0,0 +1,7 @@
package com.bpgroup.poc.admin.domain;
public class DomainException extends RuntimeException {
public DomainException(String message) {
super(message);
}
}

View File

@ -3,10 +3,12 @@ package com.bpgroup.poc.admin.domain.admin.entity;
import com.bpgroup.poc.admin.domain.BaseEntity; import com.bpgroup.poc.admin.domain.BaseEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Getter
@Entity @Entity
@Table(name = "admin_token") @Table(name = "admin_token")
@NoArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED)
@ -56,7 +58,11 @@ public class AdminToken extends BaseEntity {
return new AdminToken(ip, accessToken, refreshToken, expiredRefreshTokenDateTime, state, admin); return new AdminToken(ip, accessToken, refreshToken, expiredRefreshTokenDateTime, state, admin);
} }
public void expire(TokenState state) { public void expire() {
this.state = state; this.state = TokenState.EXPIRED;
}
public boolean isNotExpired(LocalDateTime dateTime) {
return this.expiredRefreshTokenDateTime.isAfter(dateTime);
} }
} }

View File

@ -2,8 +2,8 @@ package com.bpgroup.poc.admin.domain.admin.entity;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List; import java.util.Optional;
public interface AdminTokenRepository extends JpaRepository<AdminToken, Long> { public interface AdminTokenRepository extends JpaRepository<AdminToken, Long> {
List<AdminToken> findByAdminIdAndState(Long adminId, TokenState state); Optional<AdminToken> findLastByAdminIdAndIpAndState(Long adminId, String ip, TokenState state);
} }

View File

@ -0,0 +1,28 @@
package com.bpgroup.poc.admin.domain.admin.service;
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 AdminTokenExpireCommand {
@NotNull
private final Long adminId;
@NotBlank
private final String ip;
@NotNull
private final LocalDateTime dateTime;
public static AdminTokenExpireCommand of(
Long adminId,
String ip,
LocalDateTime dateTime
) {
return new AdminTokenExpireCommand(adminId, ip, dateTime);
}
}

View File

@ -1,6 +1,8 @@
package com.bpgroup.poc.admin.domain.admin.service; package com.bpgroup.poc.admin.domain.admin.service;
import com.bpgroup.poc.admin.domain.admin.entity.*; import com.bpgroup.poc.admin.domain.admin.entity.AdminToken;
import com.bpgroup.poc.admin.domain.admin.entity.AdminTokenRepository;
import com.bpgroup.poc.admin.domain.admin.entity.TokenState;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -8,7 +10,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.util.List; import java.time.LocalDateTime;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -22,11 +24,15 @@ public class AdminTokenService {
adminTokenRepository.save(command.toEntity()); adminTokenRepository.save(command.toEntity());
} }
public void expire(@NotNull Long adminId) { public void expire(@NotNull @Valid AdminTokenExpireCommand command) {
List<AdminToken> adminTokens = adminTokenRepository.findByAdminIdAndState(adminId, TokenState.NORMAL); AdminToken findAdminToken = adminTokenRepository.findLastByAdminIdAndIpAndState(command.getAdminId(), command.getIp(), TokenState.NORMAL)
adminTokens.forEach( .orElseThrow(() -> new IllegalArgumentException("Not found token"));
adminToken -> adminToken.expire(TokenState.EXPIRED)
); if (findAdminToken.isNotExpired(LocalDateTime.now())) {
throw new IllegalArgumentException("Token is expired");
}
findAdminToken.expire();
} }
} }

View File

@ -1,20 +1,18 @@
package com.bpgroup.poc.admin.security.authentication; package com.bpgroup.poc.admin.security.authentication;
import com.bpgroup.poc.admin.app.authentication.AuthenticationResult;
import com.bpgroup.poc.admin.app.authentication.AuthenticationAppService; import com.bpgroup.poc.admin.app.authentication.AuthenticationAppService;
import com.bpgroup.poc.admin.app.authentication.AuthenticationResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider { public class CustomAuthenticationProvider implements AuthenticationProvider {
private final AuthenticationAppService authenticationAppService; private final AuthenticationAppService authenticationAppService;
@Transactional
@Override @Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException { public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try { try {

View File

@ -22,7 +22,7 @@ public class CustomAuthenticationSuccessHandler implements AuthenticationSuccess
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException { public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
String username = (String) authentication.getPrincipal(); String username = (String) authentication.getPrincipal();
jwtTokenProvider.generateToken(request, response, username); jwtTokenProvider.generateToken(request, response, username, JwtTokenProvider.JwtTokenIssueType.GENERATE);
response.setStatus(HttpServletResponse.SC_OK); response.setStatus(HttpServletResponse.SC_OK);
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);

View File

@ -1,8 +1,10 @@
package com.bpgroup.poc.admin.security.jwt; package com.bpgroup.poc.admin.security.jwt;
import com.bpgroup.poc.admin.app.jwt.JwtTokenRequest;
import com.bpgroup.poc.admin.app.jwt.JwtTokenAppService; import com.bpgroup.poc.admin.app.jwt.JwtTokenAppService;
import com.bpgroup.poc.admin.app.jwt.JwtTokenRequest;
import com.bpgroup.poc.admin.domain.admin.entity.TokenState; import com.bpgroup.poc.admin.domain.admin.entity.TokenState;
import com.bpgroup.poc.admin.security.jwt.exception.JwtTokenExpiredException;
import com.bpgroup.poc.admin.security.jwt.exception.JwtTokenInvalidException;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
@ -27,23 +29,30 @@ public class JwtTokenProvider {
// TODO: 추후 Key 별도 관리 필요 // TODO: 추후 Key 별도 관리 필요
private static final SecretKey JWT_KEY = Keys.hmacShaKeyFor(JwtTokenConstants.KEY.getBytes(StandardCharsets.UTF_8)); private static final SecretKey JWT_KEY = Keys.hmacShaKeyFor(JwtTokenConstants.KEY.getBytes(StandardCharsets.UTF_8));
public void generateToken(HttpServletRequest request, HttpServletResponse response, String username) { public enum JwtTokenIssueType {
GENERATE, REGENERATE
}
public void generateToken(HttpServletRequest request, HttpServletResponse response, String username, JwtTokenIssueType issueType) {
Cookie accessTokenCookie = createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS); Cookie accessTokenCookie = createCookieWithToken(username, JwtTokenProvider.JwtTokenType.ACCESS);
response.addCookie(accessTokenCookie); response.addCookie(accessTokenCookie);
Cookie refreshTokenCookie = createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH); Cookie refreshTokenCookie = createCookieWithToken(username, JwtTokenProvider.JwtTokenType.REFRESH);
response.addCookie(refreshTokenCookie); response.addCookie(refreshTokenCookie);
jwtTokenAppService.save( JwtTokenRequest jwtTokenRequest = JwtTokenRequest.of(
JwtTokenRequest.of( request.getRemoteAddr(),
request.getRemoteAddr(), accessTokenCookie.getValue(),
accessTokenCookie.getValue(), refreshTokenCookie.getValue(),
refreshTokenCookie.getValue(), LocalDateTime.now().plusSeconds(refreshTokenCookie.getMaxAge()),
LocalDateTime.now().plusSeconds(refreshTokenCookie.getMaxAge()), TokenState.NORMAL,
TokenState.NORMAL, username
username
)
); );
switch (issueType) {
case GENERATE -> jwtTokenAppService.saveNewToken(jwtTokenRequest);
case REGENERATE -> jwtTokenAppService.saveRegenerateToken(jwtTokenRequest);
}
} }
public enum JwtTokenType { public enum JwtTokenType {
@ -65,7 +74,7 @@ public class JwtTokenProvider {
expirationTime = JwtTokenConstants.RT_EXPIRATION_TIME; expirationTime = JwtTokenConstants.RT_EXPIRATION_TIME;
} }
String token = generateToken(JwtTokenConstants.ISSUER, subject, username, expirationTime); String token = generateToken(subject, username, expirationTime);
Cookie tokenCookie = new Cookie(tokenName, token); Cookie tokenCookie = new Cookie(tokenName, token);
tokenCookie.setHttpOnly(true); tokenCookie.setHttpOnly(true);
@ -74,9 +83,9 @@ public class JwtTokenProvider {
return tokenCookie; return tokenCookie;
} }
private String generateToken(String issuer, String subject, String username, long expirationTime) { private String generateToken(String subject, String username, long expirationTime) {
return Jwts.builder() return Jwts.builder()
.issuer(issuer) .issuer(JwtTokenConstants.ISSUER)
.subject(subject) .subject(subject)
.claim("username", username) .issuedAt(new Date()) .claim("username", username) .issuedAt(new Date())
.expiration(getExpirationDate(expirationTime)) .expiration(getExpirationDate(expirationTime))

View File

@ -1,5 +1,7 @@
package com.bpgroup.poc.admin.security.jwt; package com.bpgroup.poc.admin.security.jwt;
import com.bpgroup.poc.admin.security.jwt.exception.JwtTokenExpiredException;
import com.bpgroup.poc.admin.security.jwt.exception.JwtTokenInvalidException;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@ -56,7 +58,7 @@ public class JwtTokenValidateFilter extends OncePerRequestFilter {
Claims claims = jwtTokenProvider.getClaims(refreshToken); Claims claims = jwtTokenProvider.getClaims(refreshToken);
String username = claims.get("username", String.class); String username = claims.get("username", String.class);
jwtTokenProvider.generateToken(request, response, username); jwtTokenProvider.generateToken(request, response, username, JwtTokenProvider.JwtTokenIssueType.REGENERATE);
setSecurityContext(username); setSecurityContext(username);
} catch (JwtTokenExpiredException e) { } catch (JwtTokenExpiredException e) {

View File

@ -1,4 +1,4 @@
package com.bpgroup.poc.admin.security.jwt; package com.bpgroup.poc.admin.security.jwt.exception;
public class JwtTokenExpiredException extends Exception { public class JwtTokenExpiredException extends Exception {
public JwtTokenExpiredException() { public JwtTokenExpiredException() {

View File

@ -1,4 +1,4 @@
package com.bpgroup.poc.admin.security.jwt; package com.bpgroup.poc.admin.security.jwt.exception;
public class JwtTokenInvalidException extends Exception { public class JwtTokenInvalidException extends Exception {
public JwtTokenInvalidException() { public JwtTokenInvalidException() {