diff --git a/src/main/java/MentosServer/mentos/config/BaseResponseStatus.java b/src/main/java/MentosServer/mentos/config/BaseResponseStatus.java index 0105a0b..334556c 100644 --- a/src/main/java/MentosServer/mentos/config/BaseResponseStatus.java +++ b/src/main/java/MentosServer/mentos/config/BaseResponseStatus.java @@ -86,6 +86,10 @@ public enum BaseResponseStatus { EMPTY_POST_TITLE(false,2411,"게시글 제목을 입력해주세요"), EMPTY_POST_CONTENTS(false,2412,"게시글 내용을 입력해주세요"), + // jwt관련 Error + EXPIRED_JWT(false,2414,"만료된 JWT입니다."), + EMPTY_REFRESH_JWT(false,2415,"리프레시 토큰이 존재하지 않습니다."), + INVALID_REFRESH_TOKEN(false,2416,"유효하지 않은 리프레시 토큰입니다."), /** * 3000 : Response 오류 */ @@ -106,6 +110,9 @@ public enum BaseResponseStatus { INVALID_POST_MEMBER(false,3404,"게시글 작성자가 아닙니다"), //[PATCH]/board PATCH_POST_SUCCESS(true,3405,"게시글 수정을 완료했습니다."), + //[GET]/jwt + SUCCESS_LOGOUT(true,3406,"로그아웃에 성공했습니다."), + FAILED_TO_LOGOUT(false,3407,"로그아웃에 실패했습니다"), /** * ROZY diff --git a/src/main/java/MentosServer/mentos/controller/JwtController.java b/src/main/java/MentosServer/mentos/controller/JwtController.java new file mode 100644 index 0000000..4a951b3 --- /dev/null +++ b/src/main/java/MentosServer/mentos/controller/JwtController.java @@ -0,0 +1,43 @@ +package MentosServer.mentos.controller; + +import MentosServer.mentos.config.BaseException; +import MentosServer.mentos.config.BaseResponse; +import MentosServer.mentos.model.dto.TokenRes; +import MentosServer.mentos.utils.JwtService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import static MentosServer.mentos.config.BaseResponseStatus.FAILED_TO_LOGOUT; +import static MentosServer.mentos.config.BaseResponseStatus.SUCCESS_LOGOUT; + +@RestController +public class JwtController { + private final JwtService jwtService; + + public JwtController(JwtService jwtService) { + this.jwtService = jwtService; + } + + /* + 새로운 액세스 토큰 혹은 리프레시 토큰이 필요한 경우 + */ + @PostMapping("/accessToken") + public BaseResponse createRefreshToken() throws BaseException { + return new BaseResponse<>(jwtService.checkRefreshJwt()); + } + /* + 로그아웃 + */ + @GetMapping("/log-out") + public BaseResponse logOut() throws BaseException { + //리프레시 토큰 버리고 + try { + jwtService.logOut(); + //반환 + return new BaseResponse<>(SUCCESS_LOGOUT); + }catch(Exception e){ + return new BaseResponse<>(FAILED_TO_LOGOUT); + } + } +} diff --git a/src/main/java/MentosServer/mentos/model/domain/Member.java b/src/main/java/MentosServer/mentos/model/domain/Member.java index 73231f5..977d8e3 100644 --- a/src/main/java/MentosServer/mentos/model/domain/Member.java +++ b/src/main/java/MentosServer/mentos/model/domain/Member.java @@ -18,9 +18,8 @@ public class Member { private String memberEmail; private String memberPw; private int memberSchoolId; - private int memberMajorId; + private String memberMajor; private String memberSex; - private String memberImage; private int memberMentos; private String memberStatus; private Timestamp memberCreateAt; diff --git a/src/main/java/MentosServer/mentos/model/dto/PostLoginRes.java b/src/main/java/MentosServer/mentos/model/dto/PostLoginRes.java index 5e67d5e..8b60574 100644 --- a/src/main/java/MentosServer/mentos/model/dto/PostLoginRes.java +++ b/src/main/java/MentosServer/mentos/model/dto/PostLoginRes.java @@ -11,4 +11,5 @@ public class PostLoginRes { private int memberId; private String jwt; + private String refreshJwt; } diff --git a/src/main/java/MentosServer/mentos/model/dto/SignUpRes.java b/src/main/java/MentosServer/mentos/model/dto/SignUpRes.java index d94c8f8..c0031c3 100644 --- a/src/main/java/MentosServer/mentos/model/dto/SignUpRes.java +++ b/src/main/java/MentosServer/mentos/model/dto/SignUpRes.java @@ -8,5 +8,6 @@ public class SignUpRes { private int memberId; private String memberJwt; + private String refreshJwt; } diff --git a/src/main/java/MentosServer/mentos/model/dto/TokenRes.java b/src/main/java/MentosServer/mentos/model/dto/TokenRes.java new file mode 100644 index 0000000..ba821d0 --- /dev/null +++ b/src/main/java/MentosServer/mentos/model/dto/TokenRes.java @@ -0,0 +1,9 @@ +package MentosServer.mentos.model.dto; + +import lombok.Data; + +@Data +public class TokenRes { + private final String memberJwt; + private final String refreshJwt; +} diff --git a/src/main/java/MentosServer/mentos/repository/JwtRepository.java b/src/main/java/MentosServer/mentos/repository/JwtRepository.java new file mode 100644 index 0000000..034fecd --- /dev/null +++ b/src/main/java/MentosServer/mentos/repository/JwtRepository.java @@ -0,0 +1,38 @@ +package MentosServer.mentos.repository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; + +@Repository +public class JwtRepository { + private JdbcTemplate jdbcTemplate; + + @Autowired + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void createRefreshToken(int memberId,String refreshToken){ + String insertQuery = "insert into JWT (memberId, refreshToken) VALUES (?,?)"; + Object[] params = new Object[]{memberId,refreshToken}; + this.jdbcTemplate.update(insertQuery,params); + } + public void updateRefreshToken(int memberId, String refreshToken){ + String updateQuery = "update JWT set refreshToken = ?,updateAt = CURRENT_TIMESTAMP() where memberId=?"; + Object[] params = new Object[]{refreshToken,memberId}; + this.jdbcTemplate.update(updateQuery,params); + } + //로그아웃 + public void deleteRefreshToken(int memberId){ + String deleteQuery = "update JWT set refreshToken= null,updateAt = CURRENT_TIMESTAMP()where memberId=?"; + this.jdbcTemplate.update(deleteQuery,memberId); + } + //멤버Id 얻어오기 + public int getMemberId(String refreshToken){ + String getQuery = "select memberId from JWT where refreshToken=?"; + return this.jdbcTemplate.queryForObject(getQuery,int.class,refreshToken); + } +} diff --git a/src/main/java/MentosServer/mentos/service/LoginService.java b/src/main/java/MentosServer/mentos/service/LoginService.java index 040cf94..f5b074b 100644 --- a/src/main/java/MentosServer/mentos/service/LoginService.java +++ b/src/main/java/MentosServer/mentos/service/LoginService.java @@ -51,7 +51,8 @@ public PostLoginRes logIn(PostLoginReq postLoginReq) throws BaseException { int memberId = loginRepository.getPwd(postLoginReq).getMemberId(); String jwt = jwtService.createJwt(memberId); - return new PostLoginRes(memberId,jwt); + String refreshToken = jwtService.updateRefreshToken(memberId); //리프레시 토큰 교체 + return new PostLoginRes(memberId,jwt,refreshToken); } else { // 비밀번호가 다르다면 에러메세지를 출력한다. diff --git a/src/main/java/MentosServer/mentos/service/SignUpService.java b/src/main/java/MentosServer/mentos/service/SignUpService.java index 4027a71..db55f27 100644 --- a/src/main/java/MentosServer/mentos/service/SignUpService.java +++ b/src/main/java/MentosServer/mentos/service/SignUpService.java @@ -39,7 +39,6 @@ public SignUpRes createMember(SignUpReq signUpReq) throws BaseException { try { // 암호화: signUpReq에서 제공받은 비밀번호를 보안을 위해 암호화시켜 DB에 저장합니다. // ex) password123 -> dfhsjfkjdsnj4@!$!@chdsnjfwkenjfnsjfnjsd.fdsfaifsadjfjaf - logger.info("암호화 로직"); pwd = new AES128(Secret.USER_INFO_PASSWORD_KEY).encrypt(signUpReq.getMemberPw()); // 암호화코드 signUpReq.setMemberPw(pwd); } catch (Exception ignored) { // 암호화가 실패하였을 경우 에러 발생 @@ -48,9 +47,9 @@ public SignUpRes createMember(SignUpReq signUpReq) throws BaseException { //실제 비즈니스 로직 try { int memberId = signUpRepository.createMember(signUpReq); - logger.info("jwt발급"); String memberJwt = jwtService.createJwt(memberId); - return new SignUpRes(memberId,memberJwt); + String refreshToken = jwtService.createRefreshToken(memberId);//리프레시 토큰 생성 + return new SignUpRes(memberId,memberJwt,refreshToken); } catch (Exception exception) { // DB에 이상이 있는 경우 에러 메시지를 보냅니다. throw new BaseException(DATABASE_ERROR); } diff --git a/src/main/java/MentosServer/mentos/utils/JwtService.java b/src/main/java/MentosServer/mentos/utils/JwtService.java index 31f1260..b997aeb 100644 --- a/src/main/java/MentosServer/mentos/utils/JwtService.java +++ b/src/main/java/MentosServer/mentos/utils/JwtService.java @@ -2,22 +2,26 @@ import MentosServer.mentos.config.BaseException; import MentosServer.mentos.config.secret.Secret; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import MentosServer.mentos.model.dto.TokenRes; +import MentosServer.mentos.repository.JwtRepository; +import io.jsonwebtoken.*; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import java.util.Date; -import static MentosServer.mentos.config.BaseResponseStatus.EMPTY_JWT; -import static MentosServer.mentos.config.BaseResponseStatus.INVALID_JWT; +import static MentosServer.mentos.config.BaseResponseStatus.*; @Service public class JwtService { + private final JwtRepository jwtRepository; + + public JwtService(JwtRepository jwtRepository) { + this.jwtRepository = jwtRepository; + } /* JWT 생성 @@ -30,10 +34,23 @@ public String createJwt(int memberId){ .setHeaderParam("type","jwt") .claim("memberId",memberId) .setIssuedAt(now) - .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*365))) + .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24))) //액세스 토큰 만료 시간 하루로 설정 1*(1000*60*60*24) 테스트 1분 .signWith(SignatureAlgorithm.HS256, Secret.JWT_SECRET_KEY) .compact(); } + /* + jwt refreshToken 생성 + */ + public String createRefreshToken(int memberId){ + Date now = new Date(); + String refreshToken = Jwts.builder() + .setIssuedAt(now) + .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*7))) //리프레시 토큰 만료 시간 일주일로 설정 + .signWith(SignatureAlgorithm.HS256,Secret.JWT_SECRET_KEY) + .compact(); + jwtRepository.createRefreshToken(memberId,refreshToken); + return refreshToken; + } /* Header에서 X-ACCESS-TOKEN 으로 JWT 추출 @@ -43,6 +60,61 @@ public String getJwt(){ HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); return request.getHeader("X-ACCESS-TOKEN"); } + /* + Header에서 X-REFRESH-TOKEN으로 REFRESH JWT 추출 + */ + public String getRefreshJwt(){ + HttpServletRequest request = ((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest(); + return request.getHeader("X-REFRESH-TOKEN"); + } + + /* + 액세스 토큰이 만료가 되면-> 프론트에서 리프레시 토큰을 보낸다. + 그 리프레시 토큰을 보내면-> 이전 리프레시 토큰과 새로 만든 액세스 토큰을 준다. + + 리프레시 토큰도 만료가 됐다면 -> 새로운 리프레시 토큰과 액세스 토큰을 준다. + */ + @Transactional + public TokenRes checkRefreshJwt() throws BaseException { + + // Refresh JWT 추출 + int memberId; + String refreshToken = getRefreshJwt(); + if(refreshToken==null || refreshToken.length()==0){ + throw new BaseException(EMPTY_REFRESH_JWT); + } + //memberId 가져오기 + try { + memberId = jwtRepository.getMemberId(refreshToken); //memberId 얻어오기 + }catch(Exception e){ + throw new BaseException(INVALID_REFRESH_TOKEN); + } + + Jws claims; + try{ + claims = Jwts.parser() + .setSigningKey(Secret.JWT_SECRET_KEY) + .parseClaimsJws(refreshToken); + }catch(ExpiredJwtException ignored){ //리프레시 토큰이 만료가 되었으면 새로운 리프레시 토큰과 액세스 토큰을 전달해줌 + String memberJwt = createJwt(memberId); //액세스 토큰을 만든다. + return new TokenRes(memberJwt,updateRefreshToken(memberId)); + }catch(Exception e){ + throw new BaseException(INVALID_REFRESH_TOKEN); + } + + return new TokenRes(createJwt(memberId),refreshToken); //액세스 토큰만 만료가 되었을 때 리턴 + } + + public String updateRefreshToken(int memberId) { + Date now = new Date(); + String refreshToken = Jwts.builder() + .setIssuedAt(now) + .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*7))) //리프레시 토큰 만료 시간 일주일로 설정 + .signWith(SignatureAlgorithm.HS256,Secret.JWT_SECRET_KEY) + .compact(); + jwtRepository.updateRefreshToken(memberId,refreshToken); + return refreshToken; + } /* JWT에서 userIdx 추출 @@ -62,7 +134,9 @@ public int getMemberId() throws BaseException{ claims = Jwts.parser() .setSigningKey(Secret.JWT_SECRET_KEY) .parseClaimsJws(accessToken); - } catch (Exception ignored) { + } catch (ExpiredJwtException ignored) { //액세스 토큰이 만료된 경우 + throw new BaseException(EXPIRED_JWT); + }catch(Exception e){ throw new BaseException(INVALID_JWT); } @@ -70,4 +144,16 @@ public int getMemberId() throws BaseException{ return claims.getBody().get("memberId",Integer.class); // jwt 에서 userIdx를 추출합니다. } + //로그아웃 - 리프레시 토큰 버리기 + public void logOut() throws BaseException { + int memberId = getMemberId(); + try { + jwtRepository.deleteRefreshToken(memberId); + } catch(Exception e){ + if(e instanceof BaseException){ + throw e; + } + throw new BaseException(DATABASE_ERROR); + } + } }