跨域
跨域解决方案
JWT
跨域
目前主流的项目架构,以前是传统的MVC项目,现在主流是前后端分离的项目模式
前端和后端是分离开来的,都是单独的项目,前端可以交给nginx、apache、nodejs,后端交给tomcat去管理
用户先访问前端页面,前端页面再发送请求给后端,后端响应对应的数据给前端,前端再负责渲染数据
后端只负责业务逻辑的处理,最后响应的只是数据,不再负责页面的渲染。
因为涉及到两台服务器了,两个服务器的IP和端口至少有一个不一样。协议、IP、端口只要有一个不一样了,就会出现跨域的问题,这个ajax就无法正常的访问了,会报错
Access to XMLHttpRequest at 'http://localhost:8080/userManager/address/getChildren?parentId=0' from origin 'http://localhost:9999' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
出现这个错误的原因主要是因为浏览器的同源策略限制导致的
同源策略/SOP(same origin policy) 是一种约定,是浏览器最核心也是最基本的安全功能,如果少了同源策略,则浏览器的正常功能都可能会受影响。web安全就是基于同源策略的,浏览器只是同源策略的一种实现,假设缺少了,可能遭受 XSS,CSRF 等攻击。
什么叫不同源:只要协议、主机、端口有一个不一样就是不同源,浏览器上只要不同源ajax就会报错
如何解决跨域的问题?
跨域解决方案
1. JSONP解决(重点)
2. document.domain + iframe解决
3. location.hash + iframe
4. window.name + iframe
5. postMessage跨域
6. 跨域资源共享(CORS) (重点)
7. nginx代理跨域
8. nodejs中间件代理跨域
9. websocket 跨域
JSONP跨域
jsonp的原理就是利用了script标签不受同源策略影响,通过script标签的src属性,发送一个带有callback参数的get请求,服务器收到参数并将数据拼凑到callback函数中,响应给客户端,客户端将callback函数解释执行,从而前端拿到后端的数据
原生js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域请求演示1(原生js实现)</title>
</head>
<body>
<form action="/user/register1" method="post">
<span>用户名</span><input type="text" name="username"><span></span><br>
<span>密码</span><input type="text" name="password"><span></span><br>
<input type="submit" value="注册">
</form>
<script>
var element = document.querySelector("input[name='username']");
let espan =document.querySelector("input[name=username] + span");
//跨域请求解决方法(原生js)
function callBack(str){
// var data =JSON.parse(str);
// let data = str; //不用进行转换了
console.log(str);
if (str.code==0){
element.style.color = "red";
} else if(str.code==1){
element.style.color = "green";
}
espan.innerHTML=str.msg;
}
element.onblur = function () {
let htmlElement = document.createElement("script");
document.body.appendChild(htmlElement);
//参数要传入一个回调的方法名 src 相当于是一个get请求 不受同源策略的影响
htmlElement.src="http://localhost:8080/user/register?username="+element.value+"&method=callBack";
//需要结合后台一起解决同源问题
}
</script>
</body>
</html>
=
public void register(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
//跨域解决方案1
response.setContentType("application/javascript;charset=utf-8");
//同源方法
String method = request.getParameter("method");
Result result = new Result();
if ("lijijun".equals(username)){
result.setCode(0);
result.setMsg("用户已存在");
}else{
result.setCode(1);
result.setMsg("√");
}
String content = JSON.toJSONString(result);
System.out.println(content);
PrintWriter writer = response.getWriter();
//同源策略解决 返回的是一个方法的调用
writer.write(method+"("+content+")");
}
原生js实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>三级联动</title>
</head>
<body>
<div id="content">
<select name="" onchange="getChildren('shi',this.value)" id="sheng"></select>
<select name="" onchange="getChildren('xian',this.value)" id="shi"></select>
<select name="" id="xian"></select>
</div>
<script>
function getChildren(selectId,parentId){
let script = document.createElement("script");
script.src = "http://localhost:8080/userManager/address/getChildren2?parentId=" + parentId + "&callback=showAddress&selectId=" + selectId;
document.head.appendChild(script);//才会发送请求出去
}
function showAddress(selectId,result){
// let result = JSON.parse(str);
var select = document.getElementById(selectId);
if(result.code == 0){//查询成功
select.innerHTML = "";
for (let i = 0;i < result.data.length;i++){
select.innerHTML += "<option value='"+result.data[i].id+"'>"+result.data[i].name+"</option>";
}
if(selectId == "sheng"){
getChildren("shi",select.value);
}else if(selectId == "shi"){
getChildren("xian",select.value);
}
}else{
alert(result.msg);
}
}
getChildren("sheng",0);
</script>
</body>
</html>
后台
private void getChildren2(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String parentId = request.getParameter("parentId");
String callback = request.getParameter("callback");
String selectId = request.getParameter("selectId");
List<Address> addresses = addressService.selectChildren(Integer.parseInt(parentId));
Result<List<Address>> result = new Result<>();
if(addresses != null && !addresses.isEmpty()){
result.setCode(0);
result.setData(addresses);
}else{
result.setCode(1);
result.setMsg("查询失败");
}
response.setContentType("application/javascript;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(callback + "('" + selectId + "'," + JSON.toJSONString(result) + ")");
}
jquery的jsonp实现
==
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域请求演示1(jquery的jsonp实现)</title>
</head>
<body>
<form action="/user/register1" method="post">
<span>用户名</span><input type="text" name="username"><span></span><br>
<span>密码</span><input type="text" name="password"><span></span><br>
<input type="submit" value="注册">
</form>
<script src="../js/jquery-3.6.0.js"></script>
<script>
$("input[name='username']").blur(function () {
let value = this.value;
if (""==value)
return;
$.ajax({
url:"http://localhost:8080/user/register",
type:"get",
data:{username:value},
dataType:"jsonp",
success:function (result) { //如果服务器返回的是json格式的字符串 jq 会自动帮我们转换成json对象
let title =$("input[name='username'] + span");
if (result.code==0){
title.css("color","red");
}else if(result.code==1){
title.css("color","black");
}
title.text(result.msg);
},
error:function () {
},
complete:function () {
}
})
});
</script>
</body>
</html>
=
public void register(HttpServletRequest request, HttpServletResponse response) throws IOException {
String username = request.getParameter("username");
response.setContentType("application/javascript;charset=utf-8");
String method = request.getParameter("callback");//默认的回调函数名
Result result = new Result();
if ("lijijun".equals(username)){
result.setCode(0);
result.setMsg("用户已存在");
}else{
result.setCode(1);
result.setMsg("√");
}
String content = JSON.toJSONString(result);
System.out.println(content);
PrintWriter writer = response.getWriter();
//同源策略解决 返回的是一个方法的调用
writer.write(method+"("+content+")");
}
====================================
function getChildren(selectId,parentId){
$.ajax({
url:"http://localhost:8080/userManager/address/getChildren3",
type:"get",//jsonp的type只能是get
data:{parentId:parentId},
dataType:"jsonp",//这里设置成jsonp
success:function (result){
if(result.code == 0){//查询成功
var select = document.getElementById(selectId);
select.innerHTML = "";
for (let i = 0;i < result.data.length;i++){
select.innerHTML += "<option value='"+result.data[i].id+"'>"+result.data[i].name+"</option>";
}
if(selectId == "sheng"){
getChildren("shi",select.value);
}else if(selectId == "shi"){
getChildren("xian",select.value);
}
}else{
alert(result.msg);
}
}
})
}
后台
private void getChildren3(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String parentId = request.getParameter("parentId");
String callback = request.getParameter("callback");
List<Address> addresses = addressService.selectChildren(Integer.parseInt(parentId));
Result<List<Address>> result = new Result<>();
if(addresses != null && !addresses.isEmpty()){
result.setCode(0);
result.setData(addresses);
}else{
result.setCode(1);
result.setMsg("查询失败");
}
response.setContentType("application/javascript;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(callback + "(" + JSON.toJSONString(result) + ")");
}
jsonp的实现是需要后台的配合,因为script的src发送请求是get请求,所以限制的jsonp的使用,jsonp只支持get请求,如果要在一些复杂情况下jsonp的使用情况就局限性很强。
CORS
CORS 跨域资源共享
CORS 是一个W3C的标准,全称是跨域资源共享(CROSS-origin resources sharing)
它允许浏览器向跨域的服务器发送一个XMLHttpRequest请求,从而克服了ajax只能同源使用的限制。CORS需要浏览器和服务器同时支持才可以。目前主流的浏览器都是支持的,除了IE10以下的版本。
浏览器将CORS的跨域请求分为简单请求和非简单请求
只要满足如下的两个条件就是简单请求
1. 请求方式是 GET\POST\HEAD
2. 请求头是 Accept、Accept-language、Content-language、Content-Type(值仅限于:application/x-www-form-urldecoded、multipart/form-data、text-plain)
只要不满足如上的两个条件都是非简单请求,浏览器针对两种请求的处理方式是不一样的
GET /cors HTTP/1.1
Origin: http://localhost //请求源
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面的请求头中:Origin 表示这个请求从哪个源(协议+主机+端口)来的,服务器根据这个源决定是否同意这个请求。
CORS的响应都是以Access-Control-开头的
1. Access-Control-Allow-Origin :必须的
他的值可以是一个*,表示接收任意域名的请求,也可以是具体的值 http://localhost 表示只接受 来自这个地方的请求
2. Access-Control-Allow-Credentials : 可选的
他的值是一个boolean类型,如果为true,表示客户端允许发送cookie,默认情况下Cookie是不会包含在跨域的请求中。也就是如果不想携带cookie不设置这个头即可。
3. Access-Control-Expose-Headers : 可选的
CORS 请求是: XMLHttpRequest 对象的 getResponseHeader 方法默认只能获取6个响应头:Cache-Control、Content-language、Content-Type、Expirse、Last-Modified、Pragma 。如果想让客户端获取更多的响应头就必须在 Access-Control-Expose-Headers 的值中进行指定。然后就可以在getResponseHeader中获取到。
实现CORS 一般只需要在服务器配置一个过滤器即可,因为主流的浏览器都是支持的。CORS一般会发送两次请求过来,第一次发送OPTIONS 请求,获取对应的响应头,第二次才会发送执行任务的请求。
=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>跨域请求演示(CORS)</title>
</head>
<body>
<form action="/user/register1" method="post">
<span>用户名</span><input type="text" name="username"><span></span><br>
<span>密码</span><input type="text" name="password"><span></span><br>
<input type="submit" value="注册">
</form>
<script src="../js/jquery-3.6.0.js"></script>
<script>
$("input[name='username']").blur(function () {
let value = this.value;
if (""==value)
return;
$.ajax({
url:"http://localhost:8080/user/register",
type:"get",
data:{username:value},
dataType:"json",
success:function (result) { //如果服务器返回的是json格式的字符串 jq 会自动帮我们转换成json对象
let title =$("input[name='username'] + span");
if (result.code==0){
title.css("color","red");
}else if(result.code==1){
title.css("color","black");
}
title.text(result.msg);
},
error:function () {
},
complete:function () {
}
})
});
</script>
</body>
</html>
=
过滤器
package com.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(filterName = "CORSFilter",urlPatterns = "/*")
public class CORSFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//设置响应头
//设置哪些地方的请求可以跨域 * 表示任意请求 多个值可以用,隔开
//"http://localhost:9999,http://localhost:8888"
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
System.out.println("Origin:"+request.getHeader("Origin"));
//是否允许携带cookie
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials","true"); // Credentials资格证书
// 允许的请求方式
response.setHeader("Access-Control-Allow-Methods","GET,POST,HEAD,OPTIONS");
//允许的请求头
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type,Token,Accept, Connection, User-Agent, Cookie");
//允许浏览器获取的响应头
response.setHeader("Access-Control-Expose-Headers","Token");
// 超时时间
response.setHeader("Access-Control-Max-Age","362880");
if("OPTIONS".equalsIgnoreCase(request.getMethod())){
return;
}
chain.doFilter(req, resp);
}
public void init(FilterConfig config) throws ServletException {
}
}
=
public void register(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("application/json;charset=utf-8");
String username = request.getParameter("username");
String method = request.getParameter("method");
Result result = new Result();
if ("lijijun".equals(username)){
result.setCode(0);
result.setMsg("用户已存在");
}else{
result.setCode(1);
result.setMsg("√");
}
String content = JSON.toJSONString(result);
System.out.println(content);
PrintWriter writer = response.getWriter();
writer.write(content);
}
=======================
package com.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter("/*")
public class CORSFilter implements Filter {
public void destroy() {
}
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
//设置响应头
//设置哪些地方的请求可以跨域 * 表示任意请求 多个值可以用,隔开
//"http://localhost:9999,http://localhost:8888"
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
//是否允许携带cookie
response.setHeader("Access-Control-Allow-Credentials","true");
// 允许的请求方式
response.setHeader("Access-Control-Allow-Methods","GET,POST,HEAD,OPTIONS");
//允许的请求头
response.setHeader("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type,Token,Accept, Connection, User-Agent, Cookie");
//允许浏览器获取的响应头
response.setHeader("Access-Control-Expose-Headers","Token");
// 超时时间
response.setHeader("Access-Control-Max-Age","362880");
if("OPTIONS".equalsIgnoreCase(request.getMethod())){
return;
}
chain.doFilter(request, response);
}
public void init(FilterConfig config) throws ServletException {
}
}
扩展
http://ruanyifeng.com/blog/2016/04/cors.html
JWT
JWT的全称是json web token 。是为了在网络应用环境中传递声明而执行的一种基于json的开发标准(RFC7519).。这个token被设置的紧凑而安全。特别适用于分布式单点登录(SSO)场景。JWT的声明一般是在身份提供者【就是统一进行登录校验的一套系统】和服务器提供者【就是各个不同的系统平台】之间传递被认证的用户身份信息。以便于从资源服务器获取资源。该token可以直接用于被认证和加密。
流程上:
1. 用户适用用户名和密码登录服务器
2. 服务器根据用户名和密码验证身份
3. 验证成功以后,服务器使用jwt**产生一个token发送给客户端**
4. 客户端将token保存下载,在以后的**每一次请求中将token带上**
5. 服务器可以使用一个过滤器验证token是否有效,如果有效则继续执行,无效则去登录
6. 这个token必须在每次请求的时候传递给服务器,发送给服务器的时候可以保存在请求头中,服务器一般需要支持CORS策略
JWT 是由三段信息构成的,将三段信息由.连接在一起就构成了JWT的字符串:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2OTI3NzAxLTNmNmMtNDA2NS1hOGFiLWNiOWUwMzUzOTViMiIsImlhdCI6MTYxODQ2OTMxMiwiZXhwIjoxNjE4NDcyOTEyfQ.mo3xwud0tH9Tjx2UQUXZohPUBgZhbGsjmG6yUGcrODU
JWT的三段构成:
第一部分:header
jwt的header部分承载了两部分信息:
声明类型:JWT
加密算法:HS256
完整头部信息如下:
{"typ":"JWT","alg":"HS256"}
将这一部分头部信息使用Base64加密后得到
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
第二部分:Payload(载荷)
载荷就是存放有效信息的地方,这个信息中包含了三部分
公有的声明
公有的声明可以添加任何信息进去,一般添加用户相关的信息或者业务相关的信息,不建议添加敏感信息,因为这部分用户可以在客户端解密
私有的声明
是提供者和消费者共同定义的声明,一般也不建议存放敏感信息,也可以解密的
注册的声明 (建议但不强制)
iss: jwt的签发者
sub: jwt面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个时间一定要大于签发时间
iat: jwt的签发时间
nbf: 定义在什么时间之前,jwt是不可用的
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
定义一个载荷
{"sub":"1234567890","name":"John Doe","admin":true,"jti":"c6927701-3f6c-4065-a8ab-cb9e035395b2","iat":1618469312,"exp":1618472912}
使用Base64加密后如下
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImp0aSI6ImM2OTI3NzAxLTNmNmMtNDA2NS1hOGFiLWNiOWUwMzUzOTViMiIsImlhdCI6MTYxODQ2OTMxMiwiZXhwIjoxNjE4NDcyOTEyfQ
第三部分:签证Signing
这部分的信息是由三部分组成
header(加密后的)
payload(加密后的)
secret
将上面的三部分使用.连接成字符串,然后将这个字符串使用header中定义的加密算法进行加盐加密,得到第三部分
加密方式如下:
HS256(Base64(header).Base64(payload),secret)
最后得到 mo3xwud0tH9Tjx2UQUXZohPUBgZhbGsjmG6yUGcrODU
需要注意的是secret 是保存在服务器的,jwt的生成也是在服务器生成的,所以secret可以看做进行jwt的签发的盐值。所以secret算是服务器的私钥,在任何情况下都不能将私钥暴露出去。一旦客户端知道了这个私钥,那就意味着客户端可以自我签发jwt了。
使用jwt
添加依赖
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
创建jwt的工具类
package com.utils;
import com.auth0.jwt.JWT;
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;
public class JWTUtil {
private static final long EXP = -24 * 60 * 60 * 1000;//过期时间
/**
* 生成token
* @param username 账号信息
* @param secret 秘钥
* @return 生成的token
*/
public static String sign(String username,String secret){
Date date = new Date(System.currentTimeMillis() + EXP);
Algorithm algorithm = Algorithm.HMAC256(secret);
//附带username信息的token
return JWT.create().withClaim("username",username).withExpiresAt(date).sign(algorithm);
}
/**
* 从token中获取账号
* @param token
* @return
*/
public static String getUsername(String token){
try {
DecodedJWT decodedJWT = JWT.decode(token);
return decodedJWT.getClaim("username").asString();
}catch (JWTDecodeException e){
return null;
}
}
/**
* 校验token是否正确
* @param token
* @param username
* @param secret
* @return
*/
public static boolean verify(String token,String username,String secret){
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier jwtVerifier = JWT.require(algorithm).withClaim("username", username).build();
DecodedJWT verify = jwtVerifier.verify(token);
return true;
} catch (Exception e){
return false;
}
}
/**
* 检查token是否过期
* @param token
* @return
*/
public static boolean isExpires(String token){
return System.currentTimeMillis() > JWT.decode(token).getExpiresAt().getTime();
}
}
jwt扩展
https://blog.csdn.net/Top_L398/article/details/109361680
https://blog.csdn.net/weixin_45070175/article/details/118559272
JSON Web Token(JSON Web令牌)
是一个开放标准(rfc7519),它定义了一种紧凑的、自包含的方式,用于在各方之间以JSON对象安全地传输信息。此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密〈使用HNAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。
————————————————
版权声明:本文为CSDN博主「一支有理想的月月鸟」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Top_L398/article/details/109361680