In an e-commerce project, when a user logs in to a single server, the user information is set to the session. The user information is obtained from the session and deleted from the session when the user logs out.

However, after setting up the Tomcat cluster, Session sharing needs to be considered, which can be realized through single sign-on solutions. There are two main methods: one is to realize it by Redis + Cookie, and the other is to solve it by using the Spring Session framework.

Redis + cookies

Single sign-on idea

User login:

  • First verify the user password is correct, and return the user information;
  • useuuidsession.getIdGenerate a uniqueid(token)And set it tocookie, write it to the client;
  • Add user information (userObject) tojsonFormat;
  • In order tokey=token.Value =(JSON format of user)Write,redisAnd set the expiration time.

Log out:

  • It is carried when the user requests itcookie, fromcookieGet to thetoken;
  • Obtained from the requestcookie, and set the expiration time to0, is written to the response, that is, deletedtoken;
  • Again fromredisRemove thetoken;

Get user information:

  • Carried from the requestcookieGet to thetoken;
  • According to thetokenredisTo query the correspondinguserThe object’sjsonString;
  • willjsonString into auserObject;

Redis connection pool and utility classes

Since both token and User objects are stored in Redis, there is a redis connection pool and utility class encapsulated here.

First, encapsulate a Redis connection pool and fetch jedis instances directly from the pool each time.

public class RedisPool {

    private static JedisPool jedisPool;

    private static String redisIP = PropertiesUtil.getProperty("redis.ip"."192.168.23.130");
    private static Integer redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port"."6379"));
    // Maximum number of connections
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total"."20"));
    // Maximum number of jedis instances in idle state
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle"."10"));
    // Minimum number of idle jedis instances
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle"."2"));
    // Whether to validate when borrow a Jedis instance
    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow"."true"));
    // Whether to validate when returning a Jedis instance
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return"."true"));

    static {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);
        jedisPool = new JedisPool(config, redisIP, redisPort, 1000*2);
    }

    public static Jedis getJedis(a) {
        return jedisPool.getResource();
    }
    public static void returnJedis(Jedis jedis) { jedis.close(); }}Copy the code

It is then packaged into a utility class. The basic operation is to fetch the JEDis instance from the Redis connection pool, perform set/ GET /expire operations, and then put it back into the Redis connection pool.

@Slf4j
public class RedisPoolUtil {

    // exTime is in seconds
    public static Long expire(String key, int exTime) {
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.expire(key, exTime);
        } catch (Exception e) {
            log.error("expire key:{}, error", key, e);
        }
        RedisPool.returnJedis(jedis);
        return result;
    }

    public static Long del(String key) {
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.del(key);
        } catch (Exception e) {
            log.error("del key:{}, error", key, e);
        }
        RedisPool.returnJedis(jedis);
        return result;
    }

    public static String get(String key) {
        Jedis jedis = null;
        String result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.get(key);
        } catch (Exception e) {
            log.error("get key:{}, error", key, e);
        }
        RedisPool.returnJedis(jedis);
        return result;
    }

    public static String set(String key, String value) {
        Jedis jedis = null;
        String result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.set(key, value);
        } catch (Exception e) {
            log.error("set key:{}, value:{}, error", key, value, e);
        }
        RedisPool.returnJedis(jedis);
        return result;
    }

    // exTime is in seconds
    public static String setEx(String key, String value, int exTime) {
        Jedis jedis = null;
        String result = null;
        try {
            jedis = RedisPool.getJedis();
            result = jedis.setex(key, exTime, value);
        } catch (Exception e) {
            log.error("setex key:{}, value:{}, error", key, value, e);
        }
        RedisPool.returnJedis(jedis);
        returnresult; }}Copy the code

JsonUtil tools

To store the User object in Redis, you need to convert it to JSON format. To retrieve the user object from Redis, you need to convert it to user object. A JSON utility class is encapsulated here.

The JsonUtil utility classes mainly use ObjectMapper classes.

  • beanClass is converted toStringType, usingwriterValueAsStringMethods.
  • StringType conversion tobeanClass, the use ofreadValueMethods.
@Slf4j
public class JsonUtil {

    private static ObjectMapper objectMapper = new ObjectMapper();

    static {
        // All fields are included when serializing
        objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.ALWAYS);
        // Cancel the default from DATES to TIMESTAMPS
        objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
        // Ignore the empty bean to JSON error
        objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false);
        // All dates have the same style
        objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
        // Ignore the presence in json strings, there is no corresponding property in Java objects
        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    public static <T> String obj2Str(T obj) {
        if (obj == null) { return null; }
        try {
            return obj instanceof String ? (String) obj : objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("Parse Object to String error", e);
            return null; }}public static <T> String obj2StrPretty(T obj) {
        if (obj == null) { return null; }
        try {
            return obj instanceof String ? (String) obj :
                    objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
        } catch (Exception e) {
            log.warn("Parse Object to String error", e);
            return null; }}public static <T> T str2Obj(String str, Class<T> clazz) {
        if (StringUtils.isEmpty(str) || clazz == null) {
            return null;
        }
        try {
            return clazz.equals(String.class) ? (T)str : objectMapper.readValue(str, clazz);
        } catch (Exception e) {
            log.warn("Parse String to Object error", e);
            return null; }}public static <T> T str2Obj(String str, TypeReference<T> typeReference) {
        if (StringUtils.isEmpty(str) || typeReference == null) {
            return null;
        }
        try {
            return typeReference.getType().equals(String.class) ? (T)str : objectMapper.readValue(str, typeReference);
        } catch (Exception e) {
            log.warn("Parse String to Object error", e);
            return null; }}public static <T> T str2Obj(String str, Class
        collectionClass, Class
        elementClass) {
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass, elementClass);
        try {
            return objectMapper.readValue(str, javaType);
        } catch (Exception e) {
            log.warn("Parse String to Object error", e);
            return null; }}}Copy the code

CookieUtil tools

Upon login, you need to set the token in the cookie and return it to the client. Upon exit, you need to read the token from the cookie carried in the request. After setting the expiration time, you need to set the token in the cookie and return it to the client. You need to read the token from the cookie carried in the request, and query in Redis to obtain the User object. Here, I’m also encapsulating a cookie utility class.

In CookieUtil:

  • readLoginTokenThe methods are mainly fromrequestreadCookie;
  • writeLoginTokenMethod Main SettingsCookieObject to theresponse;
  • delLoginTokenThe methods are mainly fromrequestReads theCookiethemaxAgeSet to0And then add toresponse;
@Slf4j
public class CookieUtil {

    private static final String COOKIE_DOMAIN = ".happymmall.com";
    private static final String COOKIE_NAME = "mmall_login_token";

    public static String readLoginToken(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        if(cookies ! =null) {
            for (Cookie cookie : cookies) {
                log.info("read cookieName:{}, cookieValue:{}", cookie.getName(), cookie.getValue());
                if (StringUtils.equals(COOKIE_NAME, cookie.getName())) {
                    log.info("return cookieName:{}, cookieValue:{}", cookie.getName(), cookie.getValue());
                    returncookie.getValue(); }}}return null;
    }

    public static void writeLoginToken(HttpServletResponse response, String token) {
        Cookie cookie  = new Cookie(COOKIE_NAME, token);
        cookie.setDomain(COOKIE_DOMAIN);
        cookie.setPath("/");
        // Prevent scripting attacks
        cookie.setHttpOnly(true);
        // The unit is seconds. If the value is -1, it is permanent.
        // If MaxAge is not set, cookies are not written to hard disk, but in memory, and are only valid for the current page
        cookie.setMaxAge(60 * 60 * 24 * 365);
        log.info("write cookieName:{}, cookieValue:{}", cookie.getName(), cookie.getValue());
        response.addCookie(cookie);
    }

    public static void delLoginToken(HttpServletRequest request, HttpServletResponse response) {
        Cookie[] cookies = request.getCookies();
        if(cookies ! =null) {
            for (Cookie cookie : cookies) {
                if (StringUtils.equals(COOKIE_NAME, cookie.getName())) {
                    cookie.setDomain(COOKIE_DOMAIN);
                    cookie.setPath("/");
                    // If maxAge is set to 0, it will be deleted
                    cookie.setMaxAge(0);
                    log.info("del cookieName:{}, cookieValue:{}", cookie.getName(), cookie.getValue());
                    response.addCookie(cookie);
                    return;
                }
            }
        }
    }

}
Copy the code

Specific business

After verifying the password during login:

CookieUtil.writeLoginToken(response, session.getId());
RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2Str(serverResponse.getData()), Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
Copy the code

When logging out:

String loginToken = CookieUtil.readLoginToken(request);
CookieUtil.delLoginToken(request, response);
RedisShardedPoolUtil.del(loginToken);
Copy the code

When obtaining user information:

String loginToken = CookieUtil.readLoginToken(request);
if (StringUtils.isEmpty(loginToken)) {
    return ServerResponse.createByErrorMessage("User is not logged in, cannot obtain current user information");
}
String userJsonStr = RedisShardedPoolUtil.get(loginToken);
User user = JsonUtil.str2Obj(userJsonStr, User.class);
Copy the code

SessionExpireFilter filter

In addition, after a user logs in, the Session validity period needs to be reset after each operation. You can do this using filters.

public class SessionExpireFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}@Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        if (StringUtils.isNotEmpty(loginToken)) {
            String userJsonStr = RedisShardedPoolUtil.get(loginToken);
            User user = JsonUtil.str2Obj(userJsonStr, User.class);
            if(user ! =null) {
                RedisShardedPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy(a) {}}Copy the code

You also need to configure it in the web.xml file:

<filter>
    <filter-name>sessionExpireFilter</filter-name>
    <filter-class>com.mmall.controller.common.SessionExpireFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>sessionExpireFilter</filter-name>
    <url-pattern>*.do</url-pattern>
</filter-mapping>
Copy the code

The pitfalls of this approach

  • redis + cookieThe single sign-on method is more intrusive to the code.
  • The client must be enabledcookieSome browsers don’t support itcookie;
  • CookieSet up thedomainThe domain name mode of the server must be unified.

The Spring Session implementation

Spring Session is one of the Spring projects that provides a solution for creating and managing Server HTTPSessions. By default, external Redis is used to store Session data, so as to solve the problem of Session sharing.

Spring Sessions can solve the Session sharing problem noninvasively, but they cannot be sharded.

Spring Session project integration

1. Introduce Spring Session POM

<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> < version > 1.2.2. RELEASE < / version > < / dependency >Copy the code

2. Configure DelegatingFilterProxy

<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>*.do</url-pattern>
</filter-mapping>
Copy the code

3, configuration RedisHttpSessionConfiguration

<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
    <property name="maxInactiveIntervalInSeconds" value="1800" />
</bean>
Copy the code

4. Configure JedisPoolConfig

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <property name="maxTotal" value="20" />
</bean>
Copy the code

5. Configure JedisSessionFactory

<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" >
    <property name="hostName" value="192.168.23.130" />
    <property name="port" value="6379" />
    <property name="database" value="0" />
    <property name="poolConfig" ref="jedisPoolConfig" />
</bean>
Copy the code

6. Configure DefaultCookieSerializer

<bean id="defaultCookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
    <property name="cookieName" value="SESSION_NAME" />
    <property name="domainName" value=".happymmall.com" />
    <property name="useHttpOnlyCookie" value="true" />
    <property name="cookiePath" value="/" />
    <property name="cookieMaxAge" value="31536000" />
</bean>
Copy the code

Business code

User login:

session.setAttribute(Const.CURRENT_USER, response.getData());
Copy the code

When logging out:

session.removeAttribute(Const.CURRENT_USER);
Copy the code

When obtaining user information:

User user = (User) session.getAttribute(Const.CURRENT_USER);
Copy the code