All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.gdface.facelog.TokenMangement Maven / Gradle / Ivy

There is a newer version: 5.3.0
Show newest version
package net.gdface.facelog;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static net.gdface.facelog.FeatureConfig.*;
import static net.gdface.facelog.db.Constant.*;

import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;

import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Predicate;
import com.google.common.base.Strings;
import com.google.common.primitives.Bytes;

import gu.simplemq.redis.JedisPoolLazy;
import gu.simplemq.redis.JedisUtils;
import gu.simplemq.redis.RedisFactory;
import gu.simplemq.redis.RedisTable;
import gu.sql2java.exception.ObjectRetrievalException;
import gu.sql2java.exception.RuntimeDaoException;
import net.gdface.facelog.db.DeviceBean;
import net.gdface.facelog.db.PersonBean;
import net.gdface.facelog.ServiceSecurityException.SecurityExceptionType;
import net.gdface.facelog.Token.TokenType;
import net.gdface.utils.BinaryUtils;

/**
 * 令牌管理模块
 * @author guyadong
 *
 */
class TokenMangement implements ServiceConstant {
	/**
	 * 由设备填写的常量字段
	 */
	private static final int[] CONST_FIELDS = {
			FL_DEVICE_ID_PRODUCT_NAME,
			FL_DEVICE_ID_MODEL,
			FL_DEVICE_ID_VENDOR,
			FL_DEVICE_ID_MANUFACTURER,
			FL_DEVICE_ID_MADE_DATE,
			FL_DEVICE_ID_VERSION,
			FL_DEVICE_ID_USED_SDKS,
			FL_DEVICE_ID_SERIAL_NO,
			FL_DEVICE_ID_EXT_BIN,
			FL_DEVICE_ID_EXT_TXT};
	private static final String ACK_PREFIX = "ack_";
	private final DaoManagement dao;
	private final CryptographGenerator cg;
	/**  {@code 设备ID -> token} 映射表 */
	private final RedisTable deviceTokenTable;
	/**  {@code 人员ID -> token} 映射表 */
	private final RedisTable personTokenTable;
	/** {@code cmd sn -> 人员ID} */
	private RedisTable cmdSnTable;
	/** {@code channel -> 人员ID} */
	private RedisTable ackChannelTable;
	/** 是否执行设备令牌验证 */
	private final boolean validateDeviceToken;
	/** 是否执行人员令牌验证 */
	private final boolean validatePersonToken;
	/** 人员令牌失效时间(分钟) */
	private final int personTokenExpire;
	/** 是否拒绝普通人员申请令牌 */
	private final boolean rejectZero;

	/**
	 * @param dao
	 */
	TokenMangement(DaoManagement dao) {
		this.dao = checkNotNull(dao,"dao is null");
		this.cg = this.dao.getCryptographGenerator();
		this.rejectZero = CONFIG.getBoolean(TOKEN_PERSON_REJECTZERO);
		this.validateDeviceToken = CONFIG.getBoolean(TOKEN_DEVICE_VALIDATE);
		this.validatePersonToken = CONFIG.getBoolean(TOKEN_PERSON_VALIDATE);
		this.personTokenExpire =CONFIG.getInt(TOKEN_PERSON_EXPIRE);
		this.deviceTokenTable =  RedisFactory.getTable(TABLE_DEVICE_TOKEN, JedisPoolLazy.getDefaultInstance());
		this.personTokenTable =  RedisFactory.getTable(TABLE_PERSON_TOKEN, JedisPoolLazy.getDefaultInstance());
		this.deviceTokenTable.setKeyHelper(Token.KEY_HELPER);
		this.personTokenTable.setKeyHelper(Token.KEY_HELPER);
		this.personTokenTable.setExpire(personTokenExpire, TimeUnit.MINUTES);
		this.cmdSnTable =  RedisFactory.getTable(TABLE_CMD_SN, JedisPoolLazy.getDefaultInstance());
		this.ackChannelTable =  RedisFactory.getTable(TABLE_ACK_CHANNEL, JedisPoolLazy.getDefaultInstance());
		this.cmdSnTable.setExpire(CONFIG.getInt(TOKEN_CMD_SERIALNO_EXPIRE), TimeUnit.SECONDS);
		this.ackChannelTable.setExpire(CONFIG.getInt(TOKEN_CMD_ACKCHANNEL_EXPIRE), TimeUnit.SECONDS);
		GlobalConfig.logTokenParameters();
	}
	/** 验证MAC地址是否有效(HEX格式,12字符,无分隔符,不区分大小写) */
	protected static final boolean isValidMac(String mac){
		return !Strings.isNullOrEmpty(mac) && mac.matches("^[a-fA-F0-9]{12}$");
	}
	protected static final void checkValidMac(String mac) throws ServiceSecurityException{
		if(!isValidMac(mac)){
			throw new ServiceSecurityException(String.format("INVALID MAC:%s ", mac))
			.setType(SecurityExceptionType.INVALID_MAC);
		}
	}
	/** 验证序列号是否有效 */
	protected boolean isValidSerialNo(String sn){
		return true;
	}
	/** 序列号已经被占用则抛出异常 */
	protected void checkNotOccupiedSerialNo(String sn) throws ServiceSecurityException{
		DeviceBean bean = null == sn ? null : dao.daoGetDeviceByIndexSerialNo(sn);
		if(null != bean){
			throw new ServiceSecurityException(
					String.format("serian no:%s be occupied by device ID[%d] MAC[%s]", sn,bean.getId(),bean.getMac()))
					.setType(SecurityExceptionType.OCCUPIED_SN).setDeviceID(bean.getId());
		}			
	}
	/** 序列号无效则抛出异常 */
	protected void checkValidSerialNo(String sn) throws ServiceSecurityException{
		if(!isValidSerialNo(sn)){
			throw new ServiceSecurityException(String.format("INVALID serial number:%s", sn))
					.setType(SecurityExceptionType.INVALID_SN);
		}
	}
	/** 允许的令牌类型 */
	enum Enable{
		/** 允许所有令牌类型 */ALL,
		/** 只允许人员令牌 */PERSON_ONLY,
		/** 只允许设备令牌 */DEVICE_ONLY,
		/** 只允许root令牌 */ROOT_ONLY;
		
		boolean isValid(TokenMangement tm,Token token){
			TokenOp.VALIDATE.asContextTokenOp();
			switch(this){
			case PERSON_ONLY:
				return tm.isValidPersonToken(token) || tm.isValidRootToken(token);
			case DEVICE_ONLY:
				return tm.isValidDeviceToken(token);
			case ROOT_ONLY:
				return tm.isValidRootToken(token);
			case ALL:
				return tm.isValidPersonToken(token) || tm.isValidDeviceToken(token) || tm.isValidRootToken(token);
			default:
				return false;
			}
		}
		/** 
		 * 验证令牌是否有效,无效抛出异常
		 * @throws ServiceSecurityException 
		 */
		void check(TokenMangement tm,Token token) throws ServiceSecurityException{
			if(isValid(tm,token)){
				return;
			}
			StringBuffer message = new StringBuffer("INVALID TOKEN");
			if(null != token){
				switch(this){
				case PERSON_ONLY:
					message.append(",Person Token required");
					break;
				case DEVICE_ONLY:
					message.append(",Device Token required");
					break;
				case ROOT_ONLY:
					message.append(",root Token required");
					break;
				default:
					break;
				}
			}else{
				message.append(",null token");
			}
			throw new ServiceSecurityException(message.toString())
								.setType(SecurityExceptionType.INVALID_TOKEN);
		}
	}
	boolean isUserToken(Token token){
		return token != null && (TokenType.PERSON.equals(token.getType()) || TokenType.ROOT.equals(token.getType()));
	}
	boolean isDeviceToken(Token token){
		return token != null && TokenType.DEVICE.equals(token.getType()) ;
	}
	boolean isRootToken(Token token){
		return token != null && TokenType.ROOT.equals(token.getType()) ;
	}
	boolean isPersonToken(Token token){
		return token != null && TokenType.PERSON.equals(token.getType()) ;
	}
	/** 验证设备令牌是否有效 */
	boolean isValidDeviceToken(Token token){
		if(validateDeviceToken){
			return null == token 
					? false 
					: TokenType.DEVICE.equals(token.getType()) && token.equals(deviceTokenTable.get(Integer.toString(token.getId())));			
		}else{
			return true;
		}
	}
	/** 验证PERSON/ROOT令牌是否有效 */
	boolean isValidUserToken(Token token){
		if(validatePersonToken){
			return null == token 
					? false 
					: (TokenType.PERSON.equals(token.getType()) || TokenType.ROOT.equals(token.getType()))
						&& token.equals(personTokenTable.get(Integer.toString(token.getId())));
		}else{
			return true;
		}
	}
	/** 验证人员令牌是否有效 */
	boolean isValidPersonToken(Token token){
		if(validatePersonToken){
			return token != null  ? TokenType.PERSON.equals(token.getType()) && isValidUserToken(token) : false;
		}else{
			return true;
		}
	}
	/** 验证root令牌是否有效 */
	boolean isValidRootToken(Token token){
		if(validatePersonToken){
			return TokenType.ROOT.equals(token.getType()) && isValidUserToken(token);
		}else{
			return true;
		}
	}
	/** 验证令牌是否有效 */
	boolean isValidToken(Token token){
		if(token != null){
			switch (token.getType()) {
			case DEVICE:
				return isValidDeviceToken(token);
			case PERSON:
			case ROOT:
				return isValidUserToken(token);
			default:
				break;
			}
		}
		return false;

	}
	/** 检查数据库是否存在指定的设备记录,没有则抛出异常{@link ServiceSecurityException} */
	protected void checkValidDeviceId(Integer deviceId) throws ServiceSecurityException{
		if(!this.dao.daoExistsDevice(deviceId)){
			throw new ServiceSecurityException(String.format("NOT EXISTS device %d", deviceId))
					.setType(SecurityExceptionType.INVALID_DEVICE_ID);
		}
	}
	private static Token makeToken(byte[] source){
		ByteBuffer buffer = ByteBuffer.wrap(new byte[8]);
		buffer.asLongBuffer().put(System.nanoTime());
		byte[] md5 = BinaryUtils.getMD5(Bytes.concat(checkNotNull(source),buffer.array()));
		ByteBuffer byteBuffers = ByteBuffer.wrap(md5);
		return new Token(byteBuffers.getInt(), byteBuffers.getInt(),byteBuffers.getInt(),byteBuffers.getInt()).asContextToken();
	}
	private static Token makeToken(Object ...objs){
		checkArgument(null != objs && 0 != objs.length,"objs must not be null or empty");
		StringBuffer buffer = new StringBuffer(64);
		for(Object obj :objs){
			buffer.append(obj);
		}
		return makeToken(buffer.toString().getBytes());
	}
	/**
	 * 计算设备令牌
	 * @param device 设备参数(包括设备ID,MAC地址,序列号),序列号为null则用MAC地址代替
	 * @return 设备访问令牌
	 * @throws IllegalArgumentException 设备ID,MAC地址为{@code null}
	 */
	private static Token makeDeviceTokenOf(DeviceBean device){
		checkArgument(null != device,"device is null");
		checkArgument(
						null != device.getId() 
				&& 	null != device.getMac(),
				"null device argument(id,mac)");
		return makeToken(device.getId(),device.getMac(),
				MoreObjects.firstNonNull(device.getSerialNo(),device.getMac()))
				.asDeviceToken(device.getId());
	}
	private static Token makePersonTokenOf(int personId){
		ByteBuffer buffer = ByteBuffer.wrap(new byte[8]);
		buffer.asLongBuffer().put(personId);
		return makeToken(buffer.array()).asPersonToken(personId);
	}
	private static Token makeRootToken(String password){
		ByteBuffer buffer = ByteBuffer.wrap(new byte[8]);
		buffer.asLongBuffer().put(System.currentTimeMillis());
		return makeToken(Bytes.concat(password.getBytes(),buffer.array())).asRootToken();
	}
	/**
	 * 从{@link #deviceTokenTable}删除指定设备的令牌
	 * @param deviceId
	 */
	private void removeDeviceTokenOf(int deviceId){
		deviceTokenTable.remove(Integer.toString(deviceId));
	}
	/**
	 * 从{@link #personTokenTable}删除指定人员的令牌
	 * @param personId
	 */
	private void removePersonTokenOf(int personId){
		personTokenTable.remove(Integer.toString(personId));
	}
	/**
	 * 如果{@code token}为设备令牌则返回对应的设备信息对象{@link DeviceBean},否则返回{@code null}
	 * @param token 令牌
	 * @return {@link DeviceBean}对象或{@code null}
	 */
	protected DeviceBean getDeviceOrNull(Token token){
		return (token != null && token.getType() == TokenType.DEVICE) ? dao.daoGetDevice(token.getId()) : null;
	}
	/**
	 * 如果{@code token}为人员令牌则返回对应的人员信息对象{@link PersonBean},否则返回{@code null}
	 * @param token 令牌
	 * @return {@link PersonBean}对象或{@code null}
	 */
	protected PersonBean getPersonOrNull(Token token){
		return (token != null && token.getType() == TokenType.PERSON) ? dao.daoGetPerson(token.getId()) : null;
	}
	/**
	 * 从令牌中获取人员等级
	 * @param token
	 * @return
	 */
	protected PersonRank rankOf(Token token){
		if(token != null && token.getType() == TokenType.ROOT){
			return PersonRank.root;
		}
		PersonBean personBean = getPersonOrNull(token);
		return personBean == null ? PersonRank.person : PersonRank.fromRank(personBean.getRank());
	}
	/**
	 * 人员令牌(token)的等级小于指定的等级(rank)时抛出异常
	 * @param token
	 * @param rank
	 * @throws ServiceSecurityException
	 */
	protected void checkRank(Token token,PersonRank rank) throws ServiceSecurityException{
		if(rankOf(token).ordinal() < checkNotNull(rank,"rank is null").ordinal()){
			throw new ServiceSecurityException("admin rank required",SecurityExceptionType.TOO_LOW_RANK);
		}
	}
	/**
	 * 设备注册
	 * @param newDevice
	 * @return
	 * @throws ServiceSecurityException
	 */
	protected DeviceBean registerDevice(DeviceBean newDevice)
			throws ServiceSecurityException{
		TokenOp.REGISTER.asContextTokenOp();
		checkArgument(null != newDevice,"deviceBean must not be null");
		// 检查是否为新记录,
		checkArgument(newDevice.isNew(),
				"for device registeration the 'newDevice' must be a new record,so the _isNew field must be true ");
		// ID为自增长键,新记录ID字段不能指定,由数据库分配
		checkArgument(
				!newDevice.isModified(FL_DEVICE_ID_ID) 
				|| Objects.equal(0,newDevice.getId()),
				"for device registeration the 'newDevice' must be a new record,so id field must be not be set or be zero");
		// sdk_version字段不可为空
		checkArgument(!Strings.isNullOrEmpty(newDevice.getUsedSdks()), "sdkVersion must not be null or empty");

		// 检查sdk_version是否允许注册
		checkArgument(FEATURE_CONFIG.allValidSdkVersions(newDevice.getUsedSdks()), 
				"UNSUPPORTED SDK Version [%s]",newDevice.getUsedSdks());
		String mac = newDevice.getMac();
    	checkArgument(mac != null && mac.matches("^[\\da-fA-F]{12}$"),"INVALID mac address");
    	mac = mac.toLowerCase();
		DeviceBean dmac = this.dao.daoGetDeviceByIndexMac(mac);
		if(null !=dmac ){
			// 设备已经注册
			boolean oldSnValid = isValidSerialNo(dmac.getSerialNo());
			if(Objects.equal(newDevice.getSerialNo(),dmac.getSerialNo()) && oldSnValid){
				// 序列号一致且有效
				// DO NOTHING
			}else	if(null == newDevice.getSerialNo() && oldSnValid){
				// 原序列号有效就使用原序列号
				newDevice.setSerialNo(dmac.getSerialNo());
			}else{
				checkNotOccupiedSerialNo(newDevice.getSerialNo());
				checkValidSerialNo(newDevice.getSerialNo());
				// 用新序列号替换原记录中无效的序列号
			}
			// 复制新记录中的常量字段
			dmac.copy(newDevice, CONST_FIELDS);			
			return dmac.beModified() ? dao.daoSaveDevice(dmac):dmac;
		}else{
			checkNotOccupiedSerialNo(newDevice.getSerialNo());
			checkValidSerialNo(newDevice.getSerialNo());
			return this.dao.daoSaveDevice(newDevice);
		}
	}
	/**
	 * 设备注销
	 * @param deviceId
	 * @throws ServiceSecurityException
	 */
	protected void unregisterDevice(int deviceId)
			throws ServiceSecurityException{
		TokenOp.UNREGISTER.asContextTokenOp();
		this.dao.daoDeleteDevice(deviceId);
	}
	/**
	 * 申请设备令牌
	 * @param loginDevice 申请信息设备信息,必须提供{@code id, mac, serialNo}字段
	 * @return
	 * @throws ServiceSecurityException
	 */
	protected Token applyDeviceToken(DeviceBean loginDevice)
			throws ServiceSecurityException{
		TokenOp.APPLY.asContextTokenOp();
		checkValidDeviceId(loginDevice.getId());

		DeviceBean device = dao.daoGetDevice(loginDevice.getId());
		if(!Objects.equal(device.getMac(), loginDevice.getMac()) ){
			throw new ServiceSecurityException(
					String.format("MISMATCH MAC:%s", device.getMac()))
				.setType(SecurityExceptionType.INVALID_MAC);
		}
		if(!Objects.equal(device.getSerialNo(), loginDevice.getSerialNo())){
			throw new ServiceSecurityException(
					String.format("MISMATCH Serial Number:%s", device.getSerialNo()))
				.setType(SecurityExceptionType.INVALID_SN);
		}
		// 生成一个新令牌
		Token token = makeDeviceTokenOf(device);
		deviceTokenTable.set(device.getId().toString(), token, false);
		return token;
	}
	/**
	 * 释放设备令牌
	 * @param token 当前持有的令牌
	 * @throws ServiceSecurityException
	 */
	protected void releaseDeviceToken(Token token)
			throws ServiceSecurityException{
		TokenOp.RELEASE.asContextTokenOp();;
		Enable.DEVICE_ONLY.check(this, token);
		removeDeviceTokenOf(token.getId());
	}
	/**
	 * 申请人员访问令牌
	 * @param personId
	 * @param password 
	 * @param isMd5 
	 * @return
	 * @throws ServiceSecurityException
	 * @see #checkValidPassword(String, String, boolean)
	 */
	protected Token applyPersonToken(int personId, String password, boolean isMd5)
			throws ServiceSecurityException{
		TokenOp.APPLY.asContextTokenOp();
		checkValidPassword(Integer.toString(personId), password, isMd5);
		if(CommonConstant.PersonRank.person.equals(CommonConstant.PersonRank.fromRank(dao.daoGetPerson(personId).getRank()))
			&&	rejectZero ){
			// 当配置参数指定不允许普通人员申请令牌时抛出异常
			throw new ServiceSecurityException(
					String.format("REJECTION OF APPLICATION for rank 0 user (id = %d)",personId))
				.setType(SecurityExceptionType.REJECT_APPLY);
		}
		Token token = makePersonTokenOf(personId);
		String key = Integer.toString(personId);
		personTokenTable.set(key, token, false);
		personTokenTable.expireValue(token);
		return token;
	}
	/**
	 * 释放人员访问令牌
	 * @param token 当前持有的令牌
	 * @throws ServiceSecurityException
	 */
	protected void releasePersonToken(Token token)
			throws ServiceSecurityException{
		TokenOp.RELEASE.asContextTokenOp();

		Enable.PERSON_ONLY.check(this, token);
		removePersonTokenOf(token.getId());
	}
	/**
	 * 申请root访问令牌
	 * @param password root密码
	 * @param isMd5 为{@code false}代表{@code password}为明文,{@code true}指定{@code password}为32位MD5密文(小写)
	 * @return
	 * @throws ServiceSecurityException
	 */
	protected Token applyRootToken(String password, boolean isMd5)
			throws ServiceSecurityException{
		TokenOp.APPLY.asContextTokenOp();
		checkValidPassword(ROOT_NAME,password,isMd5);
		Token token = makeRootToken(password);
		String key = Integer.toString(token.getId());
		personTokenTable.set(key, token, false);
		personTokenTable.expireValue(token);
		return token;
	}
	/**
	 * 释放root访问令牌
	 * @param token 当前持有的令牌
	 * @throws ServiceSecurityException
	 */
	protected void releaseRootToken(Token token)
			throws ServiceSecurityException{
		TokenOp.RELEASE.asContextTokenOp();

		Enable.ROOT_ONLY.check(this, token);
		removePersonTokenOf(token.getId());
	}
	/**
	 * 验证用户密码是否匹配
	 * @param userId 用户id字符串,root用户id即为{@link CommonConstant#ROOT_NAME}
	 * @param password 用户密码
	 * @param isMd5 为{@code false}代表{@code password}为明文,{@code true}指定{@code password}为32位MD5密文(小写)
	 * @return
	 * @throws RuntimeDaoException 
	 * @throws ServiceSecurityException 
	 * @throws IllegalArgumentException {@code userId} 无效
	 */
	protected boolean isValidPassword(String userId,String password, boolean isMd5) throws RuntimeDaoException, ServiceSecurityException {
		TokenOp.VALIDPWD.asContextTokenOp();
		checkArgument(!Strings.isNullOrEmpty(userId),"INVALID argument,must not be null or empty");
		if(ROOT_NAME.equals(userId)){
			// 从配置文件中读取root密码算出MD5与输入的密码比较
			return cg.cryptograph(CONFIG.getString(ROOT_PASSWORD),false).equals(cg.cryptograph(password,isMd5));
		}else{
			// 从数据库中读取用户密码(已经掺盐加密)与输入的密码比较
			try{
				Integer id = Integer.valueOf(userId);
				String passwordMd5InDb = dao.daoGetPersonChecked(id).getPassword();
				return cg.cryptograph(password,isMd5).equals(passwordMd5InDb);
			}catch(ObjectRetrievalException e){
				throw new ServiceSecurityException(SecurityExceptionType.INVALID_PERSON_ID);
			}catch(NumberFormatException e){
				throw new ServiceSecurityException(SecurityExceptionType.INVALID_PERSON_ID);
			}
		}
	}
	/**
	 * 检查密码是否正确
	 * @param userId
	 * @param password
	 * @param isMd5
	 * @throws ServiceSecurityException 密码不匹配,{@code userId}无效
	 * @see #isValidPassword(String, String, boolean)
	 */
	protected void checkValidPassword(String userId,String password, boolean isMd5) throws ServiceSecurityException{
		if(!isValidPassword(userId, password, isMd5)){
			throw new ServiceSecurityException(
					String.format("INVALID password [%s]for user [%s]",password,userId))
				.setType(SecurityExceptionType.INVALID_PASSWORD);
		}
	}
	/** 
	 * 申请一个唯一的命令序列号 
	 * @param id
	 */
	protected int applyCmdSn(int id){
		int sn = JedisUtils.incr(KEY_CMD_SN);
		String key = Long.toString(sn);
		this.cmdSnTable.set(key, id, false);
		this.cmdSnTable.expire(key);
		return sn;
	}
	/** 申请一个唯一的命令响应通道 
	 * @param id
	 * @param duration 通道有效时间(秒) 大于0有效,否则使用默认的有效期*/
	protected String applyAckChannel(int id, int duration){
		String channel = new StringBuffer(ACK_PREFIX)
				.append(JedisUtils.incr(KEY_ACK_SN))
				.toString();
		this.ackChannelTable.set(channel, id, false);
		if(duration>0){
			this.ackChannelTable.expire(channel,duration,TimeUnit.SECONDS);
		}else{
			this.ackChannelTable.expire(channel);
		}
		return channel;
	}
	
	/**
	 * 判断命令序列号是否有效
	 * @param cmdSn
	 * @return
	 */
	protected boolean isValidCmdSn(int cmdSn){
		return this.cmdSnTable.containsKey(Integer.toString(cmdSn));
	}
	final Predicate cmdSnValidator = new Predicate(){

		@Override
		public boolean apply(Integer input) {
			return isValidCmdSn(input);
		}};

	/**
	 * 判断命令响应通道是否有效
	 * @param ackChannel
	 * @return
	 */
	protected boolean isValidAckChannel(String ackChannel){
		return this.ackChannelTable.containsKey(ackChannel);
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy