제목 | Spring, Redis Session 연동하기 - 중복 세션 관리 | ||||||
글쓴이 | 이지섭 | 작성일 | 2020-07-12 | 수정일 | 2025-06-26 | 조회수 | 13473 |
Spring 에서 Redis Session 을 사용하여 중복 세션을 관리하는 것이다.
아래는 Maven pom.xml 의존성 설정이다. <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>6.6.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>2.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-commons</artifactId> <version>2.3.9.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-keyvalue</artifactId> <version>2.3.9.RELEASE</version> </dependency>
아래는 스프링 부트에서의 메이븐 설정이다. <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </dependency>
아래는 경우에 따라서 필요한 라이브러리이기도 해서, 같이 기재를 하였다. <dependency> <groupId>io.netty</groupId> <artifactId>netty-transport</artifactId> <version>4.1.121.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-handler</artifactId> <version>4.1.121.Final</version> </dependency> <dependency> <groupId>io.reactivex</groupId> <artifactId>rxjava</artifactId> <version>1.0.17</version> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> <version>3.4.14</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-codec</artifactId> <version>4.1.121.Final</version> </dependency> <dependency> <groupId>io.reactivex.rxjava3</groupId> <artifactId>rxjava</artifactId> <version>3.0.9</version> </dependency>
아래는 web.xml 에서의 설정이다.
그리고, <distributable /> 설정이 들어간다. <!-- redis --> <!--
아래는 JBoss 에서 사용되는 jboss-web.xml 파일이다. WEB-INF 폴더에 위치한다. <?xml version="1.0" encoding="UTF-8"?> <jboss-web> <context-root>/</context-root> <replication-config> <replication-trigger>SET</replication-trigger> <replication-granularity>SESSION</replication-granularity> </replication-config> </jboss-web>
아래는 Spring 의 Redis 설정을 Java 클래스로 구현한 것이다. @Configuration
// @EnableRedisHttpSession(maxInactiveIntervalInSeconds=32400)
@PropertySource("classpath:application.properties")
public class RedisConfigureAction {
@Value("${redis.host}")
private String redisHostName;
@Value("${redis.port}")
private int redisPort;
@Value("${redis.password}")
private String redisPwd;
// lettuce
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(redisHostName, redisPort);
redisStandaloneConfiguration.setPassword(redisPwd);
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration);
return lettuceConnectionFactory;
}
@Bean
public StringRedisTemplate redisTemplate() {
StringRedisTemplate redisTemplate = new StringRedisTemplate();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
return redisTemplate;
}
@Bean
public static ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
}
아래는 application.properties 파일이다. 내용은 각자에 맞게 채워야 할 것이다. 경우에 따라서 Redis 비밀번호는 사용하지 않기도 한다. redis.host=localhost redis.port=6379 redis.password=123456 redis.maxInactiveIntervalInSeconds=50000
아래는 application.properties 의 내용을 xml 설정에서 사용하기 위한 spring-common-context.xml 파일의 내용이다. <?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<util:properties id="application" location="classpath:application.properties" />
......
</beans>
아래는 spring-redis-context.xml 파일의 내용이다. <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:util="http://www.springframework.org/schema/util" xmlns:cache="http://www.springframework.org/schema/cache" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
web.xml 파일에서 레디스 세션 연동 관련한 필터 설정을 주석 처리하였다. spring-redis-context.xml 파일에서도 RedisHttpSessionConfiguration 설정을 주석 처리하였다. 중복 로그인 관리에 필요한 정보만 redis 에 저장하려고 함이다.
레디스에 직접 세션 데이터를 입력하고 읽어오는 방식으로 중복 세션을 관리한다.
모든 세션 정보가 redis 에 모두 저장되도록 하려면,
1) java configuration 에서 다음을 추가하고 @EnableRedisHttpSession(maxInactiveIntervalInSeconds=32400) xml configuration 이면 RedisHttpSessionConfiguration 을 주석으로 막은 것을 풀어준다.
2) web.xml 파일에서 다음을 추가해 주면 된다.
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
web.xml 파일이 없으면 건너띄면 된다.
아래는 레디스에 데이터를 입력하고 삭제하고 읽어오는 서비스 프로그램이다.
package isry.redis.service; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.SetOperations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import isry.itgcms.sysmgmt.userauth.vo.UserInstAuthVO; import isry.itgcms.sysmgmt.userlogin.vo.UserDetailsVO; @Component public class RedisService3 { private static final Logger logger = LoggerFactory.getLogger(RedisService3.class); @Autowired StringRedisTemplate redisTemplate; public void insertRedisMap(String redisKey, Map<String, Object> map) { HashOperations<String, Object, Object> stringObjectObjectHashOperations = redisTemplate.opsForHash(); //Map<Integer, Integer> map = new HashMap<Integer, Integer>(); for (Map.Entry<String, Object> entry : map.entrySet()) { //System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue()); stringObjectObjectHashOperations.put(redisKey, entry.getKey(), entry.getValue()); } //stringObjectObjectHashOperations.put(redisKey, "Hello", "rg1"); //stringObjectObjectHashOperations.put(redisKey, "Hello2", "rg2"); //stringObjectObjectHashOperations.put(redisKey, "Hello3", "rg3"); } public void processRedisLogout(String redisKey) { Set<String> keys = redisTemplate.keys(redisKey + "*"); for (String key : keys) { redisTemplate.delete(key); } } public int selectRedisLikeSessionCount(String redisKey) { int count = 0; Set<String> keys = redisTemplate.keys(redisKey + "*"); for (String key : keys) { count++; } return count; } public List<UserDetailsVO> selectPrevLoginVOList(String redisKey) { List<UserDetailsVO> list = new ArrayList<>(); if (redisKey == null || redisKey.trim().equals("")) { return null; } Set<String> keys = redisTemplate.keys(redisKey + "*"); for (String key : keys) { UserDetailsVO vo = selectRedisSession(key); list.add(vo); } return list; } public void deleteRedisLikeSession(String redisKey) { Set<String> keys = redisTemplate.keys(redisKey + "*"); for (String key : keys) { redisTemplate.delete(key); } } public void setTimeOutSecond(String key, Integer sessionTime) { redisTemplate.expire(key, sessionTime, TimeUnit.SECONDS); } @SuppressWarnings("unchecked") public UserDetailsVO selectRedisSession(String key) { HashOperations<String, Object, Object> stringObjectObjectHashOperations = redisTemplate.opsForHash(); Map<Object, Object> entries = stringObjectObjectHashOperations.entries(key); UserDetailsVO vo = new UserDetailsVO(); vo.setAge(String.valueOf(entries.get("age"))); vo.setAgencyContacts(String.valueOf(entries.get("agencyContacts"))); vo.setAuthrtSeCd(String.valueOf(entries.get("authrtSeCd"))); vo.setBirthdate(String.valueOf(entries.get("birthdate"))); vo.setCertificate(String.valueOf(entries.get("certificate"))); vo.setCtpvNm(String.valueOf(entries.get("ctpvNm"))); vo.setDeptCd(String.valueOf(entries.get("deptCd"))); vo.setDeptNm(String.valueOf(entries.get("deptNm"))); vo.setEmail(String.valueOf(entries.get("email"))); vo.setEnfsnNo(String.valueOf(entries.get("enfsnNo"))); vo.setEnfsnRoleSeCd(String.valueOf(entries.get("enfsnRoleSeCd"))); vo.setEngCtpvNm(String.valueOf(entries.get("engCtpvNm"))); vo.setGender(String.valueOf(entries.get("gender"))); vo.setGroupAuthrtSeCd(String.valueOf(entries.get("groupAuthrtSeCd"))); vo.setId(String.valueOf(entries.get("id"))); vo.setIndvIdntfcNo(String.valueOf(entries.get("indvIdntfcNo"))); vo.setInstAuthList((List<UserInstAuthVO>)entries.get("instAuthList")); vo.setInstNm(String.valueOf(entries.get("instNm"))); vo.setInstNo(Integer.parseInt(String.valueOf(entries.get("instNo")))); vo.setInstTypeSeCd(String.valueOf(entries.get("instTypeSeCd"))); vo.setIp(String.valueOf(entries.get("ip"))); vo.setLastLoginTime(String.valueOf(entries.get("lastLoginTime"))); vo.setLgnScsYn(String.valueOf(entries.get("lgnScsYn"))); vo.setManagerYn(String.valueOf(entries.get("managerYn"))); vo.setMemberType(String.valueOf(entries.get("memberType"))); vo.setMobile(String.valueOf(entries.get("mobile"))); vo.setOrgCode(Integer.parseInt(String.valueOf(entries.get("orgCode")))); vo.setOrgName(String.valueOf(entries.get("orgName"))); vo.setPass(String.valueOf(entries.get("pass"))); vo.setRgnSeCd(String.valueOf(entries.get("rgnSeCd"))); vo.setSessionId(String.valueOf(entries.get("sessionId"))); vo.setSggCd(String.valueOf(entries.get("sggCd"))); vo.setSggNm(String.valueOf(entries.get("sggNm"))); vo.setSidoNm(String.valueOf(entries.get("sidoNm"))); vo.setSigunguNm(String.valueOf(entries.get("sigunguNm"))); vo.setTopMenuNo(String.valueOf(entries.get("topMenuNo"))); vo.setUntTaskwk(String.valueOf(entries.get("untTaskwk"))); vo.setUntTaskwkSeCd(String.valueOf(entries.get("untTaskwkSeCd"))); vo.setUserInstNo(Integer.parseInt(String.valueOf(entries.get("userInstNo")))); vo.setUserName(String.valueOf(entries.get("userName"))); vo.setWrdTelno(String.valueOf(entries.get("wrdTelno"))); vo.setYngbgsPrtcrNo(String.valueOf(entries.get("yngbgsPrtcrNo"))); return vo; } public Set<String> selectKey(String key) { Set<String> set = new HashSet<>(); SetOperations<String, String> stringStringSetOperations = redisTemplate.opsForSet(); Cursor<String> cursor = stringStringSetOperations.scan(key, ScanOptions.scanOptions().match("*").build()); while (cursor.hasNext()) { //logger.debug("cursor = " + cursor.next()); set.add(cursor.next()); } return set; } }
위 프로그램을 사용하여 로그인 세션을 저장하고 읽어오는 부분을 아래와 같이 레디스를 연동할 수 있다
package com.rg.util.controller;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.rg.login.dto.CustomUserDetails;
import com.rg.login.dto.UserDetailsVO;
import com.rg.login.service.LoginService;
import com.rg.util.RedisService3;
import java.util.Optional;
@Controller
public class EnvironmentController {
private final Logger logger = LogManager.getLogger(EnvironmentController.class);
@Autowired
private LoginService loginService;
@Autowired
private RedisService3 redisService;
@RequestMapping(value = {"/getEnvironment.do", "/{lang}/getEnvironment.do"})
@ResponseBody
public Map getEnvironment(@PathVariable("lang") Optional langVal,
HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
UserDetailsVO vo = null;
CustomUserDetails userInfo = null;
String loginId = null;
String loginUserName = null;
try {
if (SecurityContextHolder.getContext().getAuthentication() != null &&
SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {
if (SecurityContextHolder.getContext().getAuthentication().getDetails() instanceof CustomUserDetails) {
userInfo = (CustomUserDetails)SecurityContextHolder.getContext().getAuthentication().getDetails();
loginId = userInfo.getUsername();
loginUserName = loginService.getUserName(loginId);
}
}
} catch (Exception e) {
e.printStackTrace();
}
String redisKey = "LOGIN||SESSION||" + loginId + "||" + session.getId();
vo = redisService.selectRedisSession(redisKey);
loginId = vo == null ? "" : vo.getLoginId();
logger.info("#### remoteAddr : " + request.getRemoteAddr());
logger.info("#### loginId : " + loginId);
logger.info("#### loginUserName : " + loginUserName);
Map map = new HashMap<String, String>();
if (vo != null && loginId != null && !"".equals(loginId) && !"null".equals(loginId)) {
map.put("loginId", loginId);
map.put("loginUserName", vo.getLoginUserName());
int sessionTime = 60 * 60 * 5;
redisService.setTimeOutSecond(redisKey, sessionTime);
}
return map;
}
}
반드시 주의할 점이 있는데,
String redisKey = "LOGIN||SESSION||" + loginId + "||" + session.getId();
이 sId 인 세션 아이디 값은 이중화 환경에서는 세션 클러스터링이 되어야만 쓸 수 있다.
아니면, WAS 가 여러 대일 경우에는 세션 값이 아닌 다른 고유의 값을 만들어 써야만 한다.
아래 부분에 세션 아이디 가져오는 부분도 마찬가지다.
String sId = session.getId();
@Component
public class CustomLoginSuccessHandler implements AuthenticationSuccessHandler {
private final Logger logger = LogManager.getLogger(CustomLoginSuccessHandler.class);
@Autowired
private LoginService loginService;
@Autowired
private RedisService3 redisService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
String userId = (String)authentication.getPrincipal();
String loginId = authentication.getName();
String loginName = loginService.getUserName(loginId);
HttpSession session = request.getSession();
String sId = session.getId();
session.setAttribute("loginId", loginId);
session.setAttribute("loginUserName", loginName);
UserDetailsVO userDetailsVO = new UserDetailsVO();
int sessionTime = 60 * 60 * 5;
userDetailsVO.setLoginId(loginId);
userDetailsVO.setLoginUserId1(userId);
userDetailsVO.setLoginUserName(loginName);
redisService.deleteRedisLikeSession("LOGIN||SESSION||" + loginId + "||*");
// redis 세션 저장
String redisKey = "LOGIN||SESSION||" + loginId + "||" + sId;
redisService.insertRedisMap(redisKey, new HashMap<String, Object>(userDetailsVO.getMap()));
redisService.setTimeOutSecond(redisKey, sessionTime);
CookieHandle.setCookie(response, "login_id", loginId);
CookieHandle.setCookie(response, "session_id", sId);
response.sendRedirect("/rg/index.jsp");
}
}
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { @Autowired private RedisService3 redisService; @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { HttpSession session = request.getSession(false); String loginId = CookieHandle.getCookie(request, "login_id"); String sessionId = CookieHandle.getCookie(request, "session_id"); redisService.processRedisLogout("LOGIN||SESSION||" + loginId + "||" + sessionId); if (session != null) { session.invalidate(); } response.setStatus(HttpServletResponse.SC_OK); response.sendRedirect("/"); } }
아래는 redis 서버의 내용을 보는 것이다. ubuntu@ip-172-31-27-22:~$ redis-cli 127.0.0.1:6379> auth 123456 OK 127.0.0.1:6379> flushall OK 127.0.0.1:6379> keys * 1) "spring:session:sessions:expires:6d51ce20-5aba-4648-91bd-2b4d40f96723" 2) "spring:session:sessions:c876fd17-6f68-46ad-9607-41656c9f6911" 3) "spring:session:sessions:ad469d20-91bf-4060-8a7b-d50aeb8922d9" 4) "spring:session:expirations:1599488220000" 5) "spring:session:sessions:6d51ce20-5aba-4648-91bd-2b4d40f96723" 6) "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:rg" 7) "spring:session:sessions:expires:c876fd17-6f68-46ad-9607-41656c9f6911" 8) "spring:session:sessions:expires:ad469d20-91bf-4060-8a7b-d50aeb8922d9" 127.0.0.1:6379> type spring:session:sessions:6d51ce20-5aba-4648-91bd-2b4d40f96723 hash 127.0.0.1:6379> hgetall spring:session:sessions:6d51ce20-5aba-4648-91bd-2b4d40f96723 1) "creationTime" 2) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01te\xf1\xd9N" 3) "lastAccessedTime" 4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01te\xf1\xd9N" 5) "sessionAttr:org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository.CSRF_TOKEN" 6) "\xac\xed\x00\x05sr\x006org.springframework.security.web.csrf.DefaultCsrfTokenZ\xef\xb7\xc8/\xa2\xfb\xd5\x02\x00\x03L\x00\nheaderNamet\x00\x12Ljava/lang/String;L\x00\rparameterNameq\x00~\x00\x01L\x00\x05tokenq\x00~\x00\x01xpt\x00\x0cX-CSRF-TOKENt\x00\x05_csrft\x00$bb6d297d-c3f0-44be-a61e-00a24c254cdd" 7) "maxInactiveInterval" 8) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\xc3P" 127.0.0.1:6379> redis 서버에 비밀번호가 없다면 auth 명령어는 필요없습니다.
첨부한 파일 중에 UserLoginServiceImpl.java 파일은 스프링 시큐리티를 사용하지 않는 버전이다.
[ 참조한 웹 페이지 ] https://m.blog.naver.com/jooda99/221460700542 https://sabarada.tistory.com/105 | |||||||
첨부파일 | StringUtil.java (32,879 byte) RedisService3.java (6,176 byte) context-redis.xml (2,719 byte) context-security.xml (2,123 byte) CookieHandle.java (922 byte) CustomLogoutSuccessHandler.java (1,261 byte) EnvironmentController.java (2,878 byte) CustomLoginSuccessHandler.java (2,415 byte) CustomAuthenticationProvider.java (2,439 byte) SHA512.java (610 byte) UserLoginServiceImpl.java (85,938 byte) UserDetailsVO.java (14,781 byte) UnitSysAuthVO.java (8,712 byte) RedisConfigureAction.java (2,390 byte) | ||||||
로그인 | Language : |