Merge branch 'dev'
This commit is contained in:
@@ -3,8 +3,10 @@ package com.sb.web;
|
|||||||
import org.mybatis.spring.annotation.MapperScan;
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@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"})
|
@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 {
|
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;
|
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.domain.Member;
|
||||||
import com.sb.web.auth.dto.LoginRequest;
|
import com.sb.web.auth.dto.LoginRequest;
|
||||||
import com.sb.web.auth.dto.LoginResponse;
|
import com.sb.web.auth.dto.LoginResponse;
|
||||||
@@ -35,6 +36,7 @@ public class AuthService {
|
|||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final RedisTemplate<String, Object> redisTemplate;
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
private final com.sb.web.admin.service.AppSettingService appSettingService;
|
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 String SESSION_PREFIX = "session:";
|
||||||
private static final Duration SESSION_TTL = Duration.ofMinutes(60); // 일반 세션
|
private static final Duration SESSION_TTL = Duration.ofMinutes(60); // 일반 세션
|
||||||
@@ -106,7 +108,14 @@ public class AuthService {
|
|||||||
session.setRememberMe(req.isRememberMe());
|
session.setRememberMe(req.isRememberMe());
|
||||||
|
|
||||||
String token = UUID.randomUUID().toString().replace("-", "");
|
String token = UUID.randomUUID().toString().replace("-", "");
|
||||||
redisTemplate.opsForValue().set(SESSION_PREFIX + token, session, ttl);
|
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());
|
log.info("[login] {} (token issued, rememberMe={})", member.getLoginId(), req.isRememberMe());
|
||||||
|
|
||||||
return LoginResponse.builder()
|
return LoginResponse.builder()
|
||||||
@@ -118,24 +127,62 @@ public class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 토큰으로 세션을 조회하고, 유효하면 TTL 을 갱신(슬라이딩 만료)한다.
|
* 토큰으로 세션을 조회하고, 유효하면 TTL 을 갱신(슬라이딩 만료)한다.
|
||||||
|
* Redis 에 없으면(재시작/유실/장애) DB 백업(auth_session)에서 복원하고 Redis 를 재수화한다.
|
||||||
*/
|
*/
|
||||||
public SessionUser getSession(String token) {
|
public SessionUser getSession(String token) {
|
||||||
if (token == null || token.isBlank()) {
|
if (token == null || token.isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String key = SESSION_PREFIX + token;
|
String key = SESSION_PREFIX + token;
|
||||||
Object cached = redisTemplate.opsForValue().get(key);
|
// 1) Redis 우선 (장애 시 예외는 무시하고 DB 백업으로)
|
||||||
if (cached instanceof SessionUser user) {
|
try {
|
||||||
// 슬라이딩 만료: 자동 로그인 세션이면 길게(30일), 아니면 60분으로 갱신
|
Object cached = redisTemplate.opsForValue().get(key);
|
||||||
redisTemplate.expire(key, user.isRememberMe() ? REMEMBER_TTL : SESSION_TTL);
|
if (cached instanceof SessionUser user) {
|
||||||
return user;
|
redisTemplate.expire(key, user.isRememberMe() ? REMEMBER_TTL : SESSION_TTL);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[session] Redis 조회 실패 → DB 백업 사용: {}", e.toString());
|
||||||
}
|
}
|
||||||
return null;
|
// 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) {
|
public void logout(String token) {
|
||||||
if (token != null && !token.isBlank()) {
|
if (token != null && !token.isBlank()) {
|
||||||
redisTemplate.delete(SESSION_PREFIX + token);
|
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)
|
PRIMARY KEY (id)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
INSERT IGNORE INTO app_setting (id, signup_enabled) VALUES (1, 1);
|
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