精华内容
下载资源
问答
  • 单设备登录

    千次阅读 2018-01-15 11:53:56
    之前写过一篇单设备登录的小文章,写的不详细,所以在原来的基础上进行了修改。另外也加入了在修复此次bug的感悟与总结。希望自己能不断反思与进步,也希望能和大家互相讨论学习。 一、什么是单设备登录? 1....

    之前写过一篇单设备登录的小文章,写的不详细,所以在原来的基础上进行了修改。另外也加入了在修复此次bug的感悟与总结。希望自己能不断反思与进步,也希望能和大家互相讨论学习。

    一、什么是单设备登录?

    1.单设备登录概念   

        以QQ登录为例,当使用不同的手机(目前QQ支持pad与电脑、手机同时在线,但是相同类型的终端是不可以的)登录同一个QQ账号的时候,后登录的终端会把之前登录的终端给踢下线。也就是说同一账号不能在两台设备上同时登录使用。

    2.单设备登录与单点登录的区别

        单点登录(SSO),英文解释为Single  Sign  On。单点登录是在一家公司的服务不断增加的环境下,用户面临输入多次账户密码的窘境。例如,要登录豆瓣FM、豆瓣读书、豆瓣电影、豆瓣日记......多种豆瓣相关的服务,每次如果都需要输入一次账号密码,岂不是用户要疯掉?并且也存在安全问题。此时就引入了单点登录,也就是说多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。与单设备登录其实并没有多大关系。

    二、单设备登录实现原理

    1.Token标识

    数据库中保存有Token与User相关联的信息:

    关键信息:user_code ---> token_id --->old_token_id,注:oldTokenId存三个tokenId。

    2.tokenCache缓存

    缓存userCode ---> token_id 与old_token_id ---> token_id;缓存时效可配置,目前为5分钟。

    3.机制

    前提条件:

        用户登录完成后,每次发送的请求需要携带登录成功后所返回的tokenId与当前设备号,并且同一台设备的设备号不能发生更改。

    具体实现的两个关键点:

     

    A.登录操作:

        用户登录会更新userToken信息,主要是更新tokenId与oldTokenId字段。另外也需要更新user的设备号信息,并把此次的设备号信息以userCode ---> DSN的形式存入deviceCache缓存中。

        在更新数tokenId时,会把此次登录的tokenId以userCode ---> tokenId存入tokenCache缓存中。在缓存有效期内,多次登录操作不再更新数据库tokenId字段。

    B.UserLoginFilter过滤操作

        根据过滤到的请求的session中是否存在user信息分为两部分操作:

        1) user信息为空,需要判断用户是否满足长登录要求。

        根据此次请求所携带的tokenId从tokenCache中获取最新的newTokenId,若为空则生成一个并存入该缓存中,使得并发的请求都能得到同一个newTokenId。当然这部分代码放在了同步块当中。

        然后通过请求中所携带的tokenId查询对应的用户,若对应用户不存在,则用户需要重新登录。反之则直接登录此用户,并把之前生成的newTokenId更新到数据库UserToken表中。以备后续请求(携带newTokenId)能够在通过tokenId在数据库中获取user对象。

        2) user信息存在,判断是否存在多设备登录的情况,需要实现单设备登录的需求。根据此次请求所携带的设备号与缓存中的设备号(每次登录后会把当前设备号存入缓存,时效为30分钟[ 目前认为30分钟时间太短 ])进行对比,不一致则通知当前请求的设备被其他设备踢下线。若设备号的缓存时效,则从数据库中查询用户的设备号进行对比。

        至此,功能基本完成。

    三、单设备登录遇到的问题

    1.并发问题。

        若A用户同时发送多个请求,req1,req2,req3...他们做携带的tokenId一致。并且都执行到过滤器,由于并发的不确定性,无法保证数据库数据与缓存中的数据的一致性。即:req1判断当前请求中user为空,进行长登录操作,需要根据tokenId查询对应的user,完毕后会进行登录操作,登录又会更新缓存与数据库中的tokenId。若req1已经更新了数据库与缓存,此时req2也判断user为空需要进行长登录操作,也需要通过tokenId查询对应的用户,而此时req1已经更新了数据库,req2再通过该tokenId就会查询不到user,则该请求就会通知客户端需要重新登录的信息。就无法完成长登录的功能。

    2.集群问题

        在修复长登录与单设备登录问题的测试过程中,总是出现在本地测试一切正常,一旦把代码打包到178环境就会有各种各样的问题出现。原因就是178环境是集群部署,另一台服务器是165,打包时并没有考虑到165环境,并且ehCache也没有实现集群配置。导致代码在本地正常,放到178就有问题。

        这一度迫使我们加班到很晚也依然没有找到问题所在,因为静态代码分析各种情况都已经考虑的十分周全,理论上是不可能出现那些情况的。这也是当时最最最头疼的问题(虽然是低级错误)。

    四、另一种实现思路

    1.长登录

        针对并发,每当App启动的时候,发送请求checkToken.app,去校验该用户的token是否还是有效的。需要与缓存中的token进行比对,若有效就更新数据库token,并返回更新后的token,同时在缓存中存入该token值。若无效,则根据tokenId查询token对象。如果可以查询到,则更新token并返回新token,同时更新缓存数据。若查询为空,则当前用户的发送请求的token已经失效,需要其重新登录。

        登录操作,需要更新用户token并返回最新token给客户端,并同时刷新缓存记录。

        假如该用户此次校验token通过,则后续发送的请求都要携带最新的token值来进行访问。Filter中需要校验该token是否有效即可(与缓存中比对)。若失效,则需要用户重新登录。

        这种做法比较关键的问题是缓存有效时长。如果只设置5分钟,用户也一直用app操作的情况下,当缓存失效,就会被迫重新登录。所以把缓存失效设置为24h。在这24h内,用户的app关闭或切换到后台较长时间,就会触发checkToken机制,重新获取新的token值。

    2.单设备登录

        在以上实现机制的前提下,单设备登录自然就实现了。A账户在A设备上登录,同时B设备也登陆A账户。此时会更新用户的token并刷新缓存,A用户在访问时,Filter会校验token值,此时A设备中的token已经是失效的、不是最新的token值,就会被强制重新登录。反之亦然。

    3.存在问题

        客户端无法控制每次重新加载App时都要去发送checkToken.app的接口,因此无法保证token的正确性。但我认为这种方式最合理有效。

    五、反思与收获

    1.反思

         我认为导致此次问题的最大原因就是考虑问题不周全个人技能不足

        人非圣贤孰能无过,对于第一个问题是无法完全解决的,但是可以尽力避免。过不仅仅是过失,还有看待问题的个人局限性,无法把问题的方方面面都考虑其中。对于自己来说,就是对并发与框架的熟悉度不够。没有把并发问题纳入代码设计中来,也就直接导致了今天这个问题。痛定思痛,应该在今后的工作与学习中,更加努力与更加勤奋!

    2.收获

        此次收获还是十分丰富的。

        首先对并发有了新的认识。考虑并发就跟排列组合一样,各种各样的问题与可能性都要考虑到。特别是并发数增多,可能性更是指数级别的增长。以及对synchronized与violate的使用也有了自己的看法。对于接触并发很少的我是一个提高自我恰好的时机。再有就是对静态代码的逻辑分析能力,由于并发情况多种多样,需要自己设想各种情况,然后按照代码逻辑走下去,并考虑各种变量的值与当时服务器缓存情况。

        学如逆水行舟,不进则退。Learn playing, Play learning !

    展开全文
  • 单设备登录与单点登录(SSO)

    千次阅读 2017-05-22 18:30:45
    首先我觉得很有必要说明一下单设备登录与单点登录的含义。我在网上搜索此类消息发现很多文章都误解了这两者的意思。单点登录:英文Single Sign On,根据英文含义不难理解,即:单一标记(单点)登录。就是说,我只...

    首先我觉得很有必要说明一下单设备登录与单点登录的含义。我在网上搜索此类消息发现很多文章都误解了这两者的意思。

    **单点登录:**英文Single Sign On,根据英文含义不难理解,即:单一标记(单点)登录。就是说,我只需要登录一次。例如:QQ,我在QQ空间登录一次,我可以去访问QQ产品的其他服务:QQ邮箱、腾讯新闻等,都能保证你的账户保持登录状态。

    **单设备登录:**就是只能在一个设备上登录,若同时在其他设备登录,先前登录的用户会被提醒:该账户在其他设备登录。例如qq,小米手机登录中,同时拿华为手机登录该账户,小米手机的账户会被挤下线。

    说清楚了这两者的区别,那来讲下我的思路吧。第一次做这个功能,并且是做的Java web端的功能。还请各位大牛指点一二。小弟在此多谢了!

    思路1:
    1.添加Session监听器,把服务器产生的session,以sessionId与session作为映射关系保存在Map中。
    2.保存用户与对应的session,以userCode与sessionId为映射关系保存在服务器缓存(Cache)中。
    3.在用户登录时,根据用户code获取sessionId。
    A:如果sessionId为空,则说明该用户还没有登录过,把此次登录的userCode与对应的sessionId保存到缓存中;并存入一个表示信息,isInvalid=0。表明此session有效;
    B:否则,为第二次登录。然后把缓存中的上次登录保存的userCode及其对应的sessionId删除;再把此次登录状态信息保存到缓存中;
    C:通过sessionId查询到上次登录的session,改变session中isInvalid状态为1,表明失效状态。
    D:最后,过滤器中从session里面取出isInvalid字段,判断其是否有效。若有效则继续其他过滤;否则把isInvalid字段修改为2,表明这个session已经被处理过。然后通过转发,把此次请求转发给其他控制器处理。

    当我按照上述思路做完代码,进行测试的时候。竟然成功了,还是很激动的。但是不久后发现了一个**问题:**在我在A端登录账号后,然后再B端登录。接下来我不在进行A端的账户操作,等到其session失效,我再进行操作,这个时候,先前的session中所存放的isInvalid标识已经不存在了。这个时候,过滤器是检测不到信息的。所以会被后续的过滤进行保持登录的代码所覆盖,然后重新登录进去。这个时候A端与B端同时操作是互相不影响的。

    思路2:

    借鉴网友:在用户表那儿添加多一个INT列,这个专门放随机数,每登陆一次,随机数就变化一次,并将随机数放在cookies或session中,每每读取和数据库的作对比,如果发现不对,就说明,别人己在另一个地方登陆了,这时就弹出“己在另一地方登陆”,然后关闭窗口,这样就行了。

    这个思路我觉得可行,但是会频繁查询数据库,每一次请求都需要校验,然后就需要去与数据库里面的值进行对比校验。这个我觉得是可以避免的。

    **我的想法:**我觉得可以把用户与对应的随机码存放在服务器缓存中(userCode==>UUID),并且把UUID存入cookies里面,之后的请求就包含该UUID。第二次登录的时候,再往缓存中放入userCode与UUID。这样就会覆盖先前存放的数据。如此,第一个登录的用户,再携带它的UUID去访问时,过滤器就会拿该UUID与缓存中的UUID进行对比。如果不一致,则说明其他设备登录了该账户。就提醒这个用户账号在其他设备登录即可。

    我觉得这个方案是可行的,并且不用频繁的去操作数据库了,应该性能方面会有所提升。

    展开全文
  • 不管是客户端接口还是网页H5接口,一般我们都需要登录验证,即要求所有的接口访问都必须在登录之后,以确认身份,防止非法调用。一般的流程都是登录的时候返回一个代表此登录的token,以后所有接口都带上此token,在...

    详情参见: https://gitee.com/xxssyyyyssxx/token

    不管是客户端接口还是网页H5接口,一般我们都需要登录验证,即要求所有的接口访问都必须在登录之后,以确认身份,防止非法调用。一般的流程都是登录的时候返回一个代表此登录的token,以后所有接口都带上此token,在所有接口调用之前拦截验证,一般都是通过AOP或者一个Filter、拦截器来实现。而退出的时候调用接口将此token删除即可。一般地,为了对接口侵入最小,能做到统一处理,可以将此token放在header中。token一般都会设置一个有效期,过期了直接提示调用者需要登录以控制条转到登录页面引导登录。

    服务端设计:

    /**
     * token管理器
     * @author xiongshiyan
     */
    public interface TokenManager<M> {
        /**
         * 生成token
         * @param m 实体
         * @return token值
         */
        String createToken(M m);
    
        /**
         * 根据token获取
         * @param token token
         * @return 根据token获取的实体
         */
        M findByToken(String token);
    
        /**
         * 更新token的过期
         * @param token token
         */
        void updateExpires(String token);
    
        /**
         * 删除
         * @param token token
         * @return 删除是否成功
         */
        boolean deleteToken(String token);
    
        /**
         * 产生token
         * @param m 实体
         * @return token
         */
        String getToken(M m);
    
    
        /**
         * 踢人
         * @param m 实体
         * @param newToken 新token
         * @param doMore 还要做的事情
         */
        default void kickingOld(M m, String newToken, Runnable doMore){
            kickingOld(m , newToken);
    
            if(null != doMore){
                doMore.run();
            }
        }
    
        /**
         * 踢人
         * @param m 实体
         * @param newToken token
         */
        void kickingOld(M m, String newToken);
    }
    

    M代表登录实体,也可以是能代表登录人的唯一标识,使用泛型指定。createToken用于生成并保存token,findByToken用于通过token找到登录实体,updateExpires用于更新token的过期时间,deleteToken用于删除token(登录退出的时候),getToken生成token字符串。一般验证token是几乎每个接口都会用,所以必须保证速度,可以采用redis来保存。

    /**
     * 基于redis的token管理器基类
     * @author xiongshiyan at 2018/8/15 , contact me with email yanshixiong@126.com or phone 15208384257
     */
    public abstract class AbstractRedisTokenManager<M> implements TokenManager<M> {
        protected RedisUtil redisUtil;
    
        public AbstractRedisTokenManager(RedisUtil redisUtil){
            this.redisUtil = redisUtil;
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public M findByToken(String token) {
            if(null == token){
                return null;
            }
            Object o = redisUtil.get(token);
            if(null == o){
                return null;
            }
            return (M) o;
        }
    
        @Override
        public boolean deleteToken(String token){
            Object o = redisUtil.get(token);
            if(null == o){
                return false;
            }
            redisUtil.del(token);
            return true;
        }
    }
    

    此基类实现了一些公共的方法,继承此类实现剩余的方法即可。

    1.允许多设备登录的实现。这种情形下,token可以随意生成,只要保证不重复即可。

    2.只允许单设备登录,即所谓的登录踢人,在登录的时候验证是够已经登录,如果已经登录就给出提示或者直接踢人登录。这种情形下,需要根据登录的标识确认是否已经登录,有两种解决方式,一种是在保存token的时候,既保存token》》实体的关系,还需要保存实体标识与token的关系;另外一种解决方式是token与实体标识强相关,根据实体标识即可算出token。

    以下的实现既支持多设备登录又支持单设备登录,设置multi即可。

    package cn.palmte.anfang.service.token.impl;
    
    import cn.palmte.anfang.redis.RedisUtil;
    import cn.palmte.anfang.service.token.AbstractRedisTokenManager;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * API接口token管理器主要逻辑实现【支持多设备登录、踢人】
     * @author xiongshiyan at 2018/8/15 , contact me with email yanshixiong@126.com or phone 15208384257
     */
    @SuppressWarnings("unchecked")
    public abstract class BaseMultiTokenManager<M> extends AbstractRedisTokenManager<M> {
        private static final Logger logger = LoggerFactory.getLogger(BaseMultiTokenManager.class);
    
        private long apiExpires;
        /**
         * 多设备登录 ?
         */
        private boolean multi = false;
    
        public BaseMultiTokenManager(RedisUtil redisUtil, long apiExpires , boolean multi) {
            super(redisUtil);
            this.apiExpires = apiExpires;
            this.multi = multi;
        }
    
        @Override
        public String createToken(M m) {
            String token = getToken(m);
            redisUtil.set(token , m , apiExpires);
            logger.info("createToken token = {} , m={}" , token , m.toString());
            return token;
        }
    
        @Override
        public void updateExpires(String token){
            if(null == token){
                return;
            }
            redisUtil.expire(token , apiExpires);
            if(multi){
                return;
            }
    
            //单设备需要额外同步更新
            M m = (M) redisUtil.get(token);
            if(null != m){
                redisUtil.expire(key(m) , apiExpires);
            }
        }
    
        @Override
        public boolean deleteToken(String token) {
            //单设备需要额外删除
            M m = (M) redisUtil.get(token);
    
            boolean b = super.deleteToken(token);
            if(multi){
                return b;
            }
    
            String key = key(m);
            if(null != m){
                redisUtil.del(key);
            }
            logger.info("deleteToken token = {} , key={}" , token , key);
            return b;
        }
    
        @Override
        public void kickingOld(M m, String newToken) {
            if(multi){
                throw new IllegalStateException("多设备登录情况不允许踢人");
            }
            //1.删除以前登录人的token,以前的人就通不过校验
            String withPrefix = key(m);
            String oldToken = (String) redisUtil.get(withPrefix);
            if(null == oldToken){
                return;
            }
            deleteToken(oldToken);
    
            //2.重新建立实体和新token的联系
            //单设备需要额外保存标识和token的关系
            logger.info("kickingOld key={} , value={}" , withPrefix , newToken);
            redisUtil.set(withPrefix, newToken , apiExpires);
        }
    
        /**
         * 根据实体或者标识获取key
         * @param m 实体或者标识
         * @return 返回保存标识和token关系的key
         */
        abstract protected String key(M m);
    }
    

    直接使用实体比如手机号的

    /**
     * 客户端API接口token管理器【支持多设备登录】
     * @author xiongshiyan at 2018/8/15 , contact me with email yanshixiong@126.com or phone 15208384257
     */
    public class ApiTokenManager extends BaseMultiTokenManager<String> {
        private String apiTokenPrefix;
        public ApiTokenManager(RedisUtil redisUtil, String apiTokenPrefix, long apiExpires , boolean multi) {
            super(redisUtil, apiExpires, multi);
            this.apiTokenPrefix = apiTokenPrefix;
        }
        public ApiTokenManager(RedisUtil redisUtil, String apiTokenPrefix, long apiExpires) {
            super(redisUtil , apiExpires , false);
            this.apiTokenPrefix = apiTokenPrefix;
        }
    
        @Override
        public String getToken(String m){
            return apiTokenPrefix + "-" + nowStr() + CommonUtil.randomString(16);
        }
        private String nowStr(){
            return DatetimeUtils.toStr(new Date() , DatetimeUtils.SDF_DATETIME_SHORT);
        }
    
        @Override
        protected String key(String s) {
            return apiTokenPrefix + "-" + s;
        }
    }
    

    用实体的情况

    package cn.palmte.anfang.service.token.impl.model;
    
    import cn.palmte.anfang.model.Member;
    import cn.palmte.anfang.redis.RedisUtil;
    import cn.palmte.anfang.service.token.impl.BaseMultiTokenManager;
    import top.jfunc.common.datetime.DatetimeUtils;
    import top.jfunc.common.utils.CommonUtil;
    
    import java.util.Date;
    
    /**
     * 客户端API接口token管理器【支持多设备登录】
     * @author xiongshiyan at 2018/8/15 , contact me with email yanshixiong@126.com or phone 15208384257
     */
    public class ApiTokenManager extends BaseMultiTokenManager<Member> {
        private String apiTokenPrefix;
    
        public ApiTokenManager(RedisUtil redisUtil, String apiTokenPrefix, long apiExpires , boolean multi) {
            super(redisUtil, apiExpires, multi);
            this.apiTokenPrefix = apiTokenPrefix;
        }
        public ApiTokenManager(RedisUtil redisUtil, String apiTokenPrefix, long apiExpires) {
            super(redisUtil, apiExpires, false);
            this.apiTokenPrefix = apiTokenPrefix;
        }
    
        @Override
        public String getToken(Member m){
            return apiTokenPrefix + "-" + nowStr() + CommonUtil.randomString(16);
        }
        private String nowStr(){
            return DatetimeUtils.toStr(new Date() , DatetimeUtils.SDF_DATETIME_SHORT);
        }
    
        @Override
        protected String key(Member member){
            return apiTokenPrefix + "-" + member.getPhone();
        }
    }
    

     

    展开全文
  • 采用Websocket实现的单设备登录的SpringBoot的后端部分
  • 采用Websocket实现的单设备登录的react前端部分,采用了前后端分离的开发模式。
  • 一个简单的 SSO 单点登录 单设备登录 解决方案SSO英文全称Single Sign On,单点登录。SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。它包括可以将这次主要的登录映射到其他应用中...
  • 单设备登录思路

    2018-10-19 14:15:23
    此篇所说的但设备登录,不是点登录,不是点登录,不是点登录。 但设备登录是说如QQ一样的,在另外的设备登录QQ会导致前面登录的登录状态失效,被挤下线的功能。 由于楼主项目使用的是shiro的session管理。...
  • 首先介绍下,什么是单设备登录单设备登录可以理解为同一个应用某一时刻只允许单一用户使用处于登录状态。单设备登录可以类比QQ的踢出第二者登录模式,可以在一定的程度上保障账号的安全。 单设备登录的...
  • 概述 ...在登录的时候,在TOKEN中存了登录id ,然后再设置一个UUID存在TOKEN中,在登录表中在把这个UUID存起来,通过Filter进行过滤,然后实现类似单设备登录的效果。 过滤器 web.xml 中添加...
  • spring-session解决同一账户,单个设备登录 问题描述 项目是BS架构,在使用spring-session掌管的了session以后,很灵活的解决了分布的session处理,但是万恶的运营提出了万恶的需求,让同一账户只能在单个设备上...
  • 这种方式叫单设备登录,作用很明显,就是为了保护用户账号安全,今天我们不说手机APP,我们来说说PC Web网站如何简单快速实现这种效果。本篇文章重点是实现单设备登录,内容未涉及WebSocket + ...
  • Laravel 单设备登录

    2019-09-21 09:17:30
    前几天在 laracasts 看了laravel5.6的新功能视频 logoutOtherDevices 用于使在其他设备上处于活动状态的用户会话无效并“注销”,而不会使其当前设备上的会话无效。,今天抽空把它应用到了系统里,在这里记录下吧。 ...
  • java单设备登录

    千次阅读 2018-01-18 15:08:30
    2.当某个账号登陆后,此账号在其他设备登陆提示已经登陆,无法登陆。 正常的应用逻辑第一种应用较为广泛,因此此篇文章讨论一下第一种逻辑在spring mvc开发中一种较为简单的实现方式。 然而在没有长连接如WebSocket...
  • spring MVC单设备登录

    千次阅读 2017-06-23 11:21:38
    2.当某个账号登陆后,此账号在其他设备登陆提示已经登陆,无法登陆。 正常的应用逻辑第一种应用较为广泛,因此此篇文章讨论一下第一种逻辑在spring mvc开发中一种较为简单的实现方式。  然而在没有长连接如...
  • 1. 用拦截器控制登录 前后端不分离,使用session: 登录 登录时将用户信息存储至session中。 session.setAttribute("usserId",user.getId()); 然后在将userId和sessionId 进行绑定存储至redis(也可以使用别的...
  • 2.当该用户其他设备再次登录时,先去缓存中获取userCode对应的sessionId,如果为空,即为此用户是首次登录;如果不为空,则比较此次登录用户与缓存中保存的sessionId是否为同一个,若是,则是同一设备
  • 配合OAuth2进行单设备登录拦截

    千次阅读 2018-11-21 18:17:00
    要进行单设备登录,在其他地点登录后,本地的其他操作会被拦截返回登录界面。 原理就在于要在登录时在redis中存储Session,进行操作时要进行Session的比对。 具体实现,假设我们的OAuth 2的登录调用接口如下: ...
  • //解决多点登录问题 RBucket<Object> bucket = redisUtil.getBucket(ConfigConstant.SYS_IS_LOGIN + vip.getPhone()); bucket.set(token); 在登录的时候将生成的token,放到redis。使用固定值 + 手机号...
  • 思路: 1.在登录的controller层中先判断用户是否登录成功,...2.创建一个自定义拦截器SysInterceptor,使用每次请求登录都去拦截对比登录信息,如果第一次登录的用户和第二次登录的用户两个的登录信息一致并且sessi...
  • 实时通讯篇幅较长,请配合目录观看项目准备1. 实时通讯-准备1. 实时通讯-后端1.1 weixin-web新建服务weixin-netty(module-springboot... 实时通讯-前端2.1 优化登录2.2 前端项目编写web-socket.js2.3 index.html引...
  • 用拦截器控制登录 前后端不分离,使用session: 登录 登录时将用户信息存储至session中。 session.setAttribute("usserId",user.getId()); 然后在将userId和sessionId 进行绑定存储至redis(也可以使用别的全局...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,102
精华内容 440
关键字:

单设备登录