一.加密算法
1.1加密和解密
数据加密的基本过程,就是对原来为明文的文件或者数据按某种算法进行处理,使其成为不可读的一段代码。通常称为密文,通过这样的处理,来达到保护数据不被非法人窃取的目的。
1.2加密的分类
加密算法分为对称加密和非对称加密,其中对称加密算法的加密和解密的密钥相同,非对称加密算法的密钥不同,常见的 对称加密 算法主要有 DES
、3DES
、AES
等,常见的非对称算法主要有 RSA
、DSA
等.
1.2.1对称加密
对称加密算法又称为共享密钥加密算法,在对称加密算法中,使用的密钥是同一个,发送和接收双方用这个密钥对数据进行加密和解密,这就要求双方事先都知道这个密钥。
数据加密过程:在对称加密算法中,数据发送方 将 明文 (原始数据) 和 加密密钥 一起经过特殊 加密处理,生成复杂的 加密密文 进行发送。
数据解密过程:数据接收方 收到密文后,若想读取原数据,则需要使用 加密使用的密钥 及相同算法的 逆算法 对加密的密文进行解密,才能使其恢复成 可读明文。
1.2.2非对称加密
非对称加密又称为公开密钥加密算法,它需要两个密钥,一个公开密钥(public key),一个私有密钥(private key) .
因为加密和解密使用的是不同的密钥,所以这种算法称为非对称加密。
如果使用公钥对数据加密,只用对应的私钥才能进行解密。
如果使用私钥对数据加密,只有对应的公钥才能进行解密。
1.2.3对称加密和非对称加密的比较
对称算法:
1.密钥管理:比较难,不适合互联网,一般用于内部系统(密钥泄漏很危险);
2.安全性:中
3.加密速度:快,适合大数据量的加密处理
非对称算法:
1.密钥管理:密钥管理简单(私钥保存好就行了)
2.安全性:高
3.加密速度:比较慢,适合小数据量的加解密或数据签名
1.3消息摘要
消息摘要又称为数字摘要,消息摘要的算法主要特征就是解密的过程不需要密钥,并且加密的数据无法进行解密,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。
比较常见的消息摘要算法有MD5和SHA等…
md5算法
1. 明文: 123
2. 密文: 202cb962ac59075b964b07152d234b70
3. 用户注册的时候: 将用户输入的明文密码经过md5算法之后变成密文,然后保存到数据库
4. 用户登录的时候: 将用户输入的明文密码经过md5算法之后变成密文,跟数据库中的密码进行比较
5. 好处: 由于md5算法的不可逆性,除了用户本人, 别人就算知道密文,也无法得知明文,这样提高了密码的安全性
二.ajax跨越请求
1.前后端分离
在目前的web开发中,前后端有两种协助模式:
1.服务端渲染
2.前后端分离
1.1服务端渲染
特点:
1.所有的web资源都放在一起,由同一个服务器进行统一管理(前后端代码必须放在一起)
2.页面和页面中使用的数据,由服务器组装,最后将完整的html页面响应给客户端
优点:
1.前端耗时少,因为服务器负责动态生成html内容,浏览器只需要直接渲染页面即可
2.有利于seo,因为服务器响应的是完整的html页面内容,所以爬虫更容易爬取获得信息。
缺点:
1.占用服务器资源。即服务器完成html页面的拼接,如果请求较多,会对服务器造成一定的访问压力
2.不利于分工合作,开发效率低。
1.2 前后端分离
特点:
1.依赖于ajax技术
2.后端不提供完整的html页面内容,而是提供一些api接口
3.前端通过ajax调用后端提供的api接口,拿到json数据之后,再在前端进行html页面的拼接
4.前端和后端是两个项目,会分开部署
优点:
1.开发体验好,前端专注于 UI 页面的开发,后端专注于api 的开发,且前端有更多的选择性。
2.用户体验好,Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
3.减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
缺点
不利于seo(解决方案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)
2 .ajax跨域请求受限
2.1跨域请求
跨域请求:当前发起请求的域与改请求指向的资源所在的域不一样。
同域:协议+域名+端口号都相同才是同域。
前后端分离需要前端项目和后端项目分开部署,就是涉及到请求跨域的问题。
比如:前端和后端项目分布部署在服务器A和服务器B,浏览器或者客户端首先获得服务器A的网页,然后从网页上再发起一个请求到服务器B,因为服务器A和服务器B的域不同,那么这个请求就是跨域请求了。
如果这个跨域请求是一个ajax请求,就会受到同源策略的限制。
2.2 同域策略
出于安全的考虑(比如CSRF攻击), 通常浏览器会对上面的跨域请求做出限制,这个限制行为就是同源策略。
CSRF:(Cross-site request forgery),跨站请求伪造
同源策略:是浏览器对跨域请求进行控制的一种基本的安全策略。
同源策略的限制
1. 限制来自不同源的document或脚本,对当前document读取或设置某些属性
2. 禁止ajax直接发起跨域HTTP请求(其实可以发送请求,但是返回的结果被浏览器拦截了,造成请求失败)
3. 禁止cookie跨域
4. <a><form><script><img><link>等带有src属性的标签可以从不同的域加载和执行资源(允许跨域)
根据同源策略, 刚刚我们谈到的前后端分离用的ajax跨域请求,就会受到限制。我们一起来看看以下代码实例。
实例
前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/vue.js"></script>
<script src="js/axios-0.18.0.js"></script>
</head>
<body>
<div id="myapp">
<button @click="send1">发送ajax请求_简单请求</button>
<br>
<button @click="send2">发送ajax请求_非简单请求</button>
<br>
<a href="http://localhost:8080/cors01/MyServlet">超链接</a>
</div>
</body>
<script>
new Vue({
el : "#myapp",
methods : {
send1 : function () {
let params = "username=zs&password=123"
axios.post("http://localhost:8080/cors01/MyServlet",params).then(response => {
console.log(response.data);
});
},
/*
* 非简单请求
* 1. 因为发送json格式参数的ajax请求的请求头是
Content-Type: application/json;charset=UTF-8
超过了简单请求的Content-Type的三个默认值
* */
send2 : function () {
let params = {
"username" : "admin",
"password" : "123"
};
console.log(params);
// axios.defaults.crossDomain = true
axios.post("http://localhost:8080/cors01/MyServlet", params).then(response => {
console.log(response.data);
});
}
}
})
</script>
</html>
后端代码
@WebServlet("/MyServlet")
public class MyServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println("接收到请求");
response.getWriter().println("hello");
}
}
2.3ajax跨域实现方案
虽然在安全层面上同源限制是必要的,但有时同源策略会对我们的合理用途造成影响,为了避免开发的应用受到限制,有多种方式可以绕开同源策略。
常见的ajax跨域请求的实现方式:
1.jsonp
2。代理服务器跨域:
1).前端项目:8080
2).后端项目:8081
3).前端项目给一个代理服务器发送请求,代理服务器允许跨域请求,接收到请求之后,自己伪装成后端项目的域,给后端项目发送请求,避开同源策略的限制
3.跨域资源共享(CORS)
三.跨域身份认证JWT
3.1跨域身份认证的问题
访问互联网服务离不开身份认证, 我们现在学习cookie&session时流程是下面这样子的。
1.用户向服务器发送用户名和密码
2.服务器验证后,再当前对话(session) 里面保存相关数据,比如用户角色,登录时间等。
3.服务器向客户端发送一个sessionId,写入用户的cookie
4.用户随后的每一个请求,都会通过cookie将sessionId传回服务器
5.服务器收到sessionId,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性不好,单机是没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求session共享,每台服务器都能够读取session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点故障。
单点故障(英语:single point of failure,缩写SPOF)是指系统中一点失效,就会让整个系统无法运作的部件,换句话说,单点故障即会整体故障。
第二种方案是服务器干脆不保存 session 数据了,服务端将用户名和密码直接保存在cookie中,并允许跨域请求,每次请求中都携带用户名和密码执行登录。这种方案的优点是让服务端变成无状态,不依赖session,容易扩展。但是这样用户每次访问都需要到数据库中查询,效率比较低。
第三种方案思想跟第二种差不多, 服务器也无需session数据, 服务端将加密过的用户身份标识(token)保存在客户端(cookie中),每次请求都发给服务器进行验证。这种方案不仅容易扩展,而且效率更高(token的验证比查询数据库节省时间)
JWT 就是第三种方案的一个代表。
3.2 JWT介绍
JSON WEB Token(JWT),是一种基于JSON的、用于在网络上声明某种主张的令牌(token), 也是目前最流行的跨域身份认证解决方案之一。
3.2.1 Token的介绍
1.token的引入,token是客户端频繁向服务器请求数据,服务端频繁的去数据库查询用户名和密码并进行比较,判断用户名和密码正确与否,并作出响应提示,在这样的背景下,token应运而生。
2.token的定义:token是由服务端生成的一串字符串(服务器可以鉴别token的有效期和完整性),以作为客户端进行请求的一个令牌,当第一次登录后,服务器生成一个token,并将token返回给客户端,以后客户端只需要带上这个token来请求数据即可,无需再次带上用户名和密码
3.使用token的目的:token的目的就是为了减轻服务器的压力,减少频繁查找数据库。
token可以看成如下Json对象:
{
"userId": "9527",
"到期时间": "2021年7月1日0点0分"
}
3.2.2JWT身份认证流程
1.前端提交用户名和密码,服务器验证。
2.服务器验证通过后,生成token,返回给前端。
3.前端自行保存token(cookie)。
4.前端登录成功之后,每次请求要在请求头带上authorization,值是token
5.服务端会校验token有效性和完整性,校验通过就知道用户身份,然后获得授权允许访问,否则需要前端重新登录。
####### 3.2.3 JWT令牌结构
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9. eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
它是一个很长的字符串,中间用 . 分割成3部分:
1. Header(头部): 用于描述 JWT 的元数据
2. Payload(负载): 用来存放实际需要传递的数据
3. Signature(签名): 对前两部分的签名,防止数据篡改
写成一行,就是下面的样子:
Header.Payload.Signature
header:
Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子。
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名的算法,默认是 HMAC SHA256(写成 HS256,这是一种对称加密算法),typ属性表示这个令牌(token)的类型,jwt令牌统一写JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。
Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,可选非必须。
- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
{
"jti": "657478e3-c1ba-4b93-bdfa-ad59583712d1",
"iss": "heima",
"iat": "1615885608000",
"exp": "1615885609000",
"userId": "9527"
}
前几个是官方字段,最后一个id是自定义的字段, 这个 JSON 对象也要使用 Base64URL 算法转成字符串。
注意:对于已签名的令牌,此信息尽管可以防止篡改,但任何人都可以读取。除非将其加密,否则请勿将机密信息放入JWT的Header和Payload中。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先指定一个密钥,这个密钥只有服务器知道,不能泄漏给用户,然后,使用hander里面的签名算法,按照下面的公式产生签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.
)分隔,就可以返回给用户。
JWT代码的实现
# 步骤
1. pom文件中导入jar包依赖
2. 编写AppJwtUtil的工具类
3. 在用户第一次登录成功时,创建token并响应给浏览器
第一步: pom文件中导入jar包依赖
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
第二步:编写工具类
package com.itheima.case2.utils;
import io.jsonwebtoken.*;
import sun.misc.BASE64Encoder;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;
public class AppJwtUtil {
// TOKEN的有效期一小时(S)
private static final int TOKEN_TIME_OUT = 3_600;
// 密钥,不能泄露
private static final String TOKEN_ENCRY_KEY = "MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY";
/*
获取token方法 :
userId 是要存到token的用户信息, 如有需要可以添加更多
*/
public static String getToken(Integer userId){
Map<String, Object> claimMaps = new HashMap<>();
claimMaps.put("userId",userId);
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.setId(UUID.randomUUID().toString()) //jwt编号:随机产生
.setIssuedAt(new Date(currentTime)) //签发时间
.setIssuer("heima") //签发者信息
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(claimMaps) //自定义
.signWith(SignatureAlgorithm.HS256, generalKey()) //加密方式
.compact();
}
/**
* 获取token中的claims信息
*
* @param token
* @return
*/
private static Jws<Claims> getJws(String token) {
return Jwts.parser()
.setSigningKey(generalKey())
.parseClaimsJws(token);
}
/**
* 获取payload body信息(指的是tocken中Payload部分)
* @param token
* @return Claims 是Map
*/
public static Claims getClaimsBody(String token) {
try {
return getJws(token).getBody();
}catch (ExpiredJwtException e){
return null;
}
}
public static JwsHeader getClaimsHeader(String token) {
try {
return getJws(token).getHeader();
}catch (ExpiredJwtException e){
return null;
}
}
public static String getClaimsSignature(String token) {
try {
return getJws(token).getSignature();
}catch (ExpiredJwtException e){
return null;
}
}
/**
*
* 检查token
* 1. 检查tocken的完整性和有效期
* 2. 检查失败会报错
* 3. 检查成功返回tocken的playload内容
*/
public static Claims checkToken(String token) {
try {
Claims claims = getClaimsBody(token);
if(claims==null){
throw new RuntimeException("token解析失败");
}
return claims;
} catch (ExpiredJwtException ex) {
throw new RuntimeException("token已经失效");
}catch (Exception e){
throw new RuntimeException("token解析失败");
}
}
/**
* 由字符串生成加密key
*
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getEncoder().encode(TOKEN_ENCRY_KEY.getBytes());
/*
使用len的第一个len字节构造来自给定字节数组的key ,从offset开始。
构成密钥的字节是key[offset]和key[offset+len-1]之间的字节。
参数
key - 密钥的密钥材料。 将复制以offset开头的数组的第一个len字节,以防止后续修改。
offset - 密钥材料开始的 key中的偏移量。
len - 密钥材料的长度。
algorithm - 与给定密钥材料关联的密钥算法的名称。 AES是一种对称加密算法
*/
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
//测试
public static void main(String[] args) {
String token = getToken(1);
System.out.println(token);
Claims claims = checkToken(token);
byte[] xx = Base64.getUrlDecoder().decode("eyJqdGkiOiIxZjQwYmVkOC03YjQ5LTRkYmQtYjAyNS02YTM4M2U5ZjM5ZTkiLCJpYXQiOjE2MTgxNTU2MTAsImlzcyI6ImhlaW1hIiwiZXhwIjoxNjE4MTU5MjEwLCJpZCI6MX0");
//{"jti":"1f40bed8-7b49-4dbd-b025-6a383e9f39e9","iat":1618155610,"iss":"heima","exp":1618159210,"id":1}
String s = new String(xx);
System.out.println(s);
byte[] yy = Base64.getUrlDecoder().decode("eyJhbGciOiJIUzI1NiJ9");
//{"jti":"1f40bed8-7b49-4dbd-b025-6a383e9f39e9","iat":1618155610,"iss":"heima","exp":1618159210,"id":1}
String s2 = new String(yy);
//{"alg":"HS256"}
System.out.println(s2);
Object userId = claims.get("userId");
Object iat = claims.get("iat");
Object exp = claims.get("exp");
Object jti = claims.get("jti");
Object sub = claims.get("sub"); //没有定义的信息是获取不到的
System.out.println(jti);
System.out.println(userId);
System.out.println(iat);
System.out.println(exp);
System.out.println(sub);
JwsHeader header = getClaimsHeader(token);
String algorithm = header.getAlgorithm();
System.out.println(algorithm); // HS256
String claimsSignature = getClaimsSignature(token);
System.out.println(claimsSignature);
}
}
第三步: 在用户第一次登录成功时,创建tocken并响应给浏览器
int userId = Integer.parseInt(user.getId());
String token = AppJwtUtil.getToken(userId);
BaseController.printResult(resp,new Result(true,"登录成功!", token));