【开源项目】使用Sa-Token框架完成API参数签名

介绍

使用 Sa-Token 内置的 sign 模块,方便的完成 API 签名创建、校验等步骤:

  • 不限制请求的参数数量,方便组织业务需求代码。
  • 自动补全 nonce、timestamp 参数,省时省力。
  • 自动构建签名,并序列化参数为字符串。
  • 一句代码完成 nonce、timestamp、sign 的校验,防伪造请求调用、防参数篡改、防重放攻击。

Java实现

引入依赖

        <dependency>
            <groupId>cn.dev33groupId>
            <artifactId>sa-token-spring-boot-starterartifactId>
            <version>1.35.0.RCversion>
        dependency>

配置秘钥

sa-token: 
    sign:
        # API 接口签名秘钥 (随便乱摁几个字母即可)
        secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor

请求发起端构建签名

String url = "http://b.com/api/addMoney";

// 请求参数
Map<String, Object> paramMap = new LinkedHashMap<>();
paramMap.put("userId", 10001);
paramMap.put("money", 1000);
// 更多参数,不限制数量...

// 补全 timestamp、nonce、sign 参数,并序列化为 kv 字符串
String paramStr = SaSignUtil.addSignParamsAndJoin(paramMap);

// 将参数字符串拼接在请求地址后面
url += "?" + paramStr;

// 发送请求
String res = HttpUtil.request(url);

// 根据返回值做后续处理
System.out.println("server 端返回信息:" + res);

请求接受端校验签名

@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money) {

    // 1、校验请求中的签名
    SaSignUtil.checkRequest(SaHolder.getRequest());
    
    // 2、校验通过,处理业务
    System.out.println("userId=" + userId);
    System.out.println("money=" + money);
    
    // 3、返回
    return SaResult.ok();
}

原理分析

构建签名

SaSignUtil#addSignParamsAndJoin

    public static String addSignParamsAndJoin(Map<String, Object> paramsMap) {
        return SaManager.getSaSignTemplate().addSignParamsAndJoin(paramsMap);
    }

SaSignTemplate#addSignParamsAndJoin,新增签名参数

public String addSignParamsAndJoin(Map paramsMap) {
    paramsMap = this.addSignParams(paramsMap);
    return this.joinParams(paramsMap);
}

SaSignTemplate#addSignParams,新增了timestampnonce随机数和sign

    public Map<String, Object> addSignParams(Map<String, Object> paramsMap) {
        paramsMap.put(timestamp, String.valueOf(System.currentTimeMillis()));
        paramsMap.put(nonce, SaFoxUtil.getRandomString(32));
        paramsMap.put(sign, this.createSign(paramsMap));
        return paramsMap;
    }

SaSignTemplate#createSign,创建签名。按照Map中的自然排序进行拼接

    public String createSign(Map<String, ?> paramsMap) {
        String secretKey = this.getSecretKey();
        SaSignException.throwByNull(secretKey, "参与参数签名的秘钥不可为空", 12201);
        if (((Map)paramsMap).containsKey(sign)) {
            paramsMap = new TreeMap((Map)paramsMap);
            ((Map)paramsMap).remove(sign);
        }

        String paramsStr = this.joinParamsDictSort((Map)paramsMap);
        String fullStr = paramsStr + "&" + key + "=" + secretKey;
        return this.abstractStr(fullStr);
    }

    public String joinParamsDictSort(Map<String, ?> paramsMap) {
        if (!(paramsMap instanceof TreeMap)) {
            paramsMap = new TreeMap((Map)paramsMap);
        }

        return this.joinParams((Map)paramsMap);
    }

SaSignTemplate#abstractStr,加密方式是md5

    public String abstractStr(String fullStr) {
        return SaSecureUtil.md5(fullStr);
    }

验证签名

SaSignUtil#checkRequest,校验请求

public static void checkRequest(SaRequest request) {
    SaManager.getSaSignTemplate().checkRequest(request);
}

SaSignTemplate#checkRequest,校验请求中的参数

    public void checkRequest(SaRequest request) {
        this.checkParamMap(request.getParamMap());
    }

SaSignTemplate#checkParamMap,校验请求中的参数timestampValuenonceValuesignValue

public void checkParamMap(Map paramMap) {
    String timestampValue = (String)paramMap.get(timestamp);
    String nonceValue = (String)paramMap.get(nonce);
    String signValue = (String)paramMap.get(sign);
    this.checkTimestamp(Long.parseLong(timestampValue));
    if (this.getSignConfigOrGlobal().getIsCheckNonce()) {
        this.checkNonce(nonceValue);
    }

    this.checkSign(paramMap, signValue);
}

SaSignTemplate#checkTimestamp,判断在一定范围内

    public void checkTimestamp(long timestamp) {
        if (!this.isValidTimestamp(timestamp)) {
            throw (new SaSignException("timestamp 超出允许的范围:" + timestamp)).setCode(12203);
        }
    }

    public boolean isValidTimestamp(long timestamp) {
        long allowDisparity = this.getSignConfigOrGlobal().getTimestampDisparity();
        long disparity = Math.abs(System.currentTimeMillis() - timestamp);
        return allowDisparity == -1L || disparity <= allowDisparity;
    }

SaSignTemplate#checkNonce,校验随机数

    public void checkNonce(String nonce) {
        if (SaFoxUtil.isEmpty(nonce)) {
            throw new SaSignException("nonce 为空,无效");
        } else {
            String key = this.splicingNonceSaveKey(nonce);
            if (SaManager.getSaTokenDao().get(key) != null) {
                throw new SaSignException("此 nonce 已被使用过,不可重复使用:" + nonce);
            } else {
                SaManager.getSaTokenDao().set(key, nonce, this.getSignConfigOrGlobal().getSaveNonceExpire() * 2L + 2L);
            }
        }
    }

SaSignTemplate#checkSign,根据请求中的参数数据创建签名,判断和原有签名是否相同。

    public void checkSign(Map<String, ?> paramsMap, String sign) {
        if (!this.isValidSign(paramsMap, sign)) {
            throw (new SaSignException("无效签名:" + sign)).setCode(12202);
        }
    }

    public boolean isValidSign(Map<String, ?> paramsMap, String sign) {
        String theSign = this.createSign(paramsMap);
        return theSign.equals(sign);
    }

SaToken存储

SaTokenDao是存储接口。

默认实现是用的是SaTokenDaoDefaultImplSaTokenDaoDefaultImpl存储数据,主要是通过ConcurrentHashMap存放在本地内存中。

public class SaTokenDaoDefaultImpl implements SaTokenDao {
	

	/**
	 * 数据集合 
	 */
	public Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();

	/**
	 * 过期时间集合 (单位: 毫秒) , 记录所有key的到期时间 [注意不是剩余存活时间] 
	 */
	public Map<String, Long> expireMap = new ConcurrentHashMap<String, Long>();
	
	/**
	 * 构造函数
	 */
	public SaTokenDaoDefaultImpl() {
		initRefreshThread();
	}
	
	
	// ------------------------ String 读写操作 
	
	@Override
	public String get(String key) {
		clearKeyByTimeout(key);
		return (String)dataMap.get(key);
	}

	@Override
	public void set(String key, String value, long timeout) {
		if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE)  {
			return;
		}
		dataMap.put(key, value);
		expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
	}
}

SaTokenDaoDefaultImpl#initRefreshThread,对过期数据要定时清除

	/**
	 * 初始化定时任务 
	 */
	public void initRefreshThread() {

		// 如果配置了<=0的值,则不启动定时清理
		if(SaManager.getConfig().getDataRefreshPeriod() <= 0) {
			return;
		}
		// 启动定时刷新
		this.refreshFlag = true;
		this.refreshThread = new Thread(() -> {
			for (;;) {
				try {
					try {
						// 如果已经被标记为结束
						if(refreshFlag == false) {
							return;
						}
						// 执行清理
						refreshDataMap(); 
					} catch (Exception e) {
						e.printStackTrace();
					}
					// 休眠N秒 
					int dataRefreshPeriod = SaManager.getConfig().getDataRefreshPeriod();
					if(dataRefreshPeriod <= 0) {
						dataRefreshPeriod = 1;
					}
					Thread.sleep(dataRefreshPeriod * 1000);
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		});
		this.refreshThread.start();
	}


	/**
	 * 清理所有已经过期的key 
	 */
	public void refreshDataMap() {
		Iterator<String> keys = expireMap.keySet().iterator();
		while (keys.hasNext()) {
			clearKeyByTimeout(keys.next());
		}
	}

如果仅仅存放在本地内存中,涉及到多个项目,可能数据无法共享。

引入仓库sa-token-dao-redis-jackson

        <dependency>
            <groupId>cn.dev33groupId>
            <artifactId>sa-token-dao-redis-jacksonartifactId>
            <version>1.33.0version>
        dependency>

SaTokenDaoRedisJackson使用Redis作为存储数据的地方。

@Component
public class SaTokenDaoRedisJackson implements SaTokenDao {
    public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    public static final String DATE_PATTERN = "yyyy-MM-dd";
    public static final String TIME_PATTERN = "HH:mm:ss";
    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss");
    public ObjectMapper objectMapper;
    public StringRedisTemplate stringRedisTemplate;
    public RedisTemplate<String, Object> objectRedisTemplate;
    public boolean isInit;

    public SaTokenDaoRedisJackson() {
    }

    @Autowired
    public void init(RedisConnectionFactory connectionFactory) {
        if (!this.isInit) {
            StringRedisSerializer keySerializer = new StringRedisSerializer();
            GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();

            try {
                Field field = GenericJackson2JsonRedisSerializer.class.getDeclaredField("mapper");
                field.setAccessible(true);
                ObjectMapper objectMapper = (ObjectMapper)field.get(valueSerializer);
                this.objectMapper = objectMapper;
                this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
                JavaTimeModule timeModule = new JavaTimeModule();
                timeModule.addSerializer(new LocalDateTimeSerializer(DATE_TIME_FORMATTER));
                timeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DATE_TIME_FORMATTER));
                timeModule.addSerializer(new LocalDateSerializer(DATE_FORMATTER));
                timeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DATE_FORMATTER));
                timeModule.addSerializer(new LocalTimeSerializer(TIME_FORMATTER));
                timeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(TIME_FORMATTER));
                this.objectMapper.registerModule(timeModule);
                SaStrategy.me.createSession = (sessionId) -> {
                    return new SaSessionForJacksonCustomized(sessionId);
                };
            } catch (Exception var7) {
                System.err.println(var7.getMessage());
            }

            StringRedisTemplate stringTemplate = new StringRedisTemplate();
            stringTemplate.setConnectionFactory(connectionFactory);
            stringTemplate.afterPropertiesSet();
            RedisTemplate<String, Object> template = new RedisTemplate();
            template.setConnectionFactory(connectionFactory);
            template.setKeySerializer(keySerializer);
            template.setHashKeySerializer(keySerializer);
            template.setValueSerializer(valueSerializer);
            template.setHashValueSerializer(valueSerializer);
            template.afterPropertiesSet();
            this.stringRedisTemplate = stringTemplate;
            this.objectRedisTemplate = template;
            this.isInit = true;
        }
    }

    public String get(String key) {
        return (String)this.stringRedisTemplate.opsForValue().get(key);
    }

    public void set(String key, String value, long timeout) {
        if (timeout != 0L && timeout > -2L) {
            if (timeout == -1L) {
                this.stringRedisTemplate.opsForValue().set(key, value);
            } else {
                this.stringRedisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
            }

        }
    }
}

获取和更新SaTokenDao

SaBeanInject#setSaTokenDaoSaBeanInject是自动配置的。当系统中存在SaTokenDao的Bean实例,则设置SaTokenDao实例。

@Autowired(
    required = false
)
public void setSaTokenDao(SaTokenDao saTokenDao) {
    SaManager.setSaTokenDao(saTokenDao);
}

SaManager#setSaTokenDao,设置SaTokenDao实例

	public static void setSaTokenDao(SaTokenDao saTokenDao) {
		setSaTokenDaoMethod(saTokenDao);
		SaTokenEventCenter.doRegisterComponent("SaTokenDao", saTokenDao);
	}
	
	private static void setSaTokenDaoMethod(SaTokenDao saTokenDao) {
		if((SaManager.saTokenDao instanceof SaTokenDaoDefaultImpl)) {
			((SaTokenDaoDefaultImpl)SaManager.saTokenDao).endRefreshThread();
		}
		SaManager.saTokenDao = saTokenDao;
	}

SaManager#getSaTokenDao,如果saTokenDao没有设置,则获取默认的实现SaTokenDaoDefaultImpl

	public static SaTokenDao getSaTokenDao() {
		if (saTokenDao == null) {
			synchronized (SaManager.class) {
				if (saTokenDao == null) {
					setSaTokenDaoMethod(new SaTokenDaoDefaultImpl());
				}
			}
		}
		return saTokenDao;
	}

【开源项目】使用Sa-Token框架完成API参数签名_第1张图片

你可能感兴趣的:(开源)