前后端分離案例
現在把自己當成是前端,要開發一個前后分離的簡單頁面,用於展示學生信息列表
第一步
編寫一個用於展示表格的靜態頁面
<!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"));
}
}