Merge branch 'dev'
This commit is contained in:
@@ -3,8 +3,10 @@ package com.sb.web;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@MapperScan({"com.sb.web.user.mapper", "com.sb.web.auth.mapper", "com.sb.web.board.mapper", "com.sb.web.account.mapper", "com.sb.web.admin.mapper"})
|
||||
public class SbBtApplication {
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.sb.web.auth.domain;
|
||||
|
||||
import com.sb.web.auth.dto.SessionUser;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 세션 영속 백업(auth_session). Redis 유실 시 여기서 SessionUser 를 복원한다.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AuthSession {
|
||||
|
||||
private String token;
|
||||
private Long memberId;
|
||||
private String loginId;
|
||||
private String name;
|
||||
private String role;
|
||||
private String provider;
|
||||
private boolean rememberMe;
|
||||
private LocalDateTime expiresAt;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public static AuthSession of(String token, SessionUser u, LocalDateTime expiresAt) {
|
||||
return AuthSession.builder()
|
||||
.token(token)
|
||||
.memberId(u.getId())
|
||||
.loginId(u.getLoginId())
|
||||
.name(u.getName())
|
||||
.role(u.getRole())
|
||||
.provider(u.getProvider())
|
||||
.rememberMe(u.isRememberMe())
|
||||
.expiresAt(expiresAt)
|
||||
.build();
|
||||
}
|
||||
|
||||
public SessionUser toSessionUser() {
|
||||
return SessionUser.builder()
|
||||
.id(memberId)
|
||||
.loginId(loginId)
|
||||
.name(name)
|
||||
.role(role)
|
||||
.provider(provider)
|
||||
.rememberMe(rememberMe)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.sb.web.auth.mapper;
|
||||
|
||||
import com.sb.web.auth.domain.AuthSession;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 세션 영속 백업(auth_session) 접근.
|
||||
*/
|
||||
@Mapper
|
||||
public interface AuthSessionMapper {
|
||||
|
||||
int insert(AuthSession session);
|
||||
|
||||
AuthSession findByToken(@Param("token") String token);
|
||||
|
||||
int updateExpiry(@Param("token") String token, @Param("expiresAt") LocalDateTime expiresAt);
|
||||
|
||||
int delete(@Param("token") String token);
|
||||
|
||||
int deleteExpired(@Param("now") LocalDateTime now);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sb.web.auth.service;
|
||||
|
||||
import com.sb.web.auth.domain.AuthSession;
|
||||
import com.sb.web.auth.domain.Member;
|
||||
import com.sb.web.auth.dto.LoginRequest;
|
||||
import com.sb.web.auth.dto.LoginResponse;
|
||||
@@ -35,6 +36,7 @@ public class AuthService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final RedisTemplate<String, Object> redisTemplate;
|
||||
private final com.sb.web.admin.service.AppSettingService appSettingService;
|
||||
private final com.sb.web.auth.mapper.AuthSessionMapper authSessionMapper;
|
||||
|
||||
private static final String SESSION_PREFIX = "session:";
|
||||
private static final Duration SESSION_TTL = Duration.ofMinutes(60); // 일반 세션
|
||||
@@ -106,7 +108,14 @@ public class AuthService {
|
||||
session.setRememberMe(req.isRememberMe());
|
||||
|
||||
String token = UUID.randomUUID().toString().replace("-", "");
|
||||
java.time.LocalDateTime expiresAt = java.time.LocalDateTime.now().plus(ttl);
|
||||
try {
|
||||
redisTemplate.opsForValue().set(SESSION_PREFIX + token, session, ttl);
|
||||
} catch (Exception e) {
|
||||
log.warn("[login] Redis 세션 저장 실패(무시, DB 백업 사용): {}", e.toString());
|
||||
}
|
||||
// 영속 백업 — Redis 재시작/유실에도 로그인 유지
|
||||
authSessionMapper.insert(com.sb.web.auth.domain.AuthSession.of(token, session, expiresAt));
|
||||
log.info("[login] {} (token issued, rememberMe={})", member.getLoginId(), req.isRememberMe());
|
||||
|
||||
return LoginResponse.builder()
|
||||
@@ -118,24 +127,62 @@ public class AuthService {
|
||||
|
||||
/**
|
||||
* 토큰으로 세션을 조회하고, 유효하면 TTL 을 갱신(슬라이딩 만료)한다.
|
||||
* Redis 에 없으면(재시작/유실/장애) DB 백업(auth_session)에서 복원하고 Redis 를 재수화한다.
|
||||
*/
|
||||
public SessionUser getSession(String token) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
String key = SESSION_PREFIX + token;
|
||||
// 1) Redis 우선 (장애 시 예외는 무시하고 DB 백업으로)
|
||||
try {
|
||||
Object cached = redisTemplate.opsForValue().get(key);
|
||||
if (cached instanceof SessionUser user) {
|
||||
// 슬라이딩 만료: 자동 로그인 세션이면 길게(30일), 아니면 60분으로 갱신
|
||||
redisTemplate.expire(key, user.isRememberMe() ? REMEMBER_TTL : SESSION_TTL);
|
||||
return user;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[session] Redis 조회 실패 → DB 백업 사용: {}", e.toString());
|
||||
}
|
||||
// 2) DB 백업에서 복원
|
||||
AuthSession db = authSessionMapper.findByToken(token);
|
||||
if (db == null) {
|
||||
return null;
|
||||
}
|
||||
if (db.getExpiresAt() == null || db.getExpiresAt().isBefore(java.time.LocalDateTime.now())) {
|
||||
authSessionMapper.delete(token); // 만료분 정리
|
||||
return null;
|
||||
}
|
||||
SessionUser user = db.toSessionUser();
|
||||
Duration ttl = user.isRememberMe() ? REMEMBER_TTL : SESSION_TTL;
|
||||
try {
|
||||
redisTemplate.opsForValue().set(key, user, ttl); // Redis 재수화
|
||||
} catch (Exception ignore) {
|
||||
// Redis 가 아직 불가해도 DB 로 동작
|
||||
}
|
||||
authSessionMapper.updateExpiry(token, java.time.LocalDateTime.now().plus(ttl)); // DB 슬라이딩
|
||||
return user;
|
||||
}
|
||||
|
||||
public void logout(String token) {
|
||||
if (token != null && !token.isBlank()) {
|
||||
try {
|
||||
redisTemplate.delete(SESSION_PREFIX + token);
|
||||
} catch (Exception ignore) {
|
||||
// Redis 장애여도 DB 삭제는 진행
|
||||
}
|
||||
authSessionMapper.delete(token);
|
||||
}
|
||||
}
|
||||
|
||||
/** 만료된 백업 세션 정리 (매일 새벽 4시) */
|
||||
@org.springframework.scheduling.annotation.Scheduled(cron = "0 0 4 * * *")
|
||||
public void cleanupExpiredSessions() {
|
||||
try {
|
||||
int n = authSessionMapper.deleteExpired(java.time.LocalDateTime.now());
|
||||
if (n > 0) log.info("[session] 만료 백업 세션 {}건 정리", n);
|
||||
} catch (Exception e) {
|
||||
log.warn("[session] 만료 세션 정리 실패: {}", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,3 +30,20 @@ CREATE TABLE IF NOT EXISTS app_setting (
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
INSERT IGNORE INTO app_setting (id, signup_enabled) VALUES (1, 1);
|
||||
|
||||
-- ============================================================
|
||||
-- 세션 백업 (Redis 유실/재시작 대비 영속 저장). getSession 미스 시 DB에서 복원.
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS auth_session (
|
||||
token VARCHAR(64) NOT NULL COMMENT '세션 토큰',
|
||||
member_id BIGINT NOT NULL,
|
||||
login_id VARCHAR(50) NULL,
|
||||
name VARCHAR(100) NULL,
|
||||
role VARCHAR(20) NULL,
|
||||
provider VARCHAR(20) NULL,
|
||||
remember_me TINYINT(1) NOT NULL DEFAULT 0,
|
||||
expires_at DATETIME NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (token),
|
||||
KEY idx_auth_session_expires (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.sb.web.auth.mapper.AuthSessionMapper">
|
||||
|
||||
<insert id="insert" parameterType="com.sb.web.auth.domain.AuthSession">
|
||||
INSERT INTO auth_session
|
||||
(token, member_id, login_id, name, role, provider, remember_me, expires_at, created_at)
|
||||
VALUES
|
||||
(#{token}, #{memberId}, #{loginId}, #{name}, #{role}, #{provider},
|
||||
#{rememberMe}, #{expiresAt}, NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
expires_at = #{expiresAt}
|
||||
</insert>
|
||||
|
||||
<select id="findByToken" resultType="com.sb.web.auth.domain.AuthSession">
|
||||
SELECT token, member_id, login_id, name, role, provider, remember_me, expires_at, created_at
|
||||
FROM auth_session
|
||||
WHERE token = #{token}
|
||||
</select>
|
||||
|
||||
<update id="updateExpiry">
|
||||
UPDATE auth_session SET expires_at = #{expiresAt} WHERE token = #{token}
|
||||
</update>
|
||||
|
||||
<delete id="delete">
|
||||
DELETE FROM auth_session WHERE token = #{token}
|
||||
</delete>
|
||||
|
||||
<delete id="deleteExpired">
|
||||
DELETE FROM auth_session WHERE expires_at < #{now}
|
||||
</delete>
|
||||
|
||||
</mapper>
|
||||
Reference in New Issue
Block a user