初識單點登入及JWT實現
單點登入
多系統,單一位置登入,實現多系統同時登入的一種技術
(三方登入:某系統使用其他系統的使用者,實現本系統登入的方式。如微信登入、支付寶登入)
單點登入一般是用於互相授信的系統,實現單一位置登入,全系統有效
一、Session跨域
所謂 Session 跨域就是摒棄了系統提供的 Session ,而使用自定義的類似 Session 的機制來儲存客戶端資料的一種解決方案。
如:通過設定 cookie 的 domain 來實現 cookie 的跨域傳遞。在 cookie 中傳遞一個自定義的 session_id。這個 session_id 是客戶端的唯一標記,將這個標記作為key,將客戶需要儲存的資料作為value,在服務端進行儲存(資料庫儲存或nosql儲存)。這種機制就是 Session 的跨域解決。
什麼為跨域:客戶端請求的時候,請求的伺服器,不是同一個IP、埠、域名、主機名的時候,都稱為跨域。
什麼是域:在應用模型中,一個完整的、有獨立訪問路徑的功能集合成為一個域。
如:百度稱為一個應用或系統,其下有若干個域,如搜尋引擎(www.baidu.com),百度貼吧(tie.baidu.com),百度知道(zhidao.baidu.com)等。
有時也稱為多級域名。域的劃分:以IP、埠、域名、主機名為標準,實現劃分。
二、Spring Session 共享
spring-session 技術是 spring 提供的用於處理叢集會話共享的解決方案。spring-session技術是將使用者 session 資料儲存到第三方容器中,如資料庫。
Spring-session 技術是解決同域名下的多伺服器叢集 session 共享問題的,不能解決跨域 Session 共享問題
三、Nginx Session 共享
nginx中的 ip_hash 技術能夠將某個 ip 的請求定向到同一臺後端,這樣一來這個ip下的某個客戶端和某個後端就能建立起穩固的session,ip_hash是在upstream配置中定義的
四、Token身份認證
使用基於 Token 的身份驗證方法,在服務端不需要儲存使用者的登入記錄,大概流程如下:
1)客戶端使用使用者名稱、密碼請求登入
2)服務端收到請求、去驗證使用者名稱與密碼
3)驗證成功後,服務端會簽發y一個 token ,再把這個 token 傳送給客戶端
4)客戶端收到 token 以後可以把它儲存起來,比如放在 cookie 裡或者 Local Storage裡
5)客戶端每次向伺服器請求資源的時候需要帶著伺服器簽發的 token
6)服務端收到請求,然後去驗證客戶端請求裡面帶著的 token,如果驗證成功,就向客戶端返回請求的資料
使用token的優勢:
無狀態、可擴充套件:
在客戶端儲存的 token 是無狀態的,並且能夠被擴充套件,基於這種無狀態和不儲存session資訊,負載均衡器能夠將使用者資訊從一個服務傳到其他伺服器上。
安全性:
請求中傳送token而不再發送cookie能夠防止CSRF(跨域請求偽造)。即使在客戶端使用cookie儲存token。cookie也僅僅是一個儲存機制而不是用於認證。
不將資訊儲存在session中,讓我們少了對session的操作。
五、JSON Web Token(JWT)機制
JWT是一種緊湊且自包含的,用於在多方傳遞 json 物件的技術。傳遞的資料可以使用數字簽名增加其安全性。可以使用HMAC加密演算法或RSA公鑰/私鑰加密方式。
緊湊:資料小,可以通過URL、POST引數,請求頭髮送,且資料小代表傳輸速度快。
自包含:使用 payload 資料塊j記錄使用者必要且不隱私的資料,可以有效的減少資料庫訪問次數,提高程式碼效能
JWT一般用於處理使用者身份校驗或資料資訊交換
JWT的資料結構
JWT的資料結構:A.B.C 以.(點)來劃分
A-header 頭資訊
B-payload (有效荷載?)
C-Signature 簽名
header:
資料結構:{"alg":"加密演算法名稱","typ":"JWT"}
alg可以有 HMAC 或 SHA256 或 RSA 等
payload:主要分為三部分:已註冊資訊、公開資料、私有資料
singature:
簽名信息,這是一個由開發者提供的資訊。是伺服器驗證的傳遞的資料是否有效安全的標準。
執行流程
簡單實現
1)造資料 JWTUsers模擬資料庫使用者名稱密碼
package cn.zytao.taosir; import java.util.HashMap; import java.util.Map; /** * 用於模擬使用者資料的,開發中應訪問資料庫驗證使用者 * @author TAOSIR * */ public class JWTUsers { private static final Map<String,String> USERS =new HashMap<>(11); static { for(int i=0;i<10;i++) { USERS.put("admin"+i, "pwd"+i); } } //驗證是否可以登入 public static boolean isLogin(String username,String pwd) { if(null == username || username.trim().length()==0) return false; String obj=USERS.get(username); if(null ==obj||!obj.equals(pwd)) return false; return true; } }
2)JWTSubject
package cn.zytao.taosir; /** * 作為Subject資料使用,也就是payload中儲存的public claims * 其中不應包含任何敏感資料 * 開發中建議使用實體型別,或BO,DTO資料物件 * @author TAOSIR * */ public class JWTSubject { private String username; public JWTSubject() { super(); } public JWTSubject(String username) { super(); this.username = username; } public String getUsername() { return username; } public void setUsername(String username) { this.username=username; } }
3)JWT結果物件
package cn.zytao.taosir; /** * 作為Subject資料使用,也就是payload中儲存的public claims * 其中不應包含任何敏感資料 * 開發中建議使用實體型別,或BO,DTO資料物件 * @author TAOSIR * */ public class JWTSubject { private String username; public JWTSubject() { super(); } public JWTSubject(String username) { super(); this.username = username; } public String getUsername() { return username; } public void setUsername(String username) { this.username=username; } }
4)響應物件
package cn.zytao.taosir; public class JWTResponseData { private Integer code;//返回碼 private Object data;//業務資料 private String msg;//返回描述 private String token;//身份標識 public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public Object getData() { return data; } public void setData(Object data) { this.data = data; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } }
5)JWT控制類
package cn.zytao.taosir; /** * JWT工具類 * @author TAOSIR * */ import java.util.Date; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import com.fasterxml.jackson.databind.ObjectMapper; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.SignatureException; public class JWTUtils { private static final String JWT_SECERT = "test_jwt_secert";//伺服器的key,金鑰 private static final ObjectMapper MAPPER = new ObjectMapper();//使用者java物件和json字串轉換 public static final int JWT_ERRCODE_EXPIRE = 1005;//Token過期 public static final int JWT_ERRCODE_FAIL = 1006;//驗證不通過 public static SecretKey generalKey() { try { byte[] encodedKey=JWT_SECERT.getBytes("UTF-8"); SecretKey key=new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES"); return key; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 簽發JWT,即建立token的方法 * @param id jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊 * @param iss jwt簽發者 * @param subject jwt所面向的使用者,payload中記錄的public,claims,當前環境中就是使用者的登入名 * @param ttlMills 有效期,單位毫秒 * @return */ public static String createJWT(String id,String iss,String subject,long ttlMillis) { //加密演算法 SignatureAlgorithm signatureAlgorithm=SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now=new Date(nowMillis); SecretKey secretKey=generalKey(); //建立JWT的構造器用於生成token JwtBuilder builder=Jwts.builder() .setId(id) .setIssuer(iss) .setSubject(subject) .setIssuedAt(now) .signWith(signatureAlgorithm, secretKey); if(ttlMillis >= 0) { long expMillis =nowMillis+ttlMillis; Date exDate = new Date(expMillis); builder.setExpiration(exDate); } return builder.compact(); } /** * 驗證JWT * @param jwtStr * @return */ public static JWTResult validateJWT(String jwtStr) { JWTResult checkResult=new JWTResult(); Claims claims=null; try { claims=parseJWT(jwtStr); checkResult.setSuccess(true); checkResult.setClaims(claims); } catch (ExpiredJwtException e) { checkResult.setSuccess(false); checkResult.setErrCode(JWT_ERRCODE_EXPIRE); } catch (SignatureException e) { checkResult.setSuccess(true); checkResult.setErrCode(JWT_ERRCODE_FAIL); } return checkResult; } /** * 解析JWT字串 * @param jwt 就是token * @return */ public static Claims parseJWT(String jwt) { SecretKey secretKey=generalKey(); //getBody獲取值就是token中記錄的payload資料,就是其中儲存的claims return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody(); } /** * 生成subject資訊 * @param subObj * @return */ public static String generalSubject(Object subObj) { try { return MAPPER.writeValueAsString(subObj); } catch (Exception e) { e.printStackTrace(); return null; } } }
6)寫個簡單的controller實踐
package cn.zytao.taosir.controller; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import cn.zytao.taosir.JWTResponseData; import cn.zytao.taosir.JWTResult; import cn.zytao.taosir.JWTSubject; import cn.zytao.taosir.JWTUsers; import cn.zytao.taosir.JWTUtils; @RestController public class JWTController { @RequestMapping("testAll") public Object testAll(HttpServletRequest request) { String token=request.getHeader("Authorization"); JWTResult result=JWTUtils.validateJWT(token); JWTResponseData responseData=new JWTResponseData(); if(result.isSuccess()) { responseData.setCode(200); responseData.setData(result.getClaims().getSubject()); String newToken=JWTUtils.createJWT(result.getClaims().getId(), result.getClaims().getIssuer(), result.getClaims().getSubject(), 1*60*1000); responseData.setToken(newToken); return responseData; }else { responseData.setCode(500); responseData.setMsg("使用者未登入"); return responseData; } } @RequestMapping("login") public Object login(String username,String password) { JWTResponseData responseData=null; //認證使用者資訊 if(JWTUsers.isLogin(username, password)) { JWTSubject subject=new JWTSubject(username); String jwtToken = JWTUtils.createJWT(UUID.randomUUID().toString(),"sxt-test-jwt", JWTUtils.generalSubject(subject), 1*60*1000); responseData=new JWTResponseData(); responseData.setCode(200); responseData.setData(null); responseData.setMsg("登入成功"); responseData.setToken(jwtToken); }else { responseData=new JWTResponseData(); responseData.setCode(500); responseData.setData(null); responseData.setMsg("登入失敗"); responseData.setToken(null); } return responseData; } }
7)Postman檢視情況