박민혁 박민혁 07-10
250710 박민혁 토큰 발급 및 로그인 로직
@f78f8430b92e39d70ae42ececbf1683fce18bd93
src/main/java/kr/co/takensoft/ai/system/auth/dao/AuthDAO.java
--- src/main/java/kr/co/takensoft/ai/system/auth/dao/AuthDAO.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/dao/AuthDAO.java
@@ -24,4 +24,12 @@
      * 사용자 회원 가입
      */
     int memberRegister(MemberVO memberVO) throws Exception;
+
+    /**
+     * @param memberId 사용자 아이디
+     * @return 사용자 정보
+     *
+     * 사용자 정보 개인 정보 조회
+     */
+    MemberVO findMemberInfo(String memberId) throws Exception;
 }
 
src/main/java/kr/co/takensoft/ai/system/auth/dao/RefreshDAO.java (added)
+++ src/main/java/kr/co/takensoft/ai/system/auth/dao/RefreshDAO.java
@@ -0,0 +1,35 @@
+package kr.co.takensoft.ai.system.auth.dao;
+
+import kr.co.takensoft.ai.system.auth.dto.RefreshTokenDTO;
+import org.apache.ibatis.annotations.Param;
+
+/*
+ * @author : 박민혁
+ * @since  : 2025.04.18
+ * @modification
+ *      since   |    author    | description
+ *  2025.04.18  |     박민혁    | 최초 등록
+ *
+ * 리프레시 토큰 관련 DAO
+ */
+public interface RefreshDAO {
+    /**
+     * @param refreshTokenDTO 리프레시 토큰과 관련된 정보
+     *
+     * 리프레시 토큰 저장 혹은 갱신
+     */
+    void saveRefreshToken(RefreshTokenDTO refreshTokenDTO);
+
+    /**
+     * @param memberId 사용자 아이디
+     * @return 리프레시 토큰 정보
+     * 유저 아이디로 리프레시 토큰을 받아오기
+     */
+    String getRefreshTokenByUserId(String memberId);
+
+    /**
+     * @param memberId 사용자 아이디
+     * 리프레시 토큰 삭제
+     */
+    void deleteRefreshToken(String memberId);
+}
 
src/main/java/kr/co/takensoft/ai/system/auth/dto/RefreshTokenDTO.java (added)
+++ src/main/java/kr/co/takensoft/ai/system/auth/dto/RefreshTokenDTO.java
@@ -0,0 +1,29 @@
+package kr.co.takensoft.ai.system.auth.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author  : 박민혁
+ * @since   : 2025.07.09
+ * @modification
+ *     since    |    author    | description
+ *  2025.07.09  |  박민혁       | 최초 등록
+ *
+ * 리프레시 액세스 토큰 관련 DTO
+ */
+@Setter
+@Getter
+public class RefreshTokenDTO {
+    @NotNull
+    private String memberId; // 사용자 아이디
+    @NotNull
+    private String refreshToken; // 리프레시 토큰 내용
+
+    public RefreshTokenDTO(String memberId, String refreshToken) {
+        this.memberId = memberId;
+        this.refreshToken = refreshToken;
+    }
+
+}
 
src/main/java/kr/co/takensoft/ai/system/auth/dto/TokenDTO.java (added)
+++ src/main/java/kr/co/takensoft/ai/system/auth/dto/TokenDTO.java
@@ -0,0 +1,31 @@
+package kr.co.takensoft.ai.system.auth.dto;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * @author  : 박민혁
+ * @since   : 2025.07.09
+ * @modification
+ *     since    |    author    | description
+ *  2025.07.09  |  박민혁       | 최초 등록
+ *
+ * 액세스 토큰 관련 DTO
+ */
+@Setter
+@Getter
+public class TokenDTO {
+    @NotNull
+    private String loginId; // 로그인 아이디
+    @NotNull
+    private String memberId; // 사용자 아이디
+    @NotNull
+    private String memberName; // 사용자 이름
+
+    public TokenDTO(String loginId, String memberId, String memberName) {
+        this.loginId = loginId;
+        this.memberId = memberId;
+        this.memberName = memberName;
+    }
+}
src/main/java/kr/co/takensoft/ai/system/auth/service/AuthService.java
--- src/main/java/kr/co/takensoft/ai/system/auth/service/AuthService.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/AuthService.java
@@ -1,6 +1,9 @@
 package kr.co.takensoft.ai.system.auth.service;
 
+import kr.co.takensoft.ai.system.auth.dto.LoginDTO;
 import kr.co.takensoft.ai.system.auth.vo.MemberVO;
+
+import java.util.Map;
 
 /*
  * @author : 박민혁
@@ -13,11 +16,27 @@
  */
 public interface AuthService {
     /**
-     * @param member 사용자 정보
+     * @param memberVO 사용자 정보
      * @return 등록 성공 여부
      *
      * 사용자 회원가입
      */
     int memberRegister(MemberVO memberVO);
 
+    /**
+     * @param LoginDTO 로그인 정보
+     * @return 액세스 토큰과 리프레시 토큰
+     *
+     * 사용자 회원가입
+     */
+    Map<String, String> memberLogin(LoginDTO loginDTO) throws Exception;
+
+    /**
+     * @param memberId 사용자 아이디
+     * @return 사용자 정보
+     *
+     * 사용자 정보 개인 정보 조회
+     */
+    MemberVO findMemberInfo(String memberId);
+
 }
 
src/main/java/kr/co/takensoft/ai/system/auth/service/RefreshService.java (added)
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/RefreshService.java
@@ -0,0 +1,23 @@
+package kr.co.takensoft.ai.system.auth.service;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+/*
+ * @author : 박민혁
+ * @since  : 2025.07.09
+ * @modification
+ *      since   |    author    | description
+ *  2025.07.09  |     박민혁    | 최초 등록
+ *
+ * 리프레시 토큰 관련 서비스
+ */
+public interface RefreshService {
+    /**
+     * @param req, res  클라이언트 요청과 서버 응답
+     * @return 액세스 토큰 정보
+     *
+     * 토큰 재발급
+     */
+    String tokenReissueProc(HttpServletRequest req, HttpServletResponse res) throws Exception;
+}
src/main/java/kr/co/takensoft/ai/system/auth/service/impl/AuthServiceImpl.java
--- src/main/java/kr/co/takensoft/ai/system/auth/service/impl/AuthServiceImpl.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/impl/AuthServiceImpl.java
@@ -10,14 +10,22 @@
  * 사용자 정보 관련 서비스
  */
 import kr.co.takensoft.ai.system.auth.dao.AuthDAO;
+import kr.co.takensoft.ai.system.auth.dao.RefreshDAO;
+import kr.co.takensoft.ai.system.auth.dto.LoginDTO;
+import kr.co.takensoft.ai.system.auth.dto.RefreshTokenDTO;
+import kr.co.takensoft.ai.system.auth.dto.TokenDTO;
 import kr.co.takensoft.ai.system.auth.service.AuthService;
 import kr.co.takensoft.ai.system.auth.vo.MemberVO;
 import kr.co.takensoft.ai.system.common.idgen.service.IdgenService;
+import kr.co.takensoft.ai.system.common.util.JwtUtil;
 import kr.co.takensoft.ai.system.common.util.Secret;
 import lombok.RequiredArgsConstructor;
 import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
 
 @Service
 @RequiredArgsConstructor
@@ -26,10 +34,12 @@
     private final AuthDAO authDAO;
     private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
     private final IdgenService memberIdgn;
+    private final JwtUtil jwtUtil;
+    private final RefreshDAO refreshDAO;
 
     /**
      * @param member 사용자 정보
-     * @return 등록 성공 여부
+     * @return int 등록 성공 여부
      *
      * 사용자 회원가입
      */
@@ -45,6 +55,55 @@
             e.printStackTrace();
             return -1;
         }
-    };
+    }
+
+
+    /**
+     * @param LoginDTO 로그인 정보
+     * @return Map<String, String> 액세스 토큰과 리프레시 토큰
+     *
+     * 사용자 회원가입
+     */
+    public Map<String, String> memberLogin(LoginDTO loginDTO) throws Exception{
+        MemberVO member = authDAO.findMemberInfo(loginDTO.getLoginId());
+        if (member == null) {
+            throw new Exception("잘못된 아이디입니다"); // 아이디가 없을 경우
+        }
+        if(!bCryptPasswordEncoder.matches(loginDTO.getPassword(), member.getPassword())) {
+            throw new Exception("잘못된 비밀번호입니다"); // 비밀번호가 틀릴 경우
+        }
+
+        TokenDTO tokenDTO = new TokenDTO(member.getLoginId(), member.getMemberId(), member.getMemberName()); // 멤버 정보로 액세스 토큰 DTO 생성
+
+        String accessToken = jwtUtil.createAccessToken(tokenDTO); // 액세스 토큰 생성
+        String refreshToken = jwtUtil.createRefreshToken(member.getMemberId()); // 리프레시 토큰 생성
+
+        RefreshTokenDTO refreshTokenDTO = new RefreshTokenDTO(member.getMemberId(), refreshToken); // 리프레시 토큰 DTO 생성
+
+        refreshDAO.saveRefreshToken(refreshTokenDTO); // 리프레시 토큰 저장
+
+        Map<String, String> tokens = new HashMap<>();
+        tokens.put("accessToken", accessToken);
+        tokens.put("refreshToken", refreshToken);
+        return tokens;
+    }
+
+    /**
+     * @param memberId 사용자 아이디
+     * @return MemberVO 사용자 정보
+     *
+     * 사용자 정보 개인 정보 조회
+     */
+    public MemberVO findMemberInfo(String memberId){
+        try {
+            MemberVO member = authDAO.findMemberInfo(memberId);
+            member.setEmail(Secret.decrypt(member.getEmail())); // 이메일 복호화
+            member.setPhoneNumber(Secret.decrypt(member.getPhoneNumber())); // 전화번호 복호화
+            return member;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
 
 }
 
src/main/java/kr/co/takensoft/ai/system/auth/service/impl/RefreshServiceImpl.java (added)
+++ src/main/java/kr/co/takensoft/ai/system/auth/service/impl/RefreshServiceImpl.java
@@ -0,0 +1,87 @@
+package kr.co.takensoft.ai.system.auth.service.impl;
+
+import io.jsonwebtoken.ExpiredJwtException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import kr.co.takensoft.ai.system.auth.dao.AuthDAO;
+import kr.co.takensoft.ai.system.auth.dao.RefreshDAO;
+import kr.co.takensoft.ai.system.auth.dto.TokenDTO;
+import kr.co.takensoft.ai.system.auth.service.RefreshService;
+import kr.co.takensoft.ai.system.auth.vo.MemberVO;
+import kr.co.takensoft.ai.system.common.util.JwtUtil;
+import lombok.RequiredArgsConstructor;
+import org.egovframe.rte.fdl.cmmn.EgovAbstractServiceImpl;
+import org.springframework.stereotype.Service;
+
+/*
+ * @author : 박민혁
+ * @since  : 2025.07.09
+ * @modification
+ *      since   |    author    | description
+ *  2025.07.09  |     박민혁    | 최초 등록
+ *
+ * 리프레시 토큰 관련 서비스
+ */
+@Service
+@RequiredArgsConstructor
+public class RefreshServiceImpl extends EgovAbstractServiceImpl implements RefreshService {
+    /**
+     * @param req, res  클라이언트 요청과 서버 응답
+     * @return 액세스 토큰 정보
+     *
+     * 토큰 재발급
+     */
+    private final JwtUtil jwtUtil;
+    private final AuthDAO authDAO;
+    private final RefreshDAO refreshDAO;
+
+    public String tokenReissueProc(HttpServletRequest req, HttpServletResponse res) throws Exception {
+        String refreshToken = req.getHeader("RefreshToken");
+        if (refreshToken == null) {
+            throw new Exception("Refresh token이 전달되지 않았습니다.");
+        }
+        String memberId;
+        try {
+            // memberId 먼저 추출: 유효기간 만료되어도 대부분 파싱 가능
+            memberId = jwtUtil.getUsid(refreshToken);
+        } catch (Exception e) {
+            throw new Exception("Refresh token에서 사용자 정보를 파싱할 수 없습니다.");
+        }
+
+        // 리프레시 토큰 만료 여부 확인
+        try {
+            // 만료 여부 체크
+            if (jwtUtil.isExpired(refreshToken)) {
+                refreshDAO.deleteRefreshToken(memberId);
+                throw new Exception("Refresh token이 만료되었습니다.");
+            }
+        } catch (ExpiredJwtException e) {
+            // 여기서도 따로 만료된 경우 캐치
+            refreshDAO.deleteRefreshToken(memberId);
+            throw new Exception("Refresh token이 만료되었습니다.");
+        }
+
+        // 리프레시 토큰이 DB에 존재하는지 확인
+        if (refreshDAO.getRefreshTokenByUserId(memberId) == null) {
+            throw new Exception("리프레시 토큰이 존재하지 않습니다.");
+        }
+
+        // 사용자 정보 조회
+        MemberVO member = authDAO.findMemberInfo(memberId);
+        if (member == null) {
+            refreshDAO.deleteRefreshToken(memberId); // 자동 로그아웃
+            throw new Exception("유효한 사용자가 아닙니다.");
+        }
+
+        // 멤버 정보를 가져와 DTO 생성
+        TokenDTO tokenDTO = new TokenDTO(member.getLoginId(), member.getMemberId(), member.getMemberName());
+
+        // 새 액세스 토큰 생성: 이름, 권한 등도 포함하여 재발급
+        String newAccessToken = jwtUtil.createAccessToken(tokenDTO);
+
+        // 응답 헤더에 새 액세스 토큰 설정 (Controller에서 이미 설정할 수도 있음)
+        res.setHeader("Authorization", newAccessToken);
+
+        return newAccessToken;
+    }
+}
src/main/java/kr/co/takensoft/ai/system/auth/web/AuthController.java
--- src/main/java/kr/co/takensoft/ai/system/auth/web/AuthController.java
+++ src/main/java/kr/co/takensoft/ai/system/auth/web/AuthController.java
@@ -1,5 +1,6 @@
 package kr.co.takensoft.ai.system.auth.web;
 
+import kr.co.takensoft.ai.system.auth.dto.LoginDTO;
 import kr.co.takensoft.ai.system.auth.service.AuthService;
 import kr.co.takensoft.ai.system.auth.vo.MemberVO;
 import lombok.RequiredArgsConstructor;
@@ -11,6 +12,7 @@
 import org.springframework.web.bind.annotation.RestController;
 
 import java.util.HashMap;
+import java.util.Map;
 
 /*
  * @author : 박민혁
@@ -41,5 +43,25 @@
         return new ResponseEntity<>(result, HttpStatus.OK);
     }
 
+    /**
+     * @param loginDTO 로그인 정보
+     * @return ResponseEntity 액세스 토큰 / 리프레시 토큰을 포함하는 응답
+     *
+     * 사용자 회원 가입
+     */
+    @PostMapping("/login.json")
+    public ResponseEntity<?> login(@RequestBody LoginDTO loginDTO) throws Exception {
+        HashMap<String, Object> result = new HashMap<>();
+        try {
+            Map<String, String> tokens = authService.memberLogin(loginDTO);
+            result.put("accessToken", tokens.get("accessToken"));
+            result.put("refreshToken", tokens.get("refreshToken"));
+            return new ResponseEntity<>(result, HttpStatus.OK);
+        } catch (Exception e) {
+            result.put("message", e.getMessage());
+            return new ResponseEntity<>(result, HttpStatus.UNAUTHORIZED);
+        }
+    }
+
 
 }
(No newline at end of file)
 
src/main/java/kr/co/takensoft/ai/system/auth/web/RefreshController.java (added)
+++ src/main/java/kr/co/takensoft/ai/system/auth/web/RefreshController.java
@@ -0,0 +1,51 @@
+package kr.co.takensoft.ai.system.auth.web;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import kr.co.takensoft.ai.system.auth.service.RefreshService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/*
+ * @author : 박민혁
+ * @since  : 2025.02.06
+ * @modification
+ *      since   |    author    | description
+ *  2025.02.06  |     박민혁    | 최초 등록
+ *  2025.03.20  |     박민혁    | 매핑 주소 수정
+ *
+ * 리프레시 토큰 재발급 컨트롤러
+ */
+@RestController
+@RequiredArgsConstructor
+@RequestMapping(value = "/api/refresh")
+public class RefreshController {
+    private final RefreshService refreshService;
+
+    @PostMapping("/tokenReissue.json")
+    public ResponseEntity<?> tokenReissue(HttpServletRequest req, HttpServletResponse res) {
+        try {
+            // 서비스에서 새 액세스 토큰 재발급
+            String newAccessToken = refreshService.tokenReissueProc(req, res);
+
+            // JSON 형태로 새 토큰과 관련 정보를 반환
+            Map<String, Object> responseData = new HashMap<>();
+            responseData.put("accessToken", newAccessToken);
+            responseData.put("message", "정상적으로 액세스 토큰이 재발급되었습니다.");
+
+            return new ResponseEntity<>(responseData, HttpStatus.OK);
+        } catch (Exception e) {
+            Map<String, Object> responseData = new HashMap<>();
+            responseData.put("message", "토큰 재발급에 실패하였습니다: " + e.getMessage());
+            return new ResponseEntity<>(responseData, HttpStatus.UNAUTHORIZED);
+        }
+    }
+
+}
src/main/java/kr/co/takensoft/ai/system/common/util/JwtUtil.java
--- src/main/java/kr/co/takensoft/ai/system/common/util/JwtUtil.java
+++ src/main/java/kr/co/takensoft/ai/system/common/util/JwtUtil.java
@@ -2,6 +2,7 @@
 
 import io.jsonwebtoken.ExpiredJwtException;
 import io.jsonwebtoken.Jwts;
+import kr.co.takensoft.ai.system.auth.dto.TokenDTO;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 
@@ -27,11 +28,11 @@
     }
 
     // 액세스 토큰 생성
-    public String createAccessToken(String userId, String userNm, String userAuth) {
+    public String createAccessToken(TokenDTO token) {
         return Jwts.builder()
-                .claim("userId", userId)
-                .claim("userNm", userNm)
-                .claim("userAuth", userAuth)
+                .claim("loginId", token.getLoginId())
+                .claim("memberId", token.getMemberId())
+                .claim("memberName", token.getMemberName())
                 .issuedAt(new Date(System.currentTimeMillis()))
                 .expiration(new Date(System.currentTimeMillis() + accessTime))
                 .signWith(secretKey)
@@ -39,9 +40,9 @@
     }
 
     // 리프레시 토큰 생성
-    public String createRefreshToken(String userId) {
+    public String createRefreshToken(String memberId) {
         return Jwts.builder()
-                .claim("userId", userId)
+                .claim("memberId", memberId)
                 .claim("tokenType", "refresh")
                 .issuedAt(new Date(System.currentTimeMillis()))
                 .expiration(new Date(System.currentTimeMillis() + refreshTime))
@@ -55,10 +56,10 @@
             return Jwts.parser().verifyWith(secretKey).build()
                     .parseSignedClaims(token)
                     .getPayload()
-                    .get("userId", String.class);
+                    .get("memberId", String.class);
         } catch (ExpiredJwtException e) {
             // 만료된 토큰이어도 claims는 존재하므로 꺼낼 수 있음
-            return e.getClaims().get("userId", String.class);
+            return e.getClaims().get("memberId", String.class);
         }
     }
 
@@ -68,21 +69,9 @@
             return Jwts.parser().verifyWith(secretKey).build()
                     .parseSignedClaims(token)
                     .getPayload()
-                    .get("userNm", String.class);
+                    .get("memberName", String.class);
         } catch (ExpiredJwtException e) {
-            return e.getClaims().get("userNm", String.class);
-        }
-    }
-
-    // 유저 권한 반환 메서드 (필요시)
-    public String getUserAuth(String token) {
-        try {
-            return Jwts.parser().verifyWith(secretKey).build()
-                    .parseSignedClaims(token)
-                    .getPayload()
-                    .get("userAuth", String.class);
-        } catch (ExpiredJwtException e) {
-            return e.getClaims().get("userAuth", String.class);
+            return e.getClaims().get("memberName", String.class);
         }
     }
 
src/main/resources/mybatis/mapper/auth/auth-SQL.xml
--- src/main/resources/mybatis/mapper/auth/auth-SQL.xml
+++ src/main/resources/mybatis/mapper/auth/auth-SQL.xml
@@ -33,12 +33,13 @@
     <!--
         작 성 자 : 박민혁
         작 성 일 : 2025.07.08
-        내    용 : 사용자 정보 찾기 (테스트용)
+        내    용 : 사용자 정보 개인 정보 조회
     -->
-    <select id="findMemberId" >
+    <select id="findMemberInfo" >
         select
-            member_id
+            *
         from member
+        where member_id = #{memberId}
     </select>
 
 </mapper>
(No newline at end of file)
 
src/main/resources/mybatis/mapper/auth/refresh-SQL.xml (added)
+++ src/main/resources/mybatis/mapper/auth/refresh-SQL.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="kr.co.takensoft.ai.system.auth.dao.RefreshDAO">
+    <!--
+        작 성 자 : 박민혁
+        작 성 일 : 2025.04.18
+        내    용 : 리프레시 토큰 저장 혹은 갱신
+    -->
+    <insert id="saveRefreshToken">
+        INSERT INTO token_info (
+            member_id,
+            refresh_token,
+            created_at
+        ) VALUES (
+            #{memberId},
+            #{refreshToken},
+            CURRENT_TIMESTAMP
+        )
+        ON CONFLICT (member_id)
+        DO UPDATE
+            SET
+                refresh_token = EXCLUDED.refresh_token,
+                created_at = CURRENT_TIMESTAMP
+    </insert>
+
+    <!--
+        작 성 자 : 박민혁
+        작 성 일 : 2025.04.18
+        내    용 : 유저 아이디로 리프레시 토큰을 받아오기
+    -->
+    <select id="getRefreshTokenByUserId" resultType="String">
+        SELECT refresh_token
+        FROM token_info
+        WHERE member_id = #{memberId}
+    </select>
+
+    <!--
+        작 성 자 : 박민혁
+        작 성 일 : 2025.04.18
+        내    용 : 리프레시 토큰 삭제
+    -->
+    <delete id="deleteRefreshToken">
+        DELETE FROM token_info
+        WHERE member_id = #{memberId}
+    </delete>
+
+</mapper>(No newline at end of file)
Add a comment
List