一、技术背景:
现在很多web项目或者小程序在上线后都需要进行交叉引流,交叉业务合作,数据传输,与其他的企业,网站,app合作。那么就需要接口数据调用。那么在做外部系统接口调用和自己开发的微服务间的接口调用显然是不同的,最明显的特征就是,系统与系统间的调用首先需要授权。意思就是首先需要登录。如果没有授权的接口调用,显然所有信息都已经暴露在互联网环境下。第二就是传输数据需要加解密;如果没有加密,通过抓包也是可以看到系统间传输了什么信息。所以一般在做一些外部接口调用时,都会涉及到这两部分的内容,但是很多系统的授权和加解密方式却不尽相同,一个大同小异的东西总是让我们在不停的造轮子。有时候A系统与B系统的接口交互是一套授权和加解密算法,A系统和C系统又是另外一套授权和加解密算法。博主认为我们主要几套通用的授权和加解密算法就行,而不是不停的在重复造轮子。我们把更多的经历放在业务的开发中。
二、外部系统接口调用授权的种类
1、获取token,在token有效期期内,携带token作为参数具有调用所有接口权限
2、每次携带签名去调用接口。
三、token的介绍和制作
业界常用的授权标准有两种,一种是使用auth2,这种方式更适合于类似第三方授权登录,比如微信、微博、QQ信任登录业务。另一种是oauth,即第三方无需知道用户和密码就可以申请获得该资源的授权,更适用于对用户的权限校验并分配访问权限,比如常见的登录后分配可见资源(按钮、菜单等)类型网站。
流程说明:
登录
服务端校验密码,成功后返回access_token和refresh_token,客户端记录上述token。
访问API
在访问API之前解析access_token,并且查看是否过期,如果不过 期则请求API,如果过期,则要刷新令牌,在请求API。
刷新token
携带有效期的refresh_token换回有效token,如果refresh_token过期,则需要用户重新登录。
注销
请求注销api,服务器端和客户端应同时删除token的存储。
客户端请求API
携带access_token信息,如果生成环境不会直接携带access_token,会使用加密后的签名校验。祥见以下防重放机制。
获取token
根据环境不同而有不同的获取token方式。
解析token
通过JWT工具将token解析。
由redis读取token
根据uid拼接key读取access_token, 如果不存在这个用户的token说明已经登出。
验证token
判断次token是否属于此uid,判断token是否过期,如果过期则进行以下刷新token的流程。
注入权限
如果token验证成功,根据user信息生成权限注入到spring安全上下文中。
客户端请求API
携带refresh_token,如果是生产环境不会直接携带refresh_token信息,详见以下防重放攻击。
获取token
根据环境不同而有不同的获取token方式。
解析token
通过JWT工具将token解析。
token读取
根据uid拼接key读取出access_token,如果不存在这个用户的token说明用户已经登出。
验证token
判断此token是否属于此uid,判断token是否已经过期,如果过期,则返回refresh_token过期错误,此时用户需要重新登录。
刷新token
如果refresh_token 验证成功,则重新生成access_token和refresh_token,上述有效期以当前时间向后计算,替换此用户在redis中的token,并将token返回给客户端。
四、获取token实例
1、获取token的参数
/**
* 组装获取Access Token参数
*/
public Map<String, Object> getACTokenParam() {
Map<String, Object> map = new HashMap<>();
String timestamp = DateTimeUtils.getDateStr();
// client_secret+timestamp+client_id+username+password+grant_type+scope+client_secret
// username使用原文;
// client_secret(双方约定)、password需要md5加密后的转大写;
// 将上述拼接的字符串使用MD5加密,加密后的值再转为大写
String clientSecM = MD5Util.getMd5(clientSecret).toUpperCase();
String passwordM = MD5Util.getMd5(password).toUpperCase();
String subString = (clientSecM + timestamp + clientId + userName + passwordM + grantType + scope + clientSecM);
String sign = MD5Util.getMd5(subString).toUpperCase();
map.put("grant_type", grantType);
map.put("client_id", clientId);
map.put("timestamp", timestamp);
map.put("username", userName);
map.put("password", passwordM);
map.put("scope", scope);
map.put("sign", sign);
return map;
}
获取token其实还是注入用户名,秘钥这样的东西去获取一个凭证,其实就是一个登录的过程。
2、携带token去请求接口实例:
public Map<String, Object> getSendParam(Map<String, Object> inputParam){
Map<String, Object> params = new LinkedHashMap<>();
String token = getACToken();
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String input = JSONObject.toJSONString(inputParam);
log.info("本次接口发送参数:"+input);
String inputDES = Des3Utils.get3DESEncryptECB(input, desKey);
byte[] decoded = Base64.getDecoder().decode(inputDES);
String inputHex = Des3Utils.byteToHexString(decoded);
String subString = subSign(input, timestamp, token);
String sign = MD5Util.getMd5(subString).toUpperCase();
params.put("token", token);
params.put("sign", sign);
params.put("timestamp", timestamp);
params.put("clientId", clientId);
params.put("jsonData", inputHex);
return params;
}
其实就是在原来需要的传递参数的基础上,加上token参数。让对方去校验权限。
五、签名的介绍
签名 相当于我自己生成一把锁和一把钥匙,然后把我想发布的内容用我的锁锁起来形成一个签名,把内容和签名一起发布,并且告诉大家我的钥匙是什么。人们可以拿到钥匙来打开签名里的内容来验证是不是跟发布的内容一致。天下人都能拿到钥匙来验证签名与内容的一致性,但只有我有签名的锁。
签名实例:
/**
* 根据密钥和消息生成签名
*
* @param secretKey
* @param message
* @return
* @throws Exception
*/
private static String generateSignature(String secretKey, String message) throws Exception {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(secretKey.getBytes("UTF-8"), algorithm));
byte[] hash = mac.doFinal(message.getBytes("UTF-8"));
String signature = Base64.getEncoder().encodeToString(hash);
return signature;
}
这里的秘钥就双方系统约定的。所以也想双方配了钥匙,只有验签通过就有权限。
通过验签去调用接口实例:
public String ltApi(ObjectNode json,URL url) throws Exception{
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json");
// 添加认证信息
String message = connection.getRequestMethod() + connection.getURL().getPath();
String signature = generateSignature(secretKey, message);
String timestamp = String.valueOf(System.currentTimeMillis());
connection.setRequestProperty("Authorization", signature + " " + timestamp);
// 创建一个ObjectMapper对象
ObjectMapper mapper = new ObjectMapper();
// 将Json对象转换为Json字符串
String jsonStr = mapper.writeValueAsString(json);
// 设置请求正文
connection.setDoOutput(true);
try (OutputStream os = connection.getOutputStream()) {
os.write(jsonStr.getBytes());
os.flush();
}
if (connection.getResponseCode() != 200) {
log.info("Failed : HTTP error code:"+connection.getResponseCode()+",信息:"+connection.getResponseMessage());
throw new RuntimeException("Failed : HTTP error code " + connection.getResponseCode());
}else {
log.info(json+"上链成功");
}
// 获取响应体
StringBuilder sb = new StringBuilder();
try (InputStream is = connection.getInputStream();
InputStreamReader isr = new InputStreamReader(is);
BufferedReader reader = new BufferedReader(isr)) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
String responseBody = sb.toString();
log.info(responseBody);
connection.disconnect();
return responseBody;
}
Token生成工具类
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
/**
* token工具类
*/
@Component
public class TokenUtil {
/**
* token过期时间
*/
private static final long EXPIRE_TIME = 30 * 60 * 1000;
/**
* 生成签名,30分钟过期
* @param username 用户名
* @param password 密码
* @param loginTime 登录时间
* @param tokenSecret 秘钥
* @return 生成的token
*/
public static String createToken(String username, String password, String loginTime ,String tokenSecret) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(tokenSecret);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("loginName", username)
.withClaim("password", password)
.withClaim("loginTime", loginTime)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验token是否正确
* @param token 需要校验的token
* @return 校验是否成功
*/
public static boolean verifyToken(String token ,String tokenSecret){
try {
//设置签名的加密算法:HMAC256
Algorithm algorithm = Algorithm.HMAC256(tokenSecret);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e){
return false;
}
}
}