Redis缓存实现单点登录

单点登录原理

实现简单的同域名下的单点登录,适合分布式系统下使用,因为在分布式系统中,用户的请求可能被不同的服务器处理,而不同的服务器自动生成的cookie–JSESSIONID是不一致的,不能通过这个cookie去定位用户的登录状态,因此需要一个自定义的cookie注入到用户浏览器,不管用户的请求被哪个服务器处理,均能通过该cookie定位用户的信息是否存在缓存中;用户首次访问网页时,需要验证登录,用户登录成功后,服务器会生成自定义的cookie注入用户浏览器,并将用户信息写入Redis缓存中,具有一定的有效期;用户再次访问时,通过cookie中存储的SessionID去Redis中查找用户信息,若用户信息存在,可直接访问网页,不需要再次登录。
原理分析时序图如下
Redis缓存实现单点登录_第1张图片

单点登录实现

  1. 引入jedis依赖到pom.xml中
<dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.6.0</version>
</dependency>
  1. 构建Redis连接池–JedisPool
public class RedisPool {
    //static --> 保证jedispool在Tomcat启动时加载
    private static JedisPool pool; //jedis连接池
    private static Integer maxTotal = Integer.parseInt(PropertiesUtil.getProperty("redis.max.total", "20")); //控制连接池与Redis Server最大的连接数
    private static Integer maxIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.max.idle", "10")); //连接池中最大的空闲的jedis实例,想用的时候可以立刻使用
    private static Integer minIdle = Integer.parseInt(PropertiesUtil.getProperty("redis.min.idle", "2")); //连接池中最小的空闲的jedis实例,想用的时候可以立刻使用
    private static Boolean testOnBorrow = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.borrow", "true")); //在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值为true,则得到的jedis实例肯定是可以用的。
    private static Boolean testOnReturn = Boolean.parseBoolean(PropertiesUtil.getProperty("redis.test.return", "true")); //在return一个jedis实例的时候,是否要进行验证操作,如果赋值为true,则放回jedispool的jedis实例肯定是可以用的。

    private static String redisIp = PropertiesUtil.getProperty("redis.ip");//ip地址
    private static Integer redisPort = Integer.parseInt(PropertiesUtil.getProperty("redis.port"));//端口

    private static void initPool() {
        JedisPoolConfig config = new JedisPoolConfig();

        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);

        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnReturn(testOnReturn);

        config.setBlockWhenExhausted(true); //连接耗尽时是否阻塞,false会抛出异常,true阻塞直到超时。默认值为true

        pool = new JedisPool(config, redisIp, redisPort, 1000*2);
    }


    // JVM 加载的时候就初始化jedispool
    static {
        initPool();
    }

    //从连接池获取一个jedis
    public static Jedis getJedis() {
        return pool.getResource();
    }

    //放回一个坏的jedis连接
    public static void returnBrokenResource(Jedis jedis) {
        pool.returnBrokenResource(jedis);
    }

    //将一个jedis放回连接池
    public static void returnResource(Jedis jedis) {
        pool.returnResource(jedis);
    }
	//测试
    public static void main(String[] args) {
        Jedis jedis = pool.getResource();
        jedis.set("dazoukey", "dazouvalue");
        returnResource(jedis);
        pool.destroy();//临时调用,销毁连接池中的所有连接池;
        System.out.println("program is end");
    }
}
  1. 封装jedis api
public class RedisPoolUtil {

    //设置key的有效期,单位是秒
    //封装expire
    public static Long expire(String key, int exTime) {
        Jedis jedis = null;
        Long result = null;
        try {
            jedis = RedisPool.getJedis();//从jedispool中获取一个jedis连接
            result = jedis.expire(key, exTime);
        } catch (Exception e) {
            log.error("expire key:{} error", key, e);
            RedisPool.returnBrokenResource(jedis);//获取jedis连接出错时,放回一个坏的连接
            return result;
        }
        RedisPool.returnResource(jedis);//获取jedis成功并使用完毕后,放回一个好的连接
        return result;
    }

    //exTime的单位是秒
    //封装setex
    public static String setEx(String key, int exTime, String value) {
        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.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
	//封装set
    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.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
	//封装get
    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("set key:{} error", key, e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
	//封装del
    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.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return result;
    }
	//测试
    public static void main(String[] args) {
        Jedis jedis = RedisPool.getJedis();

        RedisPoolUtil.set("keyTest", "value");

        String value = RedisPoolUtil.get("keyex");

        RedisPoolUtil.setEx("keyex", 60*10, "valueex");

        RedisPoolUtil.expire("keyTest", 60*20);

        RedisPoolUtil.del("keyTest");

        System.out.println("end") ;
    }
}
  1. cookie生成工具
public class CookieUtil {

    private final static String COOKIE_DOMAIN = ".happymmall.com";//cookie域名
    private final static String COOKIE_NAME = "mmall_login_token";//cookie名
	//从HttpServletRequest中读取名为mmall_login_token的cookie
    public static String readLoginToken(HttpServletRequest request){
        Cookie[] cks = request.getCookies();//获取cookie数组
        if(cks != null){
            for(Cookie ck : cks){
                log.info("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
                    log.info("return cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    return ck.getValue();
                }
            }
        }
        return null;
    }
	//读取cookie的域名规则
    //X:domain=".happymmall.com"
    //a:A.happymmall.com            cookie:domain=A.happymmall.com;path="/"
    //b:B.happymmall.com            cookie:domain=B.happymmall.com;path="/"
    //c:A.happymmall.com/test/cc    cookie:domain=A.happymmall.com;path="/test/cc"
    //d:A.happymmall.com/test/dd    cookie:domain=A.happymmall.com;path="/test/dd"
    //e:A.happymmall.com/test       cookie:domain=A.happymmall.com;path="/test"
// c,d能读取e的cookie,但是c,d之间的cookie不能相互读取,a,b也不能互相读取,但是都可以读取X的

    //往HttpServletResponse中写入cookie,名字为mmall_login_token,域名为.happymmall.com,value值为token--采用SessionID
    public static void writeLoginToken(HttpServletResponse response,String token){
        Cookie ck = new Cookie(COOKIE_NAME,token);
        ck.setDomain(COOKIE_DOMAIN);
        ck.setPath("/");//代表设置在根目录
        ck.setHttpOnly(true);
        //单位是秒。
        //如果这个maxage不设置的话,cookie就不会写入硬盘,而是写在内存。只在当前页面有效。
        ck.setMaxAge(60 * 60 * 24 * 365);//如果是-1,代表永久
        log.info("write cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
        response.addCookie(ck);
    }
	//删除名为mmall_login_token的cookie
    public static void delLoginToken(HttpServletRequest request,HttpServletResponse response){
        Cookie[] cks = request.getCookies();
        if(cks != null){
            for(Cookie ck : cks){
                if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
                    ck.setDomain(COOKIE_DOMAIN);
                    ck.setPath("/");
                    ck.setMaxAge(0);//设置成0,代表删除此cookie。
                    log.info("del cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    response.addCookie(ck);
                    return;
                }
            }
        }
    }
}
  1. 登录代码
    登录的时候往浏览器中注入自定义的cookie,我这里定义的是注入名为mmall_login_token,value值为当前的SessionID;然后再根据这个SessionID往Redis缓存中写入键值为SessionID,value值为当前登录用户的User对象的序列化结果;
 public ServiceResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse) {
        ServiceResponse<User> response = iUserService.login(username, password);
        //登录成功后,在session中加入该用户
        if (response.isSuccess()) {
            //在浏览器中写入cookie,键值为mmall_login_token,value值为sessionId
            CookieUtil.writeLoginToken(httpServletResponse, session.getId());
            //在Redis中写入用户信息,并将User对象序列化为字符串形式写入Redis,获取用户时再反序列化成User对象:其中键值为sessionID,value值为User对象序列化结果
            RedisPoolUtil.setEx(session.getId(), Const.RedisCacheExtime.REDIS_SESSION_EXTIME, JsonUtil.obj2String(response.getData()));
        }
        return response;
    }
  1. 查询用户信息代码
    查询用户信息时,根据请求时的httpServletRequest中获取cookie,根据名为:mmall_login_token的键值获取对应Cookie的value值,获得用户登录时与cookie绑定的SessionId;然后根据这个SessionID去Redis缓存中查找是否有相应的用户信息,以此判断用户的登录状态。
 public ServiceResponse<User> getUserInfo(HttpServletRequest httpServletRequest) {
        //从httpServletRequest中获取Token Cookie值,根据名为:mmall_login_token的键值获取对应Cookie的value值,获得SessionId
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        //查看浏览器中是否有cookie,没有的话就是用户还没有登录
        if (StringUtils.isEmpty(loginToken)) {
            return ServiceResponse.createByErrorMessage("用户未登录,无法获取当前用户信息");
        }
        //根据读取的SessionId从redis中获取cookie对应的值,即用户信息
        String userJsonStr = RedisPoolUtil.get(loginToken);
        //通过反序列化将Redis中获取的用户信息字符串转化为User对象
        User user = JsonUtil.string2Obj(userJsonStr, User.class);
        if (user != null) {//用户信息存在,说明该用户已经登录了
            return ServiceResponse.createBySuccess(user);
        }
        return ServiceResponse.createByErrorMessage("用户未登录,无法获取当前用户信息");
    }
  1. 用户退出代码
    用户退出时,需要从两个方面退出,一个是要将浏览器中的cookie删除,一个是要将Redis中的用户信息删除
public ServiceResponse<String> logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        //删除session
        //从httpServletRequest中获取Token Cookie值,根据名为:mmall_login_token的键值获取对应Cookie
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        //删除浏览器中的cookie
        CookieUtil.delLoginToken(httpServletRequest, httpServletResponse);
        //在Redis中删除退出用户信息
        RedisPoolUtil.del(loginToken);
        return ServiceResponse.createBySuccess();
 }
  1. 重置Session有效期
    配置拦截器,当用户访问网站中的网页时,拦截器就会拦截请求,对Redis中的Session有效期进行重置;

当我们在登录状态的时候,如果我们再访问同一个域名下的网页时,需要将Session有效期重置;要不然我们需要一个长时间登录状态访问时,当Session有效期到了,就又需要重新登录,影响用户体验。

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 = (HttpServletRequest)servletRequest;
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);
        //判断logintoken是否为空或者“”;如果不为空的话,符合条件,继续拿User信息
        if (StringUtils.isNotEmpty(loginToken)) {
            String userJsonStr = RedisPoolUtil.get(loginToken);
            User user = JsonUtil.string2Obj(userJsonStr, User.class);
            //判断User是否为空,如果user不为空,则重置session的时间,即expire命令
            if (user != null) {
                RedisPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {

    }
}

在web.xml中添加filter拦截器
<!-- 使用自定义的session共享方法,如果用户重新使用URL,就重置expire时间-->
	<filter>
        <filter-name>sessionExpireFilter</filter-name>
        <filter-class>com.mmall.controller.common.SessionExpireFilter</filter-class>
    </filter>
    <!-- 拦截后缀为.do的请求 -->
    <filter-mapping>
        <filter-name>sessionExpireFilter</filter-name>
        <url-pattern>*.do</url-pattern>
    </filter-mapping>

你可能感兴趣的:(Redis)