前后端分离之 跨域和JWT
前后端分离案例
现在把自己当成是前端,要开发一个前后分离的简单页面,用于展示学生信息列表
第一步
编写一个用于展示表格的静态页面
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<table id="tab" border="1">
<tr>
<th>编号</th>
<th>名字</th>
<th>年龄</th>
<th>性别</th>
</tr>
</table>
<button onclick="req()">请求数据</button>
<img id="img" />
</body>
</html>
不启动tomcat直接在编辑器中打开即可访问,测试他就是一个静态网页,而我们的编辑器就是一个HTTP服务器,可以响应静态网页
第二步
引入jquery使得ajax编写更方便
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
第三步
编写ajax,向服务器发送请求
第四步
将数据展示到页面上
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
<table id="tab" border="1">
<tr>
<th>编号</th>
<th>名字</th>
<th>年龄</th>
<th>性别</th>
</tr>
</table>
<button onclick="req()">请求数据</button>
<img id="img" />
</body>
<script>
function req(){
document.getElementById("img").src = "img/timg.gif";
$.ajax({
url:"http://localhost:8080/MyServer/getData",
success:function(data){
console.log(data);
document.body.insertAdjacentHTML("beforeend","<h1>%</h1>".replace("%",data));
document.getElementById("img").src = "";
},
error:function(err){
console.log(err);
document.getElementById("img").src = "";
}
});
}
</script>
</html>
现在身份切换回后端开发用于获取表格数据的接口
- 创建web项目
- 创建Servlet
- 引入fastjson
- 创建一个bean类
- 创建一堆bean放入列表中
- 将列表转为json字符串 返回给前端
Servlet代码
package com.kkb;
import java.io.IOException;
public class AServlet extends javax.servlet.http.HttpServlet {
protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
}
protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
String s = "{\"name\":\"jack\"}";
response.getWriter().println(s);
}
}
启动服务,测试访问,会发现页面上没有显示服务器返回的结果….
跨越问题
打开浏览器检查页面会发现没有输出服务器返回的消息而是,出现了一个错误信息,这就是前后端分离最常见的跨越问题
什么是跨域
跨越为题之所以产生是因为浏览器都遵循了同源策略
同源策略:
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,浏览器会先发送OPTION请求进行预检查,判断服务器是否允许跨域,如果允许才发送真正的请求,否则抛出异常。
简单的说:
同源策略浏览器的核心安全机制,其不允许在页面中解析执行来自其他服务器数据
如何判断是否跨域
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域
同源限制:
-
无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
-
无法向非同源地址发送 AJAX 请求
什么时候产生跨域问题:
浏览器在解析执行一个网页时,如果页面中的js代码请求了另一个非同源的资源,则会产生跨越问题
而浏览器直接跳转另一个非同源的地址时不会有跨域问题
解决跨越问题
既然禁止跨域问题时浏览器的行为,那么只需要设置浏览器运行解析跨域请求的数据即可,但是这个设置必须放在服务器端,由服务器端来判断对方是否可信任
在响应头中添加一个字段,告诉浏览器,某个服务器是可信的
package com.kkb;
import java.io.IOException;
public class AServlet extends javax.servlet.http.HttpServlet {
protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
}
protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
response.setHeader("Access-Control-Allow-Origin","*");
String s = "{\"name\":\"jack\"}";
response.getWriter().println(s);
}
}
其值跨越式某个指定的域名,也可以是*表示信任所有地址
其他相关设置
//指定允许其他域名访问
'Access-Control-Allow-Origin:http://XXX.XXX.XXX'//一般用法(*,指定域,动态设置),注意*不允许携带认证头和cookies
//预检查间隔时间
'Access-Control-Max-Age: 1800'
//允许的请求类型
'Access-Control-Allow-Methods:GET,POST,PUT,POST'
//列出必须携带的字段
'Access-Control-Allow-Headers:x-requested-with,content-type'
解决了跨越问题后再来完善上面的案例
Servlet代码:
package com.kkb;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
public class AServlet extends javax.servlet.http.HttpServlet {
protected void doPost(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
}
protected void doGet(javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws javax.servlet.ServletException, IOException {
//允许来自任何主机的跨越访问
response.setHeader("Access-Control-Allow-Origin","*");
//设置响应类型为json数据
response.setContentType("application/json;charset=utf-8");
//学生信息
ArrayList<Student> students = new ArrayList<>();
Student stu1 = new Student("s1","jack",20,"man");
Student stu2 = new Student("s2","tom",22,"girl");
Student stu3 = new Student("s3","jerry",10,"woman");
Student stu4 = new Student("s4","scot",24,"boy");
students.add(stu1);
students.add(stu2);
students.add(stu3);
students.add(stu4);
response.getWriter().println(JSON.toJSONString(JSON.toJSONString(students)));
}
}
HTML代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
</head>
<body>
<table id="tab" border="1">
<tr>
<th>编号</th>
<th>名字</th>
<th>年龄</th>
<th>性别</th>
</tr>
</table>
<button onclick="req()">请求数据</button>
<img id="img" />
</body>
<script>
function req(){
document.getElementById("img").src = "img/timg.gif";
$.ajax({
url:"http://localhost:8080/MyServer/getData",
success:function(data){
data = JSON.parse(data)
console.log(data)
for (var i = 0; i < data.length; i++) {
a = data[i];
var row = "<tr><td>id</td><td>name</td><td>age</td><td>gender</td></tr>"
row = row.replace("id",a.id);
row = row.replace("name",a.name);
row = row.replace("age",a.age);
row = row.replace("gender",a.gender);
document.getElementById("tab").insertAdjacentHTML("beforeend",row);
}
document.getElementById("img").src = "";
},
error:function(err){
console.log(err);
document.getElementById("img").src = "";
}
});
}
</script>
</html>
一个简单的前后端分离项目就搞定了
cookie跨域
默认情况下cookie是不允许跨域传输的.可以通过以下方式来解决
第一步
浏览器端设置允许cookie跨域
第二步
服务器端在响应中添加字段,说明允许cookie跨域
该值只能是true,为false无效,默认为false
#'Access-Control-Allow-Credentials:true'
第三步:
需要确保后台设置允许跨域的地址不是*
,必须指定为一个明确的地址,像下面这样;
response.setHeader("Access-Control-Allow-Origin", "http://localhost:8081");
动态设置允许跨域:
此时会产生一个新的问题,因为该字段无法添加多个地址,无法配置多个不同的源主机允许跨域访问,我们也可以动态设置,判断主机地址是否是允许的,若允许则允许访问
//允许访问的列表
ArrayList<String> hosts = new ArrayList<>();
hosts.add("http://localhost:8081");
hosts.add("http://127.0.0.1:8081");
//判断是否是允许的地址
if (hosts.contains(request.getHeader("Origin"))){
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials","true");
}
状态保持问题
在传统的项目中我们利用,session+cookie来保持用户的登录状态,但这在前后端分离项目中出现了问题;
sessionid是使用cookie存储在客户端的,而cookie遵守同源策略,只在同源的请求中有效,这就导致了问题出现:
前后端分离后,session+cookie的问题
- 前后端分离后静态资源完全可能(而且经常....)部署到另一个域下,导致cookie失效,例如这样:
在www.baidu.com
中设置的cookie是不会自动发送到cloud.baodu.com
的
虽然我们可以在cookie中指定domain来解决,但是cookie必须针对性的设置作用域
这对于有多个不同域要共享cookie时,可操作性差,难以维护
-
上述问题出现在前后端分离的web项目中,对于前后端分离的原生CS结构项目而言,很多客户端默认是不处理session和cookie的,需要进行相应的设置
-
**在分布式或集群的项目中,共享session和cookie也是一大问题,必须引入第三方来完成session的存储和共享(也可通过中间层做cookie转发如Nginx,Node.js),这也是传统单体服务无法支持分布式和集群的问题所在 **
正因为有这些问题,导致session+cookie的方式在某些项目中使用起来变得很麻烦,这时候就需要一种新的状态维持的方式;
JWT
JWT全称(json WEB token),是基于json数据结构的数据验证方式,其本质是对json数据进行加密后产生的字符串
JWT的亮点:
- 安全
- 稳定
- 易用
- 支持 JSON
JWT是如何做的?
回顾,之所以使用session和cookie是因为HTTP的无状态性质,导致服务器无法识别多次请求是否来自同一个用户
JWT可以对用户信息进行加密生成一个字符串,下发到客户端,客户端在后续请求中携带该字符串,服务器解析后取出用户信息,从而完成用户身份的识别,如下图:
传统单体式与分布式/集群的区别
JWT的数据结构
JWT是一个很长的字符串,分为成三个部分,中间用点.
隔开注意; JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
三个组成部分如下:
- Header(头部)
- Payload(负载)
- Signature(签名)
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,例如签名算法等,像下面这样:
{
"alg": "HS256",
"typ": "JWT"
}
alg
属性表示签名的算法,默认是 HMAC SHA256;
typ
属性表示这个令牌(token)的类型统一写为JWT
最后使用base64URL算法转换为字符串;
Payload
Payload 部分也是一个 JSON 对象,用来存放真正需要传递的数据,JWT 规定了7个保留字段,如下:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
服务器需要在Payload中添加用于识别用户身份的数据,也是键值对形式,注意不可使用保留字段,像下面这样
{
"sub": "test JWT",
"name": "jerry",
"isadmin": true
}
Payload同样使用base64URL算法转换为字符串;
强调:Payload数据默是不加密的,攻击者可以通过相同的方式解析获取
若要将用户的关键数据放入其中则必须对其进行额外的加密
Signature
部分是对前两部分的签名,防止数据篡改。
签名时需要指定一个密钥(secret)。密钥只有服务器才知道,不能泄露给用户。然后使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的方式产生签名。
signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
最后把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点".
分隔返回给用户;
总结:
JWT的优点:
满足REST Full的无状态要求(为了提高系统的扩展性,REST要求所有信息由请求端来提供,如此才使得JWT成为了分布式,集群构架的首选方式)
在分布式,集群系统,前后端分离中使身份验证变得非常简单
可用于其他数据交换
合理的使用可减少数据库查询次数
JWT的缺点:
对于同样的数据JWT整体大小超过cookie,这会增加网络开销
服务器每次解析JWT都需要再次执行对应的算法,这将增加系统开销
在传统单体服务,和WEBApp形式的前后端分离项目中使用JWT反而不如Session+cookie
注意事项:
- JWT的payload部分是不加密的,如果要放入关键数据则必须对其进行加密,或是将最后的JWT整体加密
- JWT本身用于认证,一旦泄露,则任何人都可以使用该令牌,获得其包含的所有权限,为了提高安全性.JWT的有效期不应太长,对于一些非常权限,建议在请求时再次验证
Java中JWT的使用:
懂得原理了后我们完全可以自己来实现,但是没有必要,下面是目前用的较多的一个开源库
下载地址:https://www.mvnjar.com/com.auth0/java-jwt/3.4.0/detail.html
依赖:https://github.com/yangyuanhu/jwtAndDependenc
使用JWT的步骤总体分为三步
-
生成JWT
-
验证JWT
-
提取数据
案例:
package com.kkb;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTTool {
public static final String secretKey = "askjdksadjkasdakjshdjkasdkAakjshdjasdjs";
public String getJWTWithHMAC256(String subject, Map<String, String> payload, String secretKey){
//指定JWT所使用的签名算法
Algorithm algorithm = Algorithm.HMAC256(secretKey);
//支持链式调用
JWTCreator.Builder token = JWT.create()//创建token
.withIssuer("com.kkb")//指定签发人
.withSubject(subject)//指定主体数据
.withExpiresAt(new Date(new Date().getTime()+(1000*20)));
//添加负载数据
for (String key: payload.keySet()) {
token.withClaim(key,payload.get(key));
}
return token.sign(algorithm);
}
public boolean verifyTokenWithHMAC256(String token,String secretKey){
try{
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("com.kkb")
.build();
verifier.verify(token);
return true;
}catch (JWTVerificationException e){
e.printStackTrace();
return false;
}
}
public static void main(String[] args) {
JWTTool jwtTool = new JWTTool();
//要添加到token中的数据
HashMap<String,String> data = new HashMap<>();
data.put("user","jerry");
data.put("isAdmin","true");
//生成token
String token = jwtTool.getJWTWithHMAC256("jerry test",data, secretKey);
System.out.println(token);
//验证token
//If the token has an invalid signature or the Claim requirement is not met
if (jwtTool.verifyTokenWithHMAC256(token,secretKey)){
System.out.println("token 有效");
try{
//提取数据
DecodedJWT decode = JWT.decode(token);
System.out.println("主题:"+decode.getSubject());
System.out.println("签发:"+decode.getIssuer());
System.out.println("有效期"+decode.getExpiresAt());
System.out.println("user: "+decode.getClaim("user").asString());
System.out.println("isAdmin:"+decode.getClaim("isAdmin").asString());
}catch (JWTDecodeException e){
//If the token has an invalid syntax or the header or payload are not JSONs,
System.out.println("解析token失败");
}
}else {
System.out.println("token 无效");
}
}
}
开动小脑袋,把JWT集成到上面的前后端分离项目中实现用户登录注册吧;
MD5
MD5属于hash算法
HASH翻译做散列、杂凑,或音译为哈希,是把任意长度的输入,通过散列算法变换成固定长度的输出
特点:
无法通过摘要信息还原出原始数据 无法解密
算法有很多种,但是无论哪一种hash算法,最终产生的都是一个固定长度的输出
对于任何长度的输入数据,在相同算法下,都有着相同长度输出
对于相同输入和相同算法,产生的结果一定相同
(极小概率出现,不同输入 相同算法产生相同结果)
MD5解密是如何实现的?
通过撞库
原理:提前把输入数据和输出结果做一个映射关系, 撞库时就是拿着结果(key)去查数据库
只要输入数据稍微复杂 撞库就失败了
作用:
1,加密 需要强调的是,加密的结果是无法反解的 如何判断密码正确呢?
存储的是密文, 查的时候 使用相同的算法加密,比较加密后的结果是否相同
2.数据校验 可以用校验数据是否被篡改 (游戏客户端MD5校验)
加盐
123456 数据太简单 容易被撞库 ,解决方法就是加盐,(就是一串乱七八糟的字符串,别人不可能猜到的字符串
123456+暗示可能就卡死百度金矿萨贝达即可洒不记得把撒娇的比较快萨芬的好处nbf)
MD5加密
package com.kkb.misc.util;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MD5Tool {
public static String getMD5(String text, String salt,String algorithm) {
try {
MessageDigest instance = MessageDigest.getInstance(algorithm!=null?algorithm:"MD5");
instance.update(text.getBytes("UTF-8"));
if (salt != null) {
instance.update(salt.getBytes("UTF-8"));
}
byte[] digests = instance.digest();
StringBuilder sb = new StringBuilder();
//字节转16进制
for (byte digest : digests) {
String hex = Integer.toHexString(digest & 0xFF);
if (hex.length() < 2) {
sb.append(0);
}
sb.append(hex);
}
//转字符串
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String getMD5(String text) {
return getMD5(text,null);
}
public static String getMD5(String text,String salt) {
return getMD5(text,salt,null);
}
public static void main(String[] args) {
System.out.println(getMD5("abc"));
System.out.println(getMD5("abc","akjKJHKJAJKSHJKAHSJANJNJANS"));
System.out.println(getMD5("abc","akjKJHKJAJKSHJKAHSJANJNJANS","SHA-256"));
System.out.println(getMD5("abc",null,"SHA-512"));
}
}