티스토리 뷰

 

https://ekeprl.tistory.com/66

 

[Spring Security - JWT] (4) : 로그인 API 및 Postman 테스트

https://ekeprl.tistory.com/65 [Spring Security - JWT] (3) : JwtFilter이전 포스팅을 통해 JWT 토큰의 생성과 검증을 담당하는 JwtUtil을 작성했다.https://ekeprl.tistory.com/64 이번 포스팅은 매 요청마다 JWT 토큰을 검증

ekeprl.tistory.com

 

이전포스팅에서 로그인/회원가입 api작성과 Postman을 이용해 테스트를 진행했다.

 

이번 포스팅은 RefreshToken을 구현해보려 한다.

 

0. RefreshToken이란

Access Token이 만료되었을 때 재발급을 위해 사용하는 토큰을 말한다.

Access Token만 사용하면 만료시간을 짧게 설정할수록 보안성은 높아지지만, 사용자가 자주 로그인해야 하는 불편함이 발생한다.

Refresh Token을 함께 사용하면 Access Token을 짧게 유지하면서도 사용자 경험을 해치지 않을 수 있다.

토큰 갱신 플로우차트

 

 

1.DB 테이블 설계

CREATE TABLE refresh_token (
refresh_token_id    BIGINT          NOT NULL AUTO_INCREMENT,
user_id             VARCHAR(50)     NOT NULL,
token               VARCHAR(512)    NOT NULL,
expired_at          DATETIME        NOT NULL,
created_at          DATETIME        NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (refresh_token_id),
UNIQUE KEY uk_user_id (user_id),
INDEX idx_token (token)
);

먼저 DB에 Refresh Token을 저장하기 위해 테이블을 생성했다.

1 계정 1 토큰으로 관리하고, 재로그인 시 기존 토큰을 UPDATE 한다.

 

2. RefreshTokenModel

data class RefreshTokenModel(
    val refreshTokenId: Long = 0,
    val userId: String = "",
    val token: String = "",
    val expiredAt: LocalDateTime = LocalDateTime.now(),
    val createdAt: LocalDateTime = LocalDateTime.now(),
) {
    companion object {
        fun create(
            userId: String,
            token: String,
            expiredAt: LocalDateTime,
        ) = RefreshTokenModel(
            userId = userId,
            token = token,
            expiredAt = expiredAt,
            createdAt = LocalDateTime.now(),
        )
    }
}

테이블의 구조와 매필할 수 있도록 Model을 생성했다.

 

3. RefreshTokenMapper

@Mapper
interface RefreshTokenMapper {
    fun insert(refreshToken: RefreshTokenModel)
    fun findByUserId(userId: String): RefreshTokenModel?
    fun findByToken(token: String): RefreshTokenModel?
    fun deleteByUserId(userId: String)
}

 

4. RefreshTokenMapper.xml

<mapper namespace="mapper.RefreshTokenMapper">

    <resultMap id="refreshTokenResultMap" type="RefreshTokenModel" autoMapping="false">
        <id     property="refreshTokenId"   column="refresh_token_id"/>
        <result property="userId"           column="user_id"/>
        <result property="token"            column="token"/>
        <result property="expiredAt"        column="expired_at"/>
        <result property="createdAt"        column="created_at"/>
    </resultMap>

    <insert id="insert" parameterType="RefreshTokenModel">
        INSERT INTO refresh_token (user_id, token, expired_at, created_at)
        VALUES (#{userId}, #{token}, #{expiredAt}, #{createdAt})
            ON DUPLICATE KEY UPDATE
                                 token = #{token},
                                 expired_at = #{expiredAt}
    </insert>

    <select id="findByUserId" resultMap="refreshTokenResultMap">
        SELECT * FROM refresh_token
        WHERE user_id = #{userId}
    </select>

    <select id="findByToken" resultMap="refreshTokenResultMap">
        SELECT * FROM refresh_token
        WHERE token = #{token}
    </select>

    <delete id="deleteByUserId">
        DELETE FROM refresh_token
        WHERE user_id = #{userId}
    </delete>

</mapper>

 

MyBatis를 이용해 refresh token테이블과 매핑했고, resultMap으로 프로퍼티를 명시적으로 매핑했다.

 

5. 로그인 응답 변경

기존 로그인은 Access Token만 반환했지만, Refresh Token도 함께 반환하도록 변경했다.

 

 5.1) LoginResponseModel

data class LoginResponseModel(
    val accessToken: String,
    val refreshToken: String,
)

 

 5.2) UserService

fun refresh(refreshToken: String): String {
    // 토큰 유효성 검증
    if (!jwtUtil.validateToken(refreshToken)) {
        throw IllegalArgumentException("유효하지 않은 토큰입니다.")
    }

    // DB에서 토큰 조회
    val savedToken = refreshTokenMapper.findByToken(refreshToken)
        ?: throw IllegalArgumentException("존재하지 않는 토큰입니다.")

    // 만료 시간 체크
    if (savedToken.expiredAt.isBefore(LocalDateTime.now())) {
        throw IllegalArgumentException("만료된 토큰입니다.")
    }

    // 새 Access Token 발급
    val user = userMapper.findById(savedToken.userId)
        ?: throw IllegalArgumentException("존재하지 않는 유저입니다.")

    return jwtUtil.generateAccessToken(user.id, user.email, user.role.toAuthority())
}

기존 로그인은 Access Token만 반환했지만, Refresh Token도 함께 반환하도록 변경했다.

 

이상으로 Refresh Token 구현을 알아보았다.

다음 포스팅은 재발급 / 로그아웃 API 작성 및 Postman 테스트를 진행해보려 한다.

 

감사합니다.

 

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함