接口加密
因为系统明文传输的数据会被不明身份的人用抓包工具抓取,从而威胁系统和数据的安全性,所以需要对接口进行加密
接口的安全要求主要有以下几点:
- 防伪装攻击(如在公共网络中,第三方有意或恶意的调用接口)
- 防篡改攻击(如在公共网络中,请求头/查询字符串/内容 在传输过程中被修改)
- 防重放攻击(如在公共网络中,请求被截获,稍后被重放或多次重放)
- 防数据信息泄露(如被第三方截获用户登录请求,截获到账号、密码等)
常见加密算法
加密算法主要分为对称加密 和 非对称加密和一个不需要密钥的散列算法(或称消息摘要算法),对称加密算法的加密和解密密钥相同,非对称加密算法的加密和解密密钥不同
摘要算法 常见有 CRC MD5 SHA等
对称加密算法常见有 DES 3DES AES TDEA Blowfifish RC2 RC4 RC5 IDEA SKIPJACK等
非对称加密算法 常见有 RSA Elgamal 背包算法 Rabin D-H ECC(椭圆取线加密算法)
消息摘要算法
- 消息摘要是把任何长度的输入糅合而产生固定长度的信息
- 消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文
- 消息摘要算法不存在密钥的管理和分发问题,适合于分布式网络上使用
- 主要特点:
1、无论输入的消息有多长,计算出来的消息摘要的长度总是固定的
2、消息摘要看起来是“随机的“,这些数据看上去是胡乱的杂凑在一起的
3、只要输入的消息不同,对其进行摘要后产生的摘要消息也必不相同,但相同的输入必然会产生相同的输出(ps:所以可能会被暴力破解密码)
4、只能进行正向的消息摘要,而无法从摘要中恢复出任何消息,甚至根本就找不到任何和原信息相关的信息
5、虽然碰撞时肯定存在的,但好的算法很难从中找到碰撞,即无法找到两条不同的消息,但它们的摘要是相同的
常用摘要算法(MD5、SHA、CRC)
对称加密
- 一句话解释:采用单钥密码系统的加密方式,即同一个密钥可以同时作为信息的加密和解密
- 特点:
速度快,通常在消息发送方需要加密大量数据时使用
密钥是控制加密和解密过程的指令
算法是一组规则,规定了如何加密和解密 - 典型应用场景:
离线的大量数据加密(用于存储的数据)、单点登录 - 常用的加密算法:DES、3DES、AES、TDEA、Blowfifish、RC2、RC4、RC5、IDEA、SKIPJACK等
常用对称加密算法(DES、3DES、AES)
非对称加密
- 非对称加密算法是一种密钥的保密算法,加密和解密使用两个不同的密钥(公钥和私钥)
- 公钥和私钥是一对,如果使用公钥对数据进行加密,只有对应的私钥才能进行解密
- 特点:
算法强度复杂
加密解密速度没有对称密钥算法的速度快
但密钥管理简单,安全性高
加密速度较慢,适合小数据量加解密或数据签名 - 经典应用场景:数字签名(私钥加密、公钥验证)
- 常用算法:RSA、Elgamal、背包算法、Rabin、D-H、ECC
- 非对称加密的工作原理如下:
(1)A->B,A和B都要产生一对用于加密和解密的公钥和私钥
(2)A的私钥保密,A的公钥告知给B,B的私钥保密,B的公钥告知A
(3)A->B,A用B的公钥加密信息
(4)B接收消息,B用自己的私钥解密A的消息,而其他所有收到这个报文的人都无法解密
常用的非对称加密算法(RSA、DSA、ECC)
数字签名
- 根据其字面意思,是一种类似写在纸上的普通的物理签名,是使用了公钥加密领域技术实现,用于鉴别数字信息的方法
- 签名的作用:无非就是证明某个文件上的内容确实是我写的/我认同的,别人不能冒充我的签名(不可伪造性),我也不能否认上面的签名是我的(不可抵赖性)
- 数字签名通常使用私钥生成签名,使用公钥验证签名
- 签名及验证过程:
(1)发送方用一个哈希函数(如MD5)从报文文本中生成报文摘要,然后用自己的私钥对这个摘要进行加密
(2)加密后的摘要作为报文的数字签名和报文一起发送给接收方
(3)接收方用与发送方一样的哈希函数从接收到的原始报文中重新计算(注意这里是重新计算,而不是解密摘要)报文摘要
(4)接收方再用发送方的公钥来对报文附加的数字签名进行解密
(5)然后对比两个摘要,如果两个摘要相同,接收方就能确认数字签名是该发送方的 - 应用—网站认证(oath2)
- 应用—比特币:比特币是一种完全匿名的数字货币,它的身份认证是基于ECDSA(即椭圆曲线数字签名算法)的,比特币的账户地址就是对公钥计算摘要得到的,向全世界公布,而确认你是账户拥有者的唯一办法就是看你有没有账户对应的私钥。对于比特币中的任何一个交易记录,只有当其中付款方的签名是有效的,它才是有效的。如果账户私钥丢失,那么你将永远失去里面的钱,一旦被黑客盗取,里面的钱就完全归黑客所有
Web认证机制
认证是为了确认用户身份信息,授权是为了确认用户是否有某种行为或资源允许的权利
HTTP Basic Auth
- 简单点讲,就是每次请求API的时候都会提供用户的username和password,就是我们平时常见的使用用户名和密码登录网站
- 采用的其实是使用Base64对username和password进行混合编码
- 优点: http基本认证是基本上所有流行网页浏览器都支持,但很少在公开访问的互联网站上使用,比较常见在小的私有系统中使用
- 缺点: 需要客户端和服务器主机之间的连接足够安全,如果没有使用SSL/TLS这样的传输层安全的协议,那么以明文传输的密钥和口令很容易被拦截
Cookie Auth
- Cookie认证机制就是为一次请求认证在服务端创建一个Session对象保存,同时在客户端的浏览器创建一个Cookie对象
- 通过客户端带上来的Cookie对象和服务端的Session对象进行匹配来实现状态管理。
- 默认的,当我们关闭浏览器的时候,cookie会被删除,但可以通过修改cookie的expire time来使cookie在一定时间内有效
- 缺点 随着不同客户端用户的增加,独立的服务器无法承载更多用户,随着认证用户的增加,服务端的开销会很大
因为是基于cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
用户认证后,服务端做认证记录,如果认证的记录被保存在内存中的话,说明下一次用户要在同一台服务器上才能拿到授权资源,对于分布式应用来说,这样限制了负载均衡的能力
OAuth
- OAuth是指开放授权,是一个开放授权的标准,允许第三方应用访问该用户在某一web服务上存储的私密的资源,而无需将用户名和密码提供给第三方应用
- OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据,每一个令牌授权给一个特定的第三方系统
- OAuth的处理流程:
- 应用:这种认证机制非常适用于个人消费者类的互联网应用产品,如社交类等等,但是不太适用于拥有自有认证权限管理的企业应用
Token Auth
- 使用基于Token的身份验证方法,无需长期保存用户名和密码,服务器端能主动让token失效等诸多好处,对于Web应用和App非常实用,也被很多大型网站采用,如FaceBook、Github、Google等
- 流程如下:
1、客户端使用用户名和密码请求登录
2、服务端收到请求,去验证用户名与密码
3、验证成功后,服务端签发一个Token,并将该Token返回给客户端 (授权)
4、客户端收到Token以后把它存储起来,如放在Cookie中或存在本地
5、客户端每次向服务端请求资源的时候都需要带着服务端签发的Token
6、服务端收到请求,然后去验证客户端请求里带着的Token(鉴权),如果验证成功,就向客户端返回请求的数据 - 优点(Token机制相对于Cookie机制的优点):
1、支持跨域访问:Cookie是不允许跨域(不同域名 主机和端口)访问的,这一点对于Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输
2、无状态(或称:服务端可扩展行):Token机制在服务端不需要存储Session信息,因为Token本身就包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息
3、更适用CDN(内容分发网络):可以通过内容分发网络请求服务端所有的资料(如javascript,HTML,图片等),而你的服务端只要提供API即可
4、解耦:不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成并调用即可
5、更适用于移动应用:当你的客户端是一个原生平台(iOS,Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单很多
6、CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范
7、性能:一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算的Token验证和解析要费时得多
8、不需要为登陆页面做特殊处理,如果使用Protractor做功能测试的时候,不再需要为登录页面做特殊处理
9、基于标准化:你的API可以采用标准化的JSON Web Token,这个标准已经存在多个后端库(.NET,Ruby,Java,Python,PHP)和多家公司的支持。
基于JWT的Token认证
JWT概念简述
- JWT 全称 JSON Web Token,是一个非常轻巧的规范,这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息
- 目前较为流行的跨域认证解决方案,是一种基于JSON的开发标准
- 数据可以经过签名加密,较为安全可靠
- 一般用于前端和服务器之间传递消息,也可以用于移动端和后台传递认证信息
- 需要注意的是,token只是用于身份认证,而不是数据安全传输,所以切记不要在token中放入敏感信息
JWT组成
一个JWT实际上就是一个字符串,它由三部分组成:头部(header)、载荷(playload)、签名(signature),三部分之间用”."来连接
- 头部(header)
头部用于描述关于该JWT的最基本的信息,承载两部分信息,表示为json格式- typ:声明类型,这里是jwt
- alg:声明加密的算法,通常是用HMAC-SHA256
- 一般会对头部进行Base64编码,base64是一种基于64个可打印字符(A-Z、a-z、0-9、+、/)来表示二进制数据的表示方法,由于2的6次方等于64,所以每6个比特位为一个单元,对应某个可打印字符。三个字节有24个比特,对应4个Base64单元,Base64主要是为了防止数据传输过程中发生乱码
json格式
{
'typ': 'JWT',
'alg': 'HS256'
}
base64编码后
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
- 载荷(payload)
载荷就是存放有效信息的地方,这个名字像是特指飞机上承载的货品,这些有效信息又包括三部分- 标准中注册的声明(建议有,但如果你没有也没事)
iss: jwt的签发者
sub:jwt所面向的用户
aud:接收jwt的一方
exp:jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt是不可用的
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击 - 公共的声明:可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,但不建议添加敏感信息,因为这个部分在客户端可以被解密
- 私有声明:私有声明式提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为Base64是对称的,这意味着该部分信息可以归类为明文信息
这个指的就是自定义的claim。JWT规定的claim,JWT的接收方在拿到了JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够进行验证),而私有的claim是不会验证的,除非明确告知接收方这些claim如何进行验证才行
- 标准中注册的声明(建议有,但如果你没有也没事)
payload的json格式
{"sub":"1234567890","name":"jack","admin":true}
base64编码后成为了jwt的第二部分
JTdCJTIyc3ViJTIyJTNBJTIyMTIzNDU2Nzg5MCUyMiUyQyUyMm5hbWUlMjIlM0ElMjJqYWNrJTIyJTJDJTIyYWRtaW4lMjIlM0F0cnVlJTdE
- 签证(signature)
签证信息也是由三部分组成:base64编码后的header,base64编码后的payload,secret 用户私钥
这个部分需要Base64编码后的Header和payload用“."连接组成字符串,然后通过头部声明的加密算法使用secret进行加密生成签名,成为jwt的第三部分
注意点:secret是保存在服务端的,jwt的签发生成也是在服务端的,secret就是用来进行jwt的签发和jwt的验证,就是服务端的私钥,在任何场合都不应该被透露,一旦客户端知道了这个secret,那么意味着客户端可以自行签发jwt
TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
将这三部分用"."连接成一个完整的字符串就构成了最终的jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JJWT快速入门
JJWT=java json web token ,是基于java语言的jwt工具
创建SpringBoot项目并在pom文件中添加JJWT的依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
编写工具类JWTUtil
package com.dean.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
//设置token有效期为半小时
public static final Long JWT_TTL=3600000L;
//设置密钥的明文 需要安全保护 不要让别人知道
public static final String JWT_KEY = "dean";
/**
* 创建token
* @param id token的唯一标识
* @param subject 面向的用户(管理员/普通用户)
* @param ttlMills token的有效期
* @return
*/
public static String createJwt(String id, String subject, Long ttlMills) {
JwtBuilder builder = getJwtBuilder(id, subject, ttlMills);
return builder.compact();
}
/**
* 创建token 使用自定义的claims
* claims中可以自由放入如用户Id之类的东西 但不要放入敏感信息
*/
public static String createJwt(String id, String subject, Long ttlMills, Map<String, Object> claims) {
JwtBuilder builder = getJwtBuilder(id, subject, ttlMills);
//自定义的claims
builder = builder.addClaims(claims);
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String id, String subject, Long ttlMills) {
if (ttlMills == null) {
ttlMills = JwtUtil.JWT_TTL;
}
//获取当前时间
Long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//失效时间
long expMills = nowMillis + ttlMills;
Date expDate = new Date(expMills);
//对密钥进行二次加密
SecretKey secretKey = generalKey();
return Jwts.builder()
.setId(id) //唯一的ID
.setSubject(subject) //主题 可以是JSON数据
.setIssuer("dean") //签发者
.setIssuedAt(now) //签发时间
.signWith(SignatureAlgorithm.HS256, secretKey) //使用HS256对称加密算法签名,第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 生成加密后的秘钥 签证最重要的部分
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
//注意:对密钥加密的算法也可以选择其他算法 并不要被别人知道
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*/
public static Claims parseJWT(String jwt)throws Exception{
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
编写测试用例进行测试
package com.dean.jwt;
import io.jsonwebtoken.Claims;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@SpringBootTest
class JjwtApplicationTests {
//简单生成一个Token
@Test
public void testCreateJwt() {
String id = UUID.randomUUID().toString();
String username = "dean";
String token = JwtUtil.createJwt(id, username, null);
System.out.println(token);
}
//解析token获取username和token唯一标识
@Test
public void testParseJwt() throws Exception{
//实际上这个方法可以放在拦截器里进行解析token后验证身份
Claims claims = JwtUtil.parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI3ZTA3NTM4YS1iMjdhLTQyZjgtYWY2MC1kYjRkNTkxN2U2ZjciLCJzdWIiOiJkZWFuIiwiaXNzIjoiZGVhbiIsImlhdCI6MTY0Nzg4NTIwOSwiZXhwIjoxNjQ3ODg4ODA5fQ.BipZCzCt_RLoWKj4Wg9skg7hWRTswx2g8Q4DV2pa5sw");
String id = claims.getId();
String username = claims.getSubject();
Date time = claims.getExpiration();
System.out.println(id);
System.out.println(username);
System.out.println(time);
}
//放入自定义的一些数据 生成token
@Test
public void testCustomCreateJwt() {
String id = UUID.randomUUID().toString();
Map<String, Object> claims = new HashMap<>();
claims.put("role", "admin");
claims.put("username", "dean");
claims.put("userId", 1);
String jwt = JwtUtil.createJwt(id, null, null, claims);
System.out.println(jwt);
}
//解析token 获取自定义的数据
@Test
public void testCustomParseJwt() throws Exception {
Claims claims = JwtUtil.parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyNWZjNGIyOS00NmZkLTQ2MTktODZlNi01YTBiYWU1MTEwY2EiLCJpc3MiOiJkZWFuIiwiaWF0IjoxNjQ3ODg1Nzk4LCJleHAiOjE2NDc4ODkzOTgsInJvbGUiOiJhZG1pbiIsInVzZXJJZCI6MSwidXNlcm5hbWUiOiJkZWFuIn0.5nErVTwFviIejKRYT0iJSiXyU2Zr-nZqAkyWMFMwA48");
String role = claims.get("role", String.class);
String username = claims.get("username", String.class);
Integer userId = claims.get("userId", Integer.class);
System.out.println(role+" "+username+" "+userId);
}
}
SpringCloud项目模拟Token认证
以下用的SpringCloud项目就是上篇文章里的SpringCloud项目
简述一下简单的功能流程
- 完成一个登录功能
- 完成一个修改用户的功能 可以在不同的微服务上,我们暂时放在一个微服务内
- 登录成功后,生成token并返回
- 在网关微服务中设定 登录请求放行(意思是不论登录用户是否合法,都可以发送登录请求)
而修改用户功能需要认证,如果认证成功则放行,如果认证失败则重写返回体
基于上一个SpringCloud项目的文件结构改造
主要修改了网关部分和服务提供者
开干
- Provider的pom文件中添加jjwt依赖,并将前文中的JwtUtil文件拷过来
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
- Provider中写一个登录服务和一个修改用户服务
//登录服务
@GetMapping("/login")
private String login(@RequestParam("id") Integer id) {
//查询用户是否存在
User user = map.get(id);
if (null == user) {
return "该用户不存在,登录失败!";
}
//用户存在 生成token
String token = JwtUtil.createJwt(UUID.randomUUID().toString(), "user", null);
System.out.println(token);
return token;
}
//修改用户服务
@PutMapping("/update")
private User update(@RequestParam("id") Integer id, @RequestBody User user, HttpServletRequest request) {
//也可以通过request请求的头部,在请求里拿到token
String token = request.getHeader("token");
System.out.println(token );
//模拟修改用户信息
user.setId(id + 2);
return user;
}
- GateWay中的pom文件添加所需依赖,同时将JwtUtil文件拷过来
json的依赖是在重写响应体的时候用到的,所以务必添加
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
- GateWay中改写TokenFilter的filter方法,并写一个getVoidMono方法来重写返回体
@Component
public class TokenFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//拦截请求后进行token认证
System.out.println("There is GateWay");
//获取请求url
//注意这里不要取URI URI拿到的是完整的前端请求url,包括端口号这些
ServerHttpRequest request = exchange.getRequest();
String url = request.getPath().toString();
//过滤请求url 即如果请求为/provider/login 则放行
if (url.contains("/provider/login")) {
return chain.filter(exchange);
}
//如果请求不是 则从请求头中获取token
String token=request.getHeaders().getFirst("token");
System.out.println(token);
//如果token为空 则重写返回体
//获取Response 为了重写返回体
ServerHttpResponse response = exchange.getResponse();
if (null == token) {
return getVoidMono(response);
}
//如果有token 则解析token 解析成功说明token正确 则放行请求 否则重写返回体
try {
Claims claims = JwtUtil.parseJWT(token);
String subject = claims.getSubject();
System.out.println(subject);
} catch (Exception e) {
e.printStackTrace();
return getVoidMono(response);
}
return chain.filter(exchange);
}
//封装响应体 注意此处ServerHttpResponse的包为import org.springframework.http.server.reactive.ServerHttpResponse
private Mono<Void> getVoidMono(ServerHttpResponse serverHttpResponse) {
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
Map<String, String> entity = new HashMap<>();
entity.put("code", "800");
entity.put("message", "认证失败");
DataBuffer dataBuffer = serverHttpResponse.bufferFactory().wrap(JSON.toJSONString(entity).getBytes());
return serverHttpResponse.writeWith(Flux.just(dataBuffer));
}
//设置过滤器执行顺序,值越小,优先级越高
@Override
public int getOrder() {
return 0;
}
}
- GateWay中的配置文件application.yml如下
server:
port: 9000
# 服务名称
spring:
application:
name: gateway
# 网关配置
cloud:
gateway:
# 全局配置
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 *表示通配 允许所有的域 实际可以填写ip
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
# 网关路由配置
routes:
#id是个列表 有多个路由配置时就写多个id及下面的配置,以下是完整的一个
- id: provider-router # 路由id 唯一标识
uri: lb://provider # 路由地址,动态路由 lb是一个网关的协议
predicates: # 断言
- Path=/provider/**
#eureka配置
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
测试
依次重启服务Eureka、provider、Gateway
- 首先通过网关访问login服务获取token
注意端口号
- 将token放入请求头访问更新用户服务
成功请求后,返回体如下:
但当你稍微改一下token或者不传token,那么你就会得到我们之前重写的响应体
整体来说,很nice
关于Token的一些优化
- 可以通过一些注解来标记哪些方法需要进行token认证
- 在前端,可以在登录获得token后,将其放入cookie,然后使用axios的前置拦截在每次请求的时候将token写入header
- 如果token的数据量很大,可以将token存储到redis,后返回给用户一个UUID,可以在网关拦截中拿到UUID到redis中获取token
这些优化只是一种思想,本人暂时没有试过,大家可以试一试
以上。