不管是客户端接口还是网页H5接口,一般我们都需要登录验证,即要求所有的接口访问都必须在登录之后,以确认身份,防止非法调用。一般的流程都是登录的时候返回一个代表此登录的token,以后所有接口都带上此token,在所有接口调用之前拦截验证,一般都是通过AOP或者一个Filter、拦截器来实现。而退出的时候调用接口将此token删除即可。一般地,为了对接口侵入最小,能做到统一处理,可以将此token放在header中。token一般都会设置一个有效期,过期了直接提示调用者需要登录以控制条转到登录页面引导登录。
服务端设计:
/**
* token管理器
* @author xiongshiyan
*/
public interface TokenManager {
/**
* 生成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 [email protected] or phone 15208384257
*/
public abstract class AbstractRedisTokenManager implements TokenManager {
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 [email protected] or phone 15208384257
*/
@SuppressWarnings("unchecked")
public abstract class BaseMultiTokenManager extends AbstractRedisTokenManager {
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 [email protected] or phone 15208384257
*/
public class ApiTokenManager extends BaseMultiTokenManager {
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 [email protected] or phone 15208384257
*/
public class ApiTokenManager extends BaseMultiTokenManager {
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();
}
}