제목 | Spring, Redis Session 연동하기 - 중복 세션 관리 | ||||||
글쓴이 | 이지섭 | 작성일 | 2020-07-12 | 수정일 | 2024-07-03 | 조회수 | 10915 |
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>5.3.1.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>io.netty</groupId> <artifactId>netty-transport</artifactId> <version>4.1.51.Final</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-handler</artifactId> <version>4.1.51.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.0.33.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 파일의 내용이다. application 속성을 설정한다. <?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 에 저장되도록 하려면,
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; } }
@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://handcoding.tistory.com/137 https://m.blog.naver.com/jooda99/221460700542 https://sabarada.tistory.com/105 | |||||||
첨부파일 | UnitSysAuthVO.java (9,141 byte) UserDetailsVO.java (15,210 byte) StringUtil.java (32,879 byte) RedisService3.java (6,176 byte) UserLoginServiceImpl.java (86,789 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) | ||||||
로그인 | Language : |