diff --git a/src/main/java/com/pjw/retry_view/controller/LoginController.java b/src/main/java/com/pjw/retry_view/controller/LoginController.java index aa2429a..121df23 100644 --- a/src/main/java/com/pjw/retry_view/controller/LoginController.java +++ b/src/main/java/com/pjw/retry_view/controller/LoginController.java @@ -1,10 +1,13 @@ package com.pjw.retry_view.controller; +import com.pjw.retry_view.response.JWToken; import com.pjw.retry_view.dto.UserDTO; +import com.pjw.retry_view.dto.UserInfo; import com.pjw.retry_view.request.LoginRequest; import com.pjw.retry_view.response.LoginResponse; import com.pjw.retry_view.service.JWTService; import com.pjw.retry_view.service.UserService; +import com.pjw.retry_view.util.JWTUtil; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -13,9 +16,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.HashMap; -import java.util.Map; - @RestController @RequestMapping("/login") public class LoginController { @@ -31,13 +31,17 @@ public LoginController(UserService userService, JWTService jwtService){ public ResponseEntity userLogin(@RequestBody @Valid LoginRequest loginReq){ UserDTO user = userService.userLogin(loginReq); - Map userInfo = new HashMap<>(); - userInfo.put("name", user.getName()); - userInfo.put("loginId", user.getLoginId()); + UserInfo userInfo = new UserInfo(user.getName(), user.getLoginId()); LoginResponse response = new LoginResponse(); - response.setAccessToken(jwtService.createAccessToken(userInfo)); - response.setRefreshToken(jwtService.createRefreshToken()); + String refreshToken = JWTUtil.createRefreshToken(); + JWToken token = JWToken.getJWT(JWTUtil.createAccessToken(userInfo), refreshToken); + + response.setToken(token); + + user.changeRefereshToken(refreshToken); + userService.saveUser(user); + return new ResponseEntity(response, HttpStatus.OK); } } diff --git a/src/main/java/com/pjw/retry_view/controller/TokenController.java b/src/main/java/com/pjw/retry_view/controller/TokenController.java new file mode 100644 index 0000000..8610297 --- /dev/null +++ b/src/main/java/com/pjw/retry_view/controller/TokenController.java @@ -0,0 +1,25 @@ +package com.pjw.retry_view.controller; + +import com.pjw.retry_view.response.JWToken; +import com.pjw.retry_view.service.JWTService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping("/token") +public class TokenController { + private final JWTService jwtService; + + public TokenController(JWTService jwtService) { + this.jwtService = jwtService; + } + + @PostMapping + public JWToken renewAccessToken(@RequestBody JWToken token){ + return jwtService.renewAccessToken(token.getRefreshToken()); + } +} diff --git a/src/main/java/com/pjw/retry_view/dto/UserDTO.java b/src/main/java/com/pjw/retry_view/dto/UserDTO.java index 59cf680..d98a66f 100644 --- a/src/main/java/com/pjw/retry_view/dto/UserDTO.java +++ b/src/main/java/com/pjw/retry_view/dto/UserDTO.java @@ -30,6 +30,11 @@ public class UserDTO implements Serializable { private String updatedBy; private ZonedDateTime updatedAt; + public void changeRefereshToken(String token){ + this.refreshToken = token; + this.updatedAt = ZonedDateTime.now(); + } + public User toEntity(){ return User.builder() .id(id) diff --git a/src/main/java/com/pjw/retry_view/dto/UserInfo.java b/src/main/java/com/pjw/retry_view/dto/UserInfo.java new file mode 100644 index 0000000..628ca51 --- /dev/null +++ b/src/main/java/com/pjw/retry_view/dto/UserInfo.java @@ -0,0 +1,17 @@ +package com.pjw.retry_view.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class UserInfo { + private String name; + private String loginId; + + public UserInfo(){} + public UserInfo(String name, String loginId){ + this.name = name; + this.loginId = loginId; + } +} diff --git a/src/main/java/com/pjw/retry_view/exception/InvalidTokenException.java b/src/main/java/com/pjw/retry_view/exception/InvalidTokenException.java new file mode 100644 index 0000000..f84756d --- /dev/null +++ b/src/main/java/com/pjw/retry_view/exception/InvalidTokenException.java @@ -0,0 +1,18 @@ +package com.pjw.retry_view.exception; + +import org.springframework.http.HttpStatus; + +public class InvalidTokenException extends BusinessException{ + @Override + public HttpStatus getHttpStatus() { + return HttpStatus.UNAUTHORIZED; + } + + public InvalidTokenException(){} + public InvalidTokenException(String msg){ + super(msg); + } + public InvalidTokenException(Exception e){ + super(e); + } +} diff --git a/src/main/java/com/pjw/retry_view/filter/JWTVerifyFilter.java b/src/main/java/com/pjw/retry_view/filter/JWTVerifyFilter.java index fadace7..fab28c0 100644 --- a/src/main/java/com/pjw/retry_view/filter/JWTVerifyFilter.java +++ b/src/main/java/com/pjw/retry_view/filter/JWTVerifyFilter.java @@ -1,6 +1,7 @@ package com.pjw.retry_view.filter; import com.pjw.retry_view.service.JWTService; +import com.pjw.retry_view.util.JWTUtil; import jakarta.servlet.*; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -12,10 +13,6 @@ @Component public class JWTVerifyFilter implements Filter { - private final JWTService jwtService; - public JWTVerifyFilter(JWTService jwtService){ - this.jwtService = jwtService; - } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { @@ -24,11 +21,12 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo String jwt = httpServletRequest.getHeader("Authorization"); System.out.println("Method: "+method+", JWT: "+jwt); - if(isAllowMethod(method) && jwtService.validateToken(jwt)){ - String loginId = jwtService.getClaims(jwt).get("loginId").toString(); + if(isAllowMethod(method) && JWTUtil.isValidateToken(jwt)){ + String loginId = JWTUtil.getClaims(jwt).get("loginId").toString(); servletRequest.setAttribute("loginId", loginId); filterChain.doFilter(servletRequest,servletResponse); }else{ + // accessToken 재발급받게 하기 HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse; httpServletResponse.sendError(HttpStatus.UNAUTHORIZED.value(), "권한이 없습니다."); } diff --git a/src/main/java/com/pjw/retry_view/repository/UserRepository.java b/src/main/java/com/pjw/retry_view/repository/UserRepository.java index 202498b..08c0cc1 100644 --- a/src/main/java/com/pjw/retry_view/repository/UserRepository.java +++ b/src/main/java/com/pjw/retry_view/repository/UserRepository.java @@ -1,7 +1,6 @@ package com.pjw.retry_view.repository; import com.pjw.retry_view.entity.User; -import com.pjw.retry_view.request.LoginRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -13,5 +12,6 @@ public interface UserRepository extends JpaRepository { public List findAll(); public Optional findByLoginId(String loginId); public Optional findByLoginIdAndPassword(String loginId, String password); + public Optional findByRefreshToken(String refreshToken); public User save(User user); } diff --git a/src/main/java/com/pjw/retry_view/response/JWToken.java b/src/main/java/com/pjw/retry_view/response/JWToken.java new file mode 100644 index 0000000..6db0050 --- /dev/null +++ b/src/main/java/com/pjw/retry_view/response/JWToken.java @@ -0,0 +1,21 @@ +package com.pjw.retry_view.response; + +import io.micrometer.common.util.StringUtils; +import lombok.Getter; +import lombok.Setter; + +@Getter +public class JWToken { + private String accessToken; + private String refreshToken; + + private JWToken(){} + private JWToken(String accessToken, String refreshToken){ + this.accessToken = accessToken; + this.refreshToken = refreshToken; + } + + public static JWToken getJWT(String accessToken, String refreshToken){ + return new JWToken(accessToken, refreshToken); + } +} diff --git a/src/main/java/com/pjw/retry_view/response/LoginResponse.java b/src/main/java/com/pjw/retry_view/response/LoginResponse.java index 2d5d6b8..e8b39e0 100644 --- a/src/main/java/com/pjw/retry_view/response/LoginResponse.java +++ b/src/main/java/com/pjw/retry_view/response/LoginResponse.java @@ -6,6 +6,6 @@ @Getter @Setter public class LoginResponse { - private String accessToken; - private String refreshToken; + private String msg; + private JWToken token; } diff --git a/src/main/java/com/pjw/retry_view/service/JWTService.java b/src/main/java/com/pjw/retry_view/service/JWTService.java index 8e5c965..6b1b225 100644 --- a/src/main/java/com/pjw/retry_view/service/JWTService.java +++ b/src/main/java/com/pjw/retry_view/service/JWTService.java @@ -1,82 +1,45 @@ package com.pjw.retry_view.service; -import io.jsonwebtoken.*; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.security.Keys; -import io.jsonwebtoken.security.SecurityException; -import org.springframework.beans.factory.annotation.Value; +import com.pjw.retry_view.response.JWToken; +import com.pjw.retry_view.dto.UserDTO; +import com.pjw.retry_view.dto.UserInfo; +import com.pjw.retry_view.entity.User; +import com.pjw.retry_view.exception.InvalidTokenException; +import com.pjw.retry_view.exception.UserNotFoundException; +import com.pjw.retry_view.repository.UserRepository; +import com.pjw.retry_view.util.JWTUtil; +import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; -import javax.crypto.SecretKey; -import java.time.Instant; -import java.util.Date; -import java.util.Map; - @Service public class JWTService { + private final UserRepository userRepository; - private final SecretKey secretKey; - //private static final String AUTH_KEY = "auth"; - private static final String BEARER_TYPE = "Bearer"; - private static final long ACCESS_TOKEN_EXPIRED = 1000 * 60 * 60 * 24; // 1day - private static final long REFRESH_TOKEN_EXPIRED = 1000 * 60 * 60 * 24 * 7; // 7day - - public JWTService(@Value("${jwt.key}")String key){ - this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(key)); - } - - public String createAccessToken(Map claims){ - long currentTimeMillis = System.currentTimeMillis(); - return Jwts.builder() - .claims(claims) - .issuer("issuer") - .issuedAt(new Date(currentTimeMillis)) - .expiration(new Date(currentTimeMillis+ACCESS_TOKEN_EXPIRED)) - .signWith(secretKey, Jwts.SIG.HS512) - .compact(); + public JWTService(UserRepository userRepository){ + this.userRepository = userRepository; } - public String createRefreshToken(){ - long currentTimeMillis = System.currentTimeMillis(); - return Jwts.builder() - .issuedAt(new Date(currentTimeMillis)) - .expiration(new Date(currentTimeMillis+REFRESH_TOKEN_EXPIRED)) - .signWith(secretKey, Jwts.SIG.HS512) - .compact(); - } - public Claims getClaims(String token){ - token = tokenSplit(token); - return Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .getPayload(); - } - public boolean validateToken(String token){ - if(token == null) return false; - token = tokenSplit(token); - try { - Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); - return true; - } catch (SecurityException | MalformedJwtException e) { - System.out.println("잘못된 JWT 서명입니다."); - } catch (ExpiredJwtException e) { - System.out.println("만료된 JWT 토큰입니다."); - } catch (UnsupportedJwtException e) { - System.out.println("지원되지 않는 JWT 토큰입니다."); - } catch (IllegalArgumentException e) { - System.out.println("JWT 토큰이 잘못되었습니다."); + @Transactional + public JWToken renewAccessToken(String refreshToken) throws InvalidTokenException { + if (JWTUtil.isValidateToken(refreshToken)) { + throw new InvalidTokenException(); } - return false; - } - private String tokenSplit(String token){ - if(token.startsWith(BEARER_TYPE)){ - return token.split(" ")[1]; - } else{ - return token; + UserDTO user = userRepository.findByRefreshToken(refreshToken).map(User::toDTO).orElseThrow(UserNotFoundException::new); + UserInfo userInfo = new UserInfo(user.getName(), user.getLoginId()); + + boolean isExpired = JWTUtil.isTokenExpired(refreshToken); + if (isExpired) { + refreshToken = JWTUtil.createRefreshToken(); + user.setRefreshToken(refreshToken); + userRepository.save(user.toEntity()); + return JWToken.getJWT(JWTUtil.createAccessToken(userInfo), refreshToken); } + + return JWToken.getJWT(JWTUtil.createAccessToken(userInfo), null); } + + } diff --git a/src/main/java/com/pjw/retry_view/util/JWTUtil.java b/src/main/java/com/pjw/retry_view/util/JWTUtil.java new file mode 100644 index 0000000..a04a7c4 --- /dev/null +++ b/src/main/java/com/pjw/retry_view/util/JWTUtil.java @@ -0,0 +1,101 @@ +package com.pjw.retry_view.util; + +import com.pjw.retry_view.dto.UserInfo; +import com.pjw.retry_view.exception.InvalidTokenException; +import com.pjw.retry_view.repository.UserRepository; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SecurityException; +import io.micrometer.common.util.StringUtils; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.*; + +@Component +public class JWTUtil { + private static SecretKey secretKey; + private static final long ACCESS_TOKEN_EXPIRED = 1000 * 60 * 60; // 1h + private static final long REFRESH_TOKEN_EXPIRED = 1000 * 60 * 60 * 24 * 7; + private static final String USER_INFO_NAME = "name"; + private static final String USER_INFO_LOGIN_ID = "loginId"; + //private static final String AUTH_KEY = "Authorization"; + private static final String BEARER_TYPE = "Bearer";// 7day + + public JWTUtil(@Value("${jwt.key}")String key){ + secretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(key)); + } + + public SecretKey getSecretKey(){ + return secretKey; + } + + public static String createAccessToken(UserInfo userInfo){ + long currentTimeMillis = System.currentTimeMillis(); + Map claims = new HashMap<>(); + claims.put(USER_INFO_NAME, userInfo.getName()); + claims.put(USER_INFO_LOGIN_ID, userInfo.getLoginId()); + return Jwts.builder() + .claims(claims) + .issuer("issuer") + .issuedAt(new Date(currentTimeMillis)) + .expiration(new Date(currentTimeMillis+ACCESS_TOKEN_EXPIRED)) + .signWith(secretKey, Jwts.SIG.HS512) + .compact(); + } + + public static String createRefreshToken(){ + long currentTimeMillis = System.currentTimeMillis(); + return Jwts.builder() + .issuedAt(new Date(currentTimeMillis)) + .expiration(new Date(currentTimeMillis+REFRESH_TOKEN_EXPIRED)) + .signWith(secretKey, Jwts.SIG.HS512) + .compact(); + } + + public static Claims getClaims(String token)throws InvalidTokenException { + if(StringUtils.isBlank(token)) throw new InvalidTokenException(); + token = splitToken(token); + return Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public static boolean isValidateToken(String token){ + if(StringUtils.isBlank(token)) return false; + token = splitToken(token); + try { + Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token); + return true; + } catch (SecurityException | MalformedJwtException e) { + System.out.println("잘못된 JWT 서명입니다."); + } catch (ExpiredJwtException e) { + System.out.println("만료된 JWT 토큰입니다."); + } catch (UnsupportedJwtException e) { + System.out.println("지원되지 않는 JWT 토큰입니다."); + } catch (IllegalArgumentException e) { + System.out.println("JWT 토큰이 잘못되었습니다."); + } + return false; + } + + public static boolean isTokenExpired(String token){ + Claims jwtClaims = getClaims(token); + return jwtClaims.getExpiration().before(new Date()); + } + + public static String splitToken(String token){ + if(StringUtils.isBlank(token)) return token; + + String result = ""; + if(token.startsWith(BEARER_TYPE)){ + result = token.split(" ")[1]; + } + return result; + } +}