1. 程式人生 > >初識單點登入及JWT實現

初識單點登入及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檢視情況