Retrofit实现持久化Cookie的三种方案

来源:互联网 发布:贝叶斯网络分析软件 编辑:程序博客网 时间:2024/03/30 00:01

在上一文中,我们提到retrofit 2.0中如何实现非持久化cookie的两种方案,但并未做过深的解释。
现在我们重点关注JavaNetCookieJar实现非持久化cookie背后的原理。

话不多说,步入正题。


非持久化Cookie实现分析

首先来看上文中提到的非持久化cookie的实现:

 public void setCookies(OkHttpClient.Builder builder) {        CookieManager cookieManager = new CookieManager();        cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);        builder.cookieJar(new JavaNetCookieJar(cookieManager));    }

现在我们就以这段代码为起点来研究为什么这几行代码就实现了非持久化cookie呢?不难先发现此处实现cookie的关键就是cookieJar(),我们就以该方法切入。
首先来看该方法的源码:

     //设置cookie处理器,如果没设置,则使用CookieJar.NO_COOKIES作为默认的处理器    public Builder cookieJar(CookieJar cookieJar) {      if (cookieJar == null) throw new NullPointerException("cookieJar == null");      this.cookieJar = cookieJar;      return this;    }

通过该方法我们知道这里的关键就在于CookieJar接口,来看看这个接口的定义:

public interface CookieJar { //默认的cookie处理器,不接受任何cookie  CookieJar NO_COOKIES = new CookieJar() {    @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {    }    @Override public List<Cookie> loadForRequest(HttpUrl url) {      return Collections.emptyList();    }  };   //根绝cookie处理策略,保存响应中的cookie  void saveFromResponse(HttpUrl url, List<Cookie> cookies);  //为请求添加cookie   List<Cookie> loadForRequest(HttpUrl url);}

该接口非常简单,提供了保存和加载cookie的两个方法loadForRequest(HttpUrl url)saveFromResponse(HttpUrl url,List<Cookie> cookies),这就意味这我们可以自行实现接口来实现对cookie管理。到这里想必各位可能已经在想“我们自行实现该接口来将cookie保存到本地,使用的时候再从本地读取,这样不久实现cookie持久化了么?”,这当然没问题,但我们先继续往下看。

除此之外,该接口中存在一个默认的实现NO_COOKIES。

接下来,我们来看看该接口的另外一个实现类JavaNetCookieJar,它本质上只是java.net.CookieHandler的代理类。同样,来看一下JavaNetCookieJar中的源码:

public final class JavaNetCookieJar implements CookieJar {  private final CookieHandler cookieHandler;  public JavaNetCookieJar(CookieHandler cookieHandler) {    this.cookieHandler = cookieHandler;  }  @Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {    if (cookieHandler != null) {      List<String> cookieStrings = new ArrayList<>();      for (Cookie cookie : cookies) {//遍历cookie集合        cookieStrings.add(cookie.toString());      }      Map<String, List<String>> multimap = Collections.singletonMap("Set-Cookie", cookieStrings);      try {      //具体的保存工作交给cookieHandler去处理        cookieHandler.put(url.uri(), multimap);      } catch (IOException e) {        Internal.logger.log(WARNING, "Saving cookies failed for " + url.resolve("/..."), e);      }    }  }  @Override public List<Cookie> loadForRequest(HttpUrl url) {    // The RI passes all headers. We don't have 'em, so we don't pass 'em!    Map<String, List<String>> headers = Collections.emptyMap();    Map<String, List<String>> cookieHeaders;    try {      //从cookieHandler中取出cookie集合。      cookieHeaders = cookieHandler.get(url.uri(), headers);    } catch (IOException e) {      Internal.logger.log(WARNING, "Loading cookies failed for " + url.resolve("/..."), e);      return Collections.emptyList();    }    List<Cookie> cookies = null;    for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {      String key = entry.getKey();      if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))          && !entry.getValue().isEmpty()) {        for (String header : entry.getValue()) {          if (cookies == null) cookies = new ArrayList<>();          cookies.addAll(decodeHeaderAsJavaNetCookies(url, header));        }      }    }    return cookies != null        ? Collections.unmodifiableList(cookies)        : Collections.<Cookie>emptyList();  }   //将请求Header转为OkHttp中HttpCookie  private List<Cookie> decodeHeaderAsJavaNetCookies(HttpUrl url, String header) {    List<Cookie> result = new ArrayList<>();    for (int pos = 0, limit = header.length(), pairEnd; pos < limit; pos = pairEnd + 1) {      //具体转换过程在此不做展示    }    return result;  }}

上面的代码非常简单,其核心无非在于通过CookieHandler实现对cookie的保存和取值。既然,真正的工作类是CookieHandler,那我们就重点关注CookieHandler.

public abstract class CookieHandler {    private static CookieHandler cookieHandler;    public synchronized static CookieHandler getDefault() {        SecurityManager sm = System.getSecurityManager();        if (sm != null) {            sm.checkPermission(SecurityConstants.GET_COOKIEHANDLER_PERMISSION);        }        return cookieHandler;    }    public synchronized static void setDefault(CookieHandler cHandler) {        SecurityManager sm = System.getSecurityManager();        if (sm != null) {            sm.checkPermission(SecurityConstants.SET_COOKIEHANDLER_PERMISSION);        }        cookieHandler = cHandler;    }    public abstract Map<String, List<String>>        get(URI uri, Map<String, List<String>> requestHeaders)        throws IOException;    public abstract void        put(URI uri, Map<String, List<String>> responseHeaders)        throws IOException;}

我们发现CookieHandler是个抽象类,其实现类就是我们上面在非持久化Cookie中用到的CookieManager,没办法,我们在此转移焦点至CookieManager:

public class CookieManager extends CookieHandler{    private CookiePolicy policyCallback;//cookie处理策略    private CookieStore cookieJar = null;//cookie存储    public CookieManager() {        this(null, null);    }    //在这里我们可以指定cookie存储方式及cookie处理策略    public CookieManager(CookieStore store,                         CookiePolicy cookiePolicy)    {        // use default cookie policy if not specify one        policyCallback = (cookiePolicy == null) ? CookiePolicy.ACCEPT_ORIGINAL_SERVER                                                : cookiePolicy;        // 如果没有指定持久化方式,则默认使用内存持久化。        if (store == null) {            cookieJar = new InMemoryCookieStore();        } else {            cookieJar = store;        }    }    //指定cookie处理策略    public void setCookiePolicy(CookiePolicy cookiePolicy) {        if (cookiePolicy != null) policyCallback = cookiePolicy;    }    public CookieStore getCookieStore() {        return cookieJar;    }    //获取cookie    public Map<String, List<String>>        get(URI uri, Map<String, List<String>> requestHeaders)        throws IOException    {       //... 省略多行代码        Map<String, List<String>> cookieMap =                        new java.util.HashMap<String, List<String>>();      //... 省略多行代码        for (HttpCookie cookie : cookieJar.get(uri)) {            //... 省略多行代码        }        // apply sort rule (RFC 2965 sec. 3.3.4)        List<String> cookieHeader = sortByPath(cookies);        cookieMap.put("Cookie", cookieHeader);        return Collections.unmodifiableMap(cookieMap);    }    //保存cookie    public void        put(URI uri, Map<String, List<String>> responseHeaders)        throws IOException    {        //... 省略多行代码    PlatformLogger logger = PlatformLogger.getLogger("java.net.CookieManager");        for (String headerKey : responseHeaders.keySet()) {            // RFC 2965 3.2.2, key must be 'Set-Cookie2'            // we also accept 'Set-Cookie' here for backward compatibility            if (headerKey == null                || !(headerKey.equalsIgnoreCase("Set-Cookie2")                     || headerKey.equalsIgnoreCase("Set-Cookie")                    )                )            {                continue;            }            for (String headerValue : responseHeaders.get(headerKey)) {                try {                    List<HttpCookie> cookies;                    try {                        cookies = HttpCookie.parse(headerValue);                    } catch (IllegalArgumentException e) {                        // Bogus header, make an empty list and log the error                        cookies = java.util.Collections.emptyList();                        if (logger.isLoggable(PlatformLogger.Level.SEVERE)) {                            logger.severe("Invalid cookie for " + uri + ": " + headerValue);                        }                    }                    for (HttpCookie cookie : cookies) {                        if (cookie.getPath() == null) {                            //... 省略多行代码                            cookie.setPath(path);                        }                        if (cookie.getDomain() == null) {                            //... 省略多行代码                            cookie.setDomain(host);                        }                        //... 省略多行代码                            if (shouldAcceptInternal(uri, cookie)) {                                cookieJar.add(uri, cookie);                            }                    }                } catch (IllegalArgumentException e) {                    // invalid set-cookie header string                    // no-op                }            }        }    }}

在CacheManager中,需要重点关注的便是policyCallback和cookieJar两个成员变量。分别来看看这二者做了什么?

cookie处理策略
cookie的处理策略由CookiePolicy接口确定,该接口中预置了三种处理策略:ACCEPT_ALL,ACCEPT_NONE及ACCEPT_SERVER,分别用于接受所有cookie,不接受cookie,只接受初始服务器的cookie。

cookie存储
cookie的存储则由CookieStore接口负责,该接口目前存在唯一的实现类InMemoryCookieStore,通过该实现类的名字也大概猜的出它将cookie存在内存中了,同样我们简单的浏览一下其源码:

class InMemoryCookieStore implements CookieStore {    private List<HttpCookie> cookieJar = null;    private Map<String, List<HttpCookie>> domainIndex = null;    private Map<URI, List<HttpCookie>> uriIndex = null;    // use ReentrantLock instead of syncronized for scalability    private ReentrantLock lock = null;    /**     * The default ctor     */    public InMemoryCookieStore() {        cookieJar = new ArrayList<HttpCookie>();        domainIndex = new HashMap<String, List<HttpCookie>>();        uriIndex = new HashMap<URI, List<HttpCookie>>();        lock = new ReentrantLock(false);    }   //保存cookie    public void add(URI uri, HttpCookie cookie) {        // pre-condition : argument can't be null        if (cookie == null) {            throw new NullPointerException("cookie is null");        }        lock.lock();        try {            // 首先移除已经存在的cookie            cookieJar.remove(cookie);            // 缓存时间不为0            if (cookie.getMaxAge() != 0) {                cookieJar.add(cookie);                // 保存到域索引中                if (cookie.getDomain() != null) {                    addIndex(domainIndex, cookie.getDomain(), cookie);                }                //保存到url索引中                if (uri != null) {                    // add it to uri index, too                    addIndex(uriIndex, getEffectiveURI(uri), cookie);                }            }        } finally {            lock.unlock();        }    }}

到现在我们已经弄明白非持久化Cookie的原理,这里用一张类图来概括一下:
此处输入图片的描述

从设计的来说,这里也充分体现了面向抽象编程,单一职责理念等。对这些设计原则不明白的可以看我以前写的文章。


持久化Cookie实现

自定义InDiskCookieStore实现持久化Cookie

通过上面的分析,我们最终知道非持久化Cookie的存储靠InMemoryCookieStore实现,除此之外,我们发现CookieManager中也可以指定另外的CookieStore的实现。这样看来,我们只需要自定义InDiskCookieStore实现CookieStore,并将其设置给CookieManager就可以实现Cookie的持久化了。接下来,我们仿照inMemoryCookieStore来实现自己的InDiskCookieStore的即可。

不难发现这里的重点在于如何持久化Cookie到本地。我们可以从响应头中读取set-cookie(可能有多个),然后拼接存储在相关的配置文件中,也可以直接序列化HttpCookie对象到本地。这里我们采用序列化的方式实现,对Java序列化和反序列化不熟的同学可以自行补充。

查看HttpCookie发现,该类并不支持序列化,哪该怎么办呢?最简单的方式是为该类创建辅助类DiskHttpCookie,用来实现序列化和反序列化的过程:

public class DiskHttpCookie implements Serializable {    private static final long serialVersionUID = -6370968478600008500L;    private transient final HttpCookie mCookie;    private transient HttpCookie mClientCookie;    public DiskHttpCookie(HttpCookie cookie) {        mCookie = cookie;    }    public HttpCookie getCookie() {        HttpCookie cookie = mCookie;        if (mClientCookie != null) {            cookie = mClientCookie;        }        return cookie;    }    private void writeObject(ObjectOutputStream out) throws IOException {        out.writeObject(mCookie.getName());        out.writeObject(mCookie.getValue());        out.writeObject(mCookie.getComment());        out.writeObject(mCookie.getCommentURL());        out.writeObject(mCookie.getDomain());        out.writeLong(mCookie.getMaxAge());        out.writeObject(mCookie.getPath());        out.writeObject(mCookie.getPortlist());        out.writeBoolean(mCookie.getSecure());        out.writeBoolean(mCookie.getDiscard());        out.writeInt(mCookie.getVersion());    }    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {        String name = (String) in.readObject();        String value = (String) in.readObject();        mClientCookie = new HttpCookie(name, value);        mClientCookie.setComment((String) in.readObject());        mClientCookie.setCommentURL((String) in.readObject());        mClientCookie.setDomain((String) in.readObject());        mClientCookie.setMaxAge(in.readLong());        mClientCookie.setPath((String) in.readObject());        mClientCookie.setPortlist((String) in.readObject());        mClientCookie.setSecure(in.readBoolean());        mClientCookie.setDiscard(in.readBoolean());        mClientCookie.setVersion(in.readInt());    }}

在DiskHttpCookie中,通过writeObject()和readObject()方法实现自定义序列化的过程。下面我们正式来看InDiskCookieStore的实现,此次我们参考了async-http-client的相关源码:

public class InDiskCookieStore implements CookieStore {    private static final String LOG_TAG = "InDiskCookieStore";    private static final String COOKIE_PREFS = "CookiePrefsFile";    private static final String COOKIE_NAME_PREFIX = "cookie_";    //在内存中,缓存cookie,加快访问速度    private final HashMap<String, ConcurrentHashMap<String, HttpCookie>> cookies;    //存储cookie到本地    private final SharedPreferences cookiePrefs;    private ReentrantLock mLock = null;    public InDiskCookieStore(Context context) {        cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);        mLock = new ReentrantLock(false);        cookies = new HashMap<>();        initMemoryCookie();    }    private void initMemoryCookie() {        // 加载本地cookie到内存中        Map<String, ?> prefsMap = cookiePrefs.getAll();        for (Map.Entry<String, ?> entry : prefsMap.entrySet()) {            if (((String) entry.getValue()) != null && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX)) {                String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");                for (String name : cookieNames) {                    String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);                    if (encodedCookie != null) {                        HttpCookie decodedCookie = decodeCookie(encodedCookie);                        if (decodedCookie != null) {                            if (!cookies.containsKey(entry.getKey()))                                cookies.put(entry.getKey(), new ConcurrentHashMap<String, HttpCookie>());                            cookies.get(entry.getKey()).put(name, decodedCookie);                        }                    }                }            }        }    }    @Override    public void add(URI uri, HttpCookie cookie) {        mLock.lock();        try {            String name = getCookieToken(uri, cookie);            if (!cookie.hasExpired()) {                if (!cookies.containsKey(uri.getHost()))                    cookies.put(uri.getHost(), new ConcurrentHashMap<String, HttpCookie>());                cookies.get(uri.getHost()).put(name, cookie);            } else {//清除过期cookie                if (cookies.containsKey(uri.toString()))                    cookies.get(uri.getHost()).remove(name);            }            // Save cookie into persistent store            SharedPreferences.Editor prefsWriter = cookiePrefs.edit();            prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));            prefsWriter.putString(COOKIE_NAME_PREFIX + name, encodeCookie(new DiskHttpCookie(cookie)));            prefsWriter.commit();        } finally {            mLock.unlock();        }    }    protected String getCookieToken(URI uri, HttpCookie cookie) {        return cookie.getName() + cookie.getDomain();    }    @Override    public List<HttpCookie> get(URI uri) {        mLock.lock();        ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();        if (cookies.containsKey(uri.getHost()))            ret.addAll(cookies.get(uri.getHost()).values());        mLock.unlock();        return ret;    }    @Override    public boolean removeAll() {        mLock.lock();        try {            SharedPreferences.Editor prefsWriter = cookiePrefs.edit();            prefsWriter.clear();            prefsWriter.commit();            cookies.clear();        } finally {            mLock.unlock();        }        return true;    }    @Override    public boolean remove(URI uri, HttpCookie cookie) {        mLock.lock();        try {            String name = getCookieToken(uri, cookie);            if (cookies.containsKey(uri.getHost()) && cookies.get(uri.getHost()).containsKey(name)) {                cookies.get(uri.getHost()).remove(name);                SharedPreferences.Editor prefsWriter = cookiePrefs.edit();                if (cookiePrefs.contains(COOKIE_NAME_PREFIX + name)) {                    prefsWriter.remove(COOKIE_NAME_PREFIX + name);                }                prefsWriter.putString(uri.getHost(), TextUtils.join(",", cookies.get(uri.getHost()).keySet()));                prefsWriter.commit();                return true;            } else {                return false;            }        } finally {            mLock.unlock();        }    }    @Override    public List<HttpCookie> getCookies() {        mLock.lock();        ArrayList<HttpCookie> ret = new ArrayList<HttpCookie>();        for (String key : cookies.keySet())            ret.addAll(cookies.get(key).values());        mLock.unlock();        return ret;    }    @Override    public List<URI> getURIs() {        mLock.lock();        ArrayList<URI> ret = new ArrayList<URI>();        for (String key : cookies.keySet())            try {                ret.add(new URI(key));            } catch (URISyntaxException e) {                e.printStackTrace();            }        mLock.unlock();        return ret;    }    //序列化DiskHttpCookie,并将其转为字符串    protected String encodeCookie(DiskHttpCookie cookie) {        if (cookie == null)            return null;        ByteArrayOutputStream os = new ByteArrayOutputStream();        try {            ObjectOutputStream outputStream = new ObjectOutputStream(os);            outputStream.writeObject(cookie);        } catch (IOException e) {            Log.d(LOG_TAG, "IOException in encodeCookie", e);            return null;        }        return byteArrayToHexString(os.toByteArray());    }    //通过cookie字符串反序列化为HttpCookie对象    protected HttpCookie decodeCookie(String cookieString) {        byte[] bytes = hexStringToByteArray(cookieString);        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);        HttpCookie cookie = null;        try {            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);            cookie = ((DiskHttpCookie) objectInputStream.readObject()).getCookie();        } catch (IOException e) {            Log.d(LOG_TAG, "IOException in decodeCookie", e);        } catch (ClassNotFoundException e) {            Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);        }        return cookie;    }    //字节转16进制    protected String byteArrayToHexString(byte[] bytes) {        StringBuilder sb = new StringBuilder(bytes.length * 2);        for (byte element : bytes) {            int v = element & 0xff;            if (v < 16) {                sb.append('0');            }            sb.append(Integer.toHexString(v));        }        return sb.toString().toUpperCase(Locale.US);    }    //16进制转字节    protected byte[] hexStringToByteArray(String hexString) {        int len = hexString.length();        byte[] data = new byte[len / 2];        for (int i = 0; i < len; i += 2) {            data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));        }        return data;    }}

此类一方面将cookie暂存在内存中,以便实现快速的访问,另一方面会将cookie序列化在本地。在每次创建InDiskCookieStore时,会用本地的cookie初始化到缓存中。

那么如何使用呢?其实用和我们使用JavaNetCookieJar非常相似:

 public void setCookies(OkHttpClient.Builder builder) {         CookieManager cookieManager = new java.net.CookieManager(new InDiskCookieStore(this),CookiePolicy.ACCEPT_ORIGINAL_SERVER);         builder.cookieJar(new JavaNetCookieJar(cookieManager));    }

通过这种方式,无需做其他的变化就实现了持久化Cookie。

自定义拦截器实现持久化Cookie

通过自定义拦截器实现Cookie持久化的思路我们已经在上一篇文章说过,就不多做解释了,直接来看代码即可:

首先定义响应拦截器,该拦截器实现从response获取set-cookie字段的值,并将其保存在本地。

public class SaveCookiesInterceptor implements Interceptor {    private static final String COOKIE_PREF = "cookies_prefs";    private Context mContext;    public SaveCookiesInterceptor(Context context) {        mContext = context;    }    @Override    public Response intercept(Chain chain) throws IOException {        Request request = chain.request();        Response response = chain.proceed(request);        //set-cookie可能为多个        if (!response.headers("set-cookie").isEmpty()) {            List<String> cookies = response.headers("set-cookie");            String cookie = encodeCookie(cookies);            saveCookie(request.url().toString(),request.url().host(),cookie);        }        return response;    }    //整合cookie为唯一字符串    private String encodeCookie(List<String> cookies) {        StringBuilder sb = new StringBuilder();        Set<String> set=new HashSet<>();        for (String cookie : cookies) {            String[] arr = cookie.split(";");            for (String s : arr) {                if(set.contains(s))continue;                set.add(s);            }        }        Iterator<String> ite = set.iterator();        while (ite.hasNext()) {            String cookie = ite.next();            sb.append(cookie).append(";");        }        int last = sb.lastIndexOf(";");        if (sb.length() - 1 == last) {            sb.deleteCharAt(last);        }        return sb.toString();    }    //保存cookie到本地,这里我们分别为该url和host设置相同的cookie,其中host可选    //这样能使得该cookie的应用范围更广    private void saveCookie(String url,String domain,String cookies) {        SharedPreferences sp = mContext.getSharedPreferences(COOKIE_PREF, Context.MODE_PRIVATE);        SharedPreferences.Editor editor = sp.edit();        if (TextUtils.isEmpty(url)) {            throw new NullPointerException("url is null.");        }else{            editor.putString(url, cookies);        }        if (!TextUtils.isEmpty(domain)) {            editor.putString(domain, cookies);        }        editor.apply();    }}

其次定义请求拦截器,如果该请求存在cookie,则为其添加到Header的Cookie中,代码如下:

public class AddCookiesInterceptor implements Interceptor {    private static final String COOKIE_PREF = "cookies_prefs";    private Context mContext;    public AddCookiesInterceptor(Context context) {        mContext = context;    }    @Override    public Response intercept(Chain chain) throws IOException {        Request request = chain.request();        Request.Builder builder = request.newBuilder();        String cookie = getCookie(request.url().toString(), request.url().host());        if (!TextUtils.isEmpty(cookie)) {            builder.addHeader("Cookie", cookie);        }        return chain.proceed(builder.build());    }    private String getCookie(String url, String domain) {        SharedPreferences sp = mContext.getSharedPreferences(COOKIE_PREF, Context.MODE_PRIVATE);        if (!TextUtils.isEmpty(url)&&sp.contains(url)&&!TextUtils.isEmpty(sp.getString(url,""))) {            return sp.getString(url, "");        }        if (!TextUtils.isEmpty(domain)&&sp.contains(domain) && !TextUtils.isEmpty(sp.getString(domain, ""))) {            return sp.getString(domain, "");        }        return null;    }}

最后我们将这两个拦截器设置到OkHttpClient即可:

 public void setCookies(OkHttpClient.Builder builder) {     builder.addInterceptor(new AddCookiesInterceptor(this));     builder.addInterceptor(new SaveCookiesInterceptor(this));   }

这样,我们也实现了Cookie的持久化,再次证明了OkHttp的拦截器机制是如此方便。
和上面的方案相比,该方案需要额外添加两个拦截器。对某些有代码洁癖的人来说,可能并不喜欢这种方案。

自定义CookieJar实现持久化Cookie

到现在我们已经实现了持久化Cookie的两种方案。自定义CookieJar来实现Cookie持久化和以上两种方案并无太大的区别,结合上一篇文章中通过CookieJar实现非持久化Cookie,你也可以很容易的搞定持久化Cookie,故不做详细的说明了。


总结

今天我们从头到位了解了Cookie持久化技术,并对不同的方案做了简单的说明,希望对各位有所帮助。

4 0
原创粉丝点击