1. 程式人生 > >Redis+Cookie+Jackson+Filter實現單點登入

Redis+Cookie+Jackson+Filter實現單點登入

1.Redis連線池構建

首先maven匯入依賴包

    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.6.0</version>
    </dependency>

下面正式寫一個連線池

public class RedisPool {
    private static JedisPool pool;//jedis連線池
private static Integer maxTotal = Integer.valueOf(PropertiesUtil.getProperty("redis.max.total","20")); //最大連線數 private static Integer maxIdle = Integer.valueOf(PropertiesUtil.getProperty("redis.max.idle","10"));//在jedispool中最大的idle狀態(空閒的)的jedis例項的個數 private static Integer minIdle = Integer.valueOf(PropertiesUtil.getProperty("redis.min.idle","2"));//
在jedispool中最小的idle狀態(空閒的)的jedis例項的個數 private static Boolean testOnBorrow = Boolean.valueOf(PropertiesUtil.getProperty("redis.test.borrow","true"));//在borrow一個Jedis例項的時候,是否要進行驗證操作,如果賦值true,那麼得到的jedis例項肯定是可用的 private static Boolean testOnReturn = Boolean.valueOf(PropertiesUtil.getProperty("redis.test.return","true"));//
在borrow一個Jedis例項的時候,是否要進行驗證操作,如果賦值true,則放回jedispool的jedis例項肯定是可用的 private static String redisIp = PropertiesUtil.getProperty("redis.ip");//在jedispool中最大的idle狀態(空閒的)的jedis例項的個數 private static Integer redisPort = Integer.valueOf(PropertiesUtil.getProperty("redis.port"));//在jedispool中最小的idle狀態(空閒的)的jedis例項的個數 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);//連線耗盡的時候是否阻塞,true:阻塞,false:丟擲異常 pool = new JedisPool(config,redisIp,redisPort,1000*2); } static { initPool(); } public static Jedis getJedis(){ return pool.getResource(); } public static void returnBrokenResource(Jedis jedis){ pool.returnBrokenResource(jedis); } public static void returnResource(Jedis jedis){ pool.returnResource(jedis); } public static void main (String[] args){ //Jedis jedis = pool.getResource(); Jedis jedis = RedisPool.getJedis(); jedis.set("key","value"); RedisPool.returnResource(jedis); pool.destroy();//臨時呼叫 System.out.println("program is end"); } }

程式碼內的PropertiesUtil是一個讀取properties的工具類

public class PropertiesUtil {

    private static Logger logger = LoggerFactory.getLogger(PropertiesUtil.class);

    private static Properties props;

    static {
        String fileName = "zfb.properties";
        props = new Properties();
        try {
            props.load(new InputStreamReader(PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName),"UTF-8"));
        } catch (IOException e) {
            logger.error("配置檔案讀取異常",e);
        }
    }

    public static String getProperty(String key){
        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            return null;
        }
        return value.trim();
    }

    public static String getProperty(String key,String defaultValue){

        String value = props.getProperty(key.trim());
        if(StringUtils.isBlank(value)){
            value = defaultValue;
        }
        return value.trim();
    }



}

與redis的連線搞定了,那接下來就要封裝一下操作redis的方法了

2.Jedis API封裝

@Slf4j
public class RedisPoolUtil {

    /*設定key的有效期,單位是秒*/
    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:{} exTime:{}  error",key,exTime,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return  result;

    }


    //exTime的單位是秒
    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:{} exTime:{} value:{} error",key,exTime,value,e);
            RedisPool.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(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.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(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.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(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.returnBrokenResource(jedis);
            return result;
        }
        RedisPool.returnResource(jedis);
        return  result;

    }

    public  static  void main (String[] args){
        Jedis jedis = RedisPool.getJedis();
        RedisShardedPoolUtil.set("keyTest","value");
        String value = RedisShardedPoolUtil.get("keyTest");
        RedisShardedPoolUtil.setEx("kevex","valueex",60*10);
        RedisShardedPoolUtil.expire("keyTest",60*20);
        RedisShardedPoolUtil.del("keyTest");
        System.out.println("end");
    }




}

類裡面封裝了一些常用的操作redis的方法,set(),設定鍵值;del(),刪除鍵值;setEx(),set的時候就把生存時間設定了等等

3.JSONUtil封裝

RedisPoolUtil 工具類是對String操作的,但是我們在登入的時候,使用者User是一個物件,裡面有具體的引數,那麼我們就需要把這個物件User序列化,然後儲存到redis裡面

    <dependency>
      <groupId>org.codehaus.jackson</groupId>
      <artifactId>jackson-mapper-asl</artifactId>
      <version>1.9.12</version>
    </dependency>
public class JsonUtil {
    private static ObjectMapper objectMapper = new ObjectMapper();
    static {
        //物件的所有欄位全部列入
        objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.ALWAYS);
        //取消預設轉換timestamps形式
        objectMapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS,false);
        //忽略空bean轉json的錯誤
        objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS,false);
        //所有日期格式都統一為一下格式,即yyyy-MM--dd HH:mm:ss
        objectMapper.setDateFormat(new SimpleDateFormat(DateTimeUtil.STANDARD_FORMAT));

        //忽略 在json字串中存在,但在Java物件中不存在對應屬性的情況,防止錯誤
        objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,false);

    }

    public static <T> String obj2String(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 obj2StringPretty(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 string2Obj(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 (IOException e) {
            log.warn("Parse String to Object error",e);
            return  null;
        }
    }

    public static <T> T string2Obj(String str, Class<?> collectionClass,Class<?>... elementClasses){
        JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass,elementClasses);
        try {
            return objectMapper.readValue(str,javaType);
        } catch (IOException e) {
            log.warn("Parse String to Object error",e);
            return  null;
        }
    }

    public static <T> T string2Obj(String str, TypeReference<T> typeReference){
        if (StringUtils.isEmpty(str)|| typeReference == null){
            return  null;
        }
        try {
            return (T)( typeReference.getType().equals(String.class)?str : objectMapper.readValue(str,typeReference));
        } catch (IOException e) {
            log.warn("Parse String to Object error",e);
            return  null;
        }
    }

    public static  void main (String[] args){
        User u1 = new User();
        u1.setId(1);
        u1.setEmail("[email protected]");

        User u2 = new User();
        u2.setId(2);
        u2.setEmail("[email protected]");

        String user1Json = JsonUtil.obj2String(u1);
        String user1JsonPretty = JsonUtil.obj2StringPretty(u1);

        log.info("user1Json:{}",user1Json);
        log.info("user1JsonPretty:{}",user1JsonPretty);

        User user = JsonUtil.string2Obj(user1Json,User.class);

        List<User> userList = Lists.newArrayList();
        userList.add(u1);
        userList.add(u2);

        String userListStr = JsonUtil.obj2StringPretty(userList);
        log.info("==================");
        log.info(userListStr);


        List<User> userListObj1 = JsonUtil.string2Obj(userListStr, new TypeReference<List<User>>() {
        });

        List<User> userListObj2 = JsonUtil.string2Obj(userListStr,List.class,User.class);

        System.out.println("end");
    }


}

準備工作做完以後,就是使用redis來儲存session了

4.單點登入Redis儲存Session

以下程式碼是以單機多部署兩個tomcat叢集為基礎的,如果只是一個tomcat,那麼執行程式碼

  RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(serverResponse.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME);

瀏覽器端cookie的value值和redis儲存的key值肯定是一樣的。

如果是兩個tomcat,使用tomcat1登入,那麼Cookie有了值,且儲存到redis上了,再次發起請求,負載均衡到tomcat2上了,那麼這個時候session.getId()得到的值在redis上是沒有這個key,就會認為使用者沒有登入

    @RequestMapping(value = "login.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse){
        ServerResponse<User> serverResponse = iUserService.login(username,password);
        if (serverResponse.isSuccess()){
           // session.setAttribute(Const.CURRENT_USER,serverResponse.getData());
            RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(serverResponse.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
        }
        return serverResponse;
    }

那麼我們就寫一個Cookie的工具類

@Slf4j
public class CookieUtil {

    private final static String COOKIE_DOMAIN = ".XXX.com";
    private final static String COOKIE_NAME = "study_login_token";


    public static String readLoginToken(HttpServletRequest request){
        Cookie[] cks = request.getCookies();
        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("read cookieName:{},cookieValue:{}",ck.getName(),ck.getValue());
                    return ck.getValue();
                }
            }
        }
        return null;
    }





    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);
    }

    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;
                }
            }
        }
    }

}

通過這個類,我們登入的時候就從服務端給客戶端種上了一個cookie,只要我們是在.XXX.com這個一級域名下進行操作,就能拿到這個cookie

    @RequestMapping(value = "login.do",method = RequestMethod.POST)
    @ResponseBody
    public ServerResponse<User> login(String username, String password, HttpSession session, HttpServletResponse httpServletResponse){
        ServerResponse<User> serverResponse = iUserService.login(username,password);
        if (serverResponse.isSuccess()){
           // session.setAttribute(Const.CURRENT_USER,serverResponse.getData());

       //登入的時候我們就把cookie種上返回給客戶端,那麼我們校驗是否登入的時候就readLoginToken
       //就是說我們現在不管你走哪個tomcat,cookie是一樣的 CookieUtil.writeLoginToken(httpServletResponse,session.getId()); RedisShardedPoolUtil.setEx(session.getId(), JsonUtil.obj2String(serverResponse.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME); } return serverResponse; }

校驗是否登入

    @RequestMapping("cancel.do")
    @ResponseBody
    public ServerResponse cancel(HttpServletRequest request, Long orderNo){
        //User user = (User) session.getAttribute(Const.CURRENT_USER);
        String loginToken = CookieUtil.readLoginToken(request);
        if(StringUtils.isEmpty(loginToken)){
            return ServerResponse.createByErrorMessage("使用者未登入,無法獲取當前使用者的資訊");
        }
        String userJsonStr = RedisShardedPoolUtil.get(loginToken);
        User user = JsonUtil.string2Obj(userJsonStr,User.class);
        if (user == null){
            return ServerResponse.createByErrorCode(ResponseCode.NEED_LOGIN.getCode(),ResponseCode.NEED_LOGIN.getDesc());
        }
        return iOrderService.cancle(user.getId(),orderNo);
    }

tomcat1和tomcat2還是會產生兩個不同的Cookie,但同時我們又自己增加了name為study_login_token的cookie,所以我們只需要讀取這個name,就可以判斷使用者登入狀態

但現在又有了一個問題,我們儲存在redis裡面的“session”,一次登入以後有效期是30分鐘,我們後面再執行其他操作,這個session的有效期是不會更新的,就是說一次登入只能玩30分鐘,所以我們就用過濾器來解決這個問題

5.過濾器Filter重置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) servletRequest;
        String loginToken = CookieUtil.readLoginToken(httpServletRequest);

        if (StringUtils.isNotEmpty(loginToken)){
            //判斷logintoken是否為空或者""
            //如果不為空,符合條件,繼續拿user資訊

            String userJsonStr = RedisShardedPoolUtil.get(loginToken);
            User user = JsonUtil.string2Obj(userJsonStr,User.class);
            if (user != null){
                //如果user不為空,則重置session的時間,即呼叫expire命令
                RedisShardedPoolUtil.expire(loginToken, Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
            }
        }
        filterChain.doFilter(servletRequest,servletResponse);
    }

    @Override
    public void destroy() {
    }
}

然後進入web.xml配置上這個過濾器

<filter>
        <filter-name>sesssionExpireFilter</filter-name>
        <filter-class>com.study.controller.commom.SessionExpireFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>sesssionExpireFilter</filter-name>
        <url-pattern>*.do</url-pattern>
    </filter-mapping>

 

現在單點登入就到這裡了