admin管理员组

文章数量:1532179

详情参见: https://gitee/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 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 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 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.jfuncmon.datetime.DatetimeUtils;
import top.jfuncmon.utils.CommonUtil;

import java.util.Date;

/**
 * 客户端API接口token管理器【支持多设备登录】
 * @author xiongshiyan at 2018/8/15 , contact me with email yanshixiong@126 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();
    }
}

 

本文标签: 设备Token