JWT登陸方案

使用JWT登陸驗證方案

  • session登陸驗證:
    1> 登陸時攜帶用戶名和密碼,向服務器發送post請求。
    2> 服務器收到請求後查詢數據庫的用戶名和密碼是否匹配。
    3> 不匹配,登錄失敗,返回:用戶名或密碼錯誤
    3> 匹配:將用戶信息儲存到session中,並返回cookie。
    4> 用戶下次請求時攜帶cookie信息查詢session中是否有用戶信息。
    5> 有,通過驗證,執行下一步操作。沒有,驗證失敗,重新登錄。

一、JWT簡介

    JSON Web Token(JWT)是一個非常輕巧的規範。這個規範允許我們使用JWT在用戶和服務器之間傳遞安全可靠的信息。

  • JWT實際上就是一個字符串,它由三部分組成,頭部載荷簽名
1、頭部(Header)

    頭部用於描述關於該JWT的最基本的信息,例如其類型以及簽名所用的算法等。使用一個JSON對象來表示,並對其進行base64加密形成的字符串就是header。

{
"type":"JWT"   // 類型
,"alg":"HS256" // 簽名所用加密算法
}

base64加密後:

eyJ0eXBlIjoiSldUIiwiYWxnIjoiSFMyNTYifQ==
2、載荷(playload)

    載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分,也稱聲明。也是使用json對象表示,對其進行base64加密。

  • 1> 標準中註冊的聲明(建議但不強制使用)
    iss: jwt簽發者
    sub: jwt所面向的用戶
    aud: 接收jwt的一方
    exp: jwt的過期時間,這個過期時間必須要大於簽發時間
    nbf: 定義在什麼時間之前,該jwt都是不可用的.
    iat: jwt的簽發時間
    jti: jwt的唯一身份標識,主要用來作爲一次性token。
    
  • 2> 公共的聲明
    公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因爲該部分在客戶端可解密。
  • 3> 私有的聲明:自定義聲明
    私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因爲base64是對稱解密的,意味着該部分信息可以歸類爲明文信息。

標準聲明和自定義聲明區別:
JWT規定的claim(聲明),JWT的接收方在拿到JWT之後,都知道怎麼對這些標準的claim進行驗證(還不知道是否能夠驗證成功);而私有claims不會驗證,除非明確告訴接收方要對這些claim進行驗證以及規則才行。

  • 定義一個載荷:
{
"sub":"1234567890"  // 設置標準聲明
,"usrId":"125"   // 自定義聲明,用戶id
,"name":"John Doe"  // 自定義聲明,用戶name
,"admin":true}  // 自定義聲明,用戶權限

base64加密後:

eyJzdWIiOiIxMjM0NTY3ODkwIiwidXNySWQiOiIxMjUiLCJuYW1lIjoiSm9ob
iBEb2UiLCJhZG1pbiI6dHJ1ZX0=
3、簽證(signature)

簽證由三部分組成:

header (base64後的)
payload (base64後的)
secret // 服務器端保存的私匙

    這個部分由header(base64加密後的)和payload(base64加密後的)使用.(點),連接組成的字符串,然後再通過header中聲明的加密方式進行加鹽secret組合加密,然後就構成了jwt的第三部分。如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意: secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。

二、使用JJWT簽發與驗證token

    JJWT是一個提供端到端的JWT創建和驗證的Java庫。永遠免費和開源(Apache License,版本2.0),JJWT很容易使用和理解。它被設計成一個以建築爲中心的流暢界面,隱藏了它的大部分複雜性。

1、引入maven依賴:
  • jjwt依賴:
    <!--jwt依賴-->
    <dependency>
    	<groupId>io.jsonwebtoken</groupId>
    	<artifactId>jjwt</artifactId>
    	<version>0.9.1</version>
    </dependency>
    
  • json依賴:用來將java對象轉化成json對象
    <!--json依賴-->
    <dependency>
    	<groupId>com.alibaba</groupId>
    	<artifactId>fastjson</artifactId>
    	<version>1.2.41</version>
    </dependency>
    
2、創建JwtHelper工具類:
/**
 * @Author: smile
 * @Date: 2020/2/4
 */
public class JwtHelper {
    
    /**
     * 設置過期時間爲30天(自定義)
     */
    private static final long TOKEN_EXPIRED_TIME = 30*24*60*60*1000L;
    /**
     * 設置生成祕鑰的字符串(自定義)
     */
    private static final String JWT_SECRET = "132456";
    /**
     * 設置jwt的id(自定義)
     */
    private static final String jwtId = "tokenId";
    
    /**
     * 創建JWT的工具
     * @param claim //私有聲明(自定義聲明)
     * @param time // 過期時間
     * @return // jwt token
     */
    public static String createJWT(Map<String,Object> claim, long time) {
        //指定簽名的時候使用的簽名算法,也就是header那部分,jjwt已經將這部分內容封裝好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        //生成JWT的時間
        long nowMillis = System.currentTimeMillis();
        // 指定簽發時間
        Date iat = new Date(nowMillis);
        // 生成密匙,方法是下面自定義的
        SecretKey secretKey = generalKey();
        //爲payload添加各種標準聲明和私有聲明瞭
        JwtBuilder jwtBuilder = Jwts.builder()
                // 如果有私有聲明,一定要先設置這個自己創建的私有的聲明,這個是給builder的claim賦值,一旦寫在標準的聲明賦值之後,就是覆蓋了那些標準的聲明的
                .setClaims(claim)
                //設置jti(JWT ID):是JWT的唯一標識,根據業務需要,這個可以設置爲一個不重複的值,主要用來作爲一次性token,從而回避重放攻擊。
                .setId(jwtId)
                //iat: jwt的簽發時間
                .setIssuedAt(iat)
                //設置簽名使用的簽名算法和簽名使用的祕鑰
                .signWith(signatureAlgorithm, secretKey);
        //設置過期時間        
        if (time >= 0) {
            long expMillis = nowMillis + time;
            Date exp = new Date(expMillis);
            
            jwtBuilder.setExpiration(exp);
        }
       
        return jwtBuilder.compact();
    }
    
    /**
     * 驗證jwt
     */
    public static Claims verifyJwt(String token) {
        //簽名祕鑰,和生成的簽名的祕鑰一模一樣
        SecretKey key = generalKey();
        Claims claims;
        try {
            //得到DefaultJwtParser
            claims = Jwts.parser()
                    //設置簽名的祕鑰
                    .setSigningKey(key)
                    //設置需要解析的jwt
                    .parseClaimsJws(token).getBody();
        } catch (Exception e) {
            claims = null;
        }
        return claims;
    }
    
    /**
     * 從token中單獨解析出簽名
     * @param token
     * @return
     */
    public static String resolveSignture(String token) {
        //簽名祕鑰,和生成的簽名的祕鑰一模一樣
        SecretKey key = generalKey();
        String signature = Jwts.parser()
                //設置簽名的祕鑰
                .setSigningKey(key)
                //設置需要解析的jwt
                .parsePlaintextJws(token).getSignature();
        return signature;
    }
    
   
    /**
     * 由字符串JWT_SECRET生成加密key
     * @return
     */
    private static SecretKey generalKey() {
        String stringKey = JWT_SECRET;
        byte[] encodedKey = Base64.encodeBase64(stringKey.getBytes());
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
    
    /**
     * 根據對象生成token的方法
     */
    public static<T> String generateToken(String key, T object) {
        Map<String, Object> map = new HashMap<>();
        map.put(key, object);
        return createJWT(map, TOKEN_EXPIRED_TIME);
    }
}
3、創建認證中心(controller層):
/**
 * 認證中心
 * @Author: smile
 * @Date: 2020/2/3
 */
@RestController
public class AuthenticationCenterController {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 登錄接口,生成token
     * @param user 接收用戶信息
     * @param request
     * @return
     */
    @PostMapping("login")
    public ResponseEntity loginToken(User user, HttpServletRequest request) {
        // 校驗用戶名或密碼
        if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
            return new ResponseEntity(new RestfulBody<>(401,"請輸入正確的用戶名或密碼"), HttpStatus.BAD_REQUEST);
        }
        // 查詢數據庫驗證用戶名或密碼
        if (false) {
            return new ResponseEntity(new RestfulBody<>(401,"用戶名或密碼錯誤"), HttpStatus.BAD_REQUEST);
        }
        // 獲取瀏覽器UA,用於下次判斷是否同一用戶
        String userAgent = request.getHeader("user-agent");
        user.setId(120L);
        user.setUserAgent(userAgent);
        // 使用user信息生成token
        String token = JwtHelper.generateToken("user", user);
        // 將簽名保存到redis中,並設置過期時間
        redisTemplate.opsForValue().set("token:"+user.getId(), token, 60*60, TimeUnit.SECONDS);
        return new ResponseEntity(new RestfulBody<User>(200,token,"成功"), HttpStatus.CREATED);
    }
    
    /**
     * 登出接口
     * @param request
     * @return
     */
    @GetMapping("logout")
    public ResponseEntity logout(HttpServletRequest request) {
        // 接收客戶端的token
        String token = request.getHeader("token");
        // 解析客戶端token
        Claims claims = JwtHelper.verifyJwt(token);
        // 獲取token中的用戶信息
        if (claims != null) {
            // 從解析出的客戶端token獲取用戶
            Object object = claims.get("user");
            User user = JSON.parseObject(JSON.toJSONString(object), User.class);
            redisTemplate.delete("token:" + user.getId());
        }
        return new ResponseEntity(new RestfulBody<User>(200,"退出成功"), HttpStatus.valueOf(200));
    }
4、創建攔截器:

    創建攔截器,攔截所有需要驗證的接口請求。

  • 認證攔截器:設置攔截規則

    /**
     * 認證攔截
     * @Author: smile
     * @Date: 2020/2/4
     */
    @Component
    public class AuthenticationInteceptor implements HandlerInterceptor{
        
        @Autowired
        private StringRedisTemplate redisTemplate;
        
        /**
         * 攔截所有需要認證的請求,驗證token
         * @param request
         * @param response
         * @param handler
         * @return
         * @throws Exception
         */
        @Override
        public boolean preHandle(HttpServletRequest request,
                                 HttpServletResponse response,
                                 Object handler) throws Exception {
            // 允許跨域
            response.setHeader("Access-Control-Allow-Origin", "*");
            // 接收客戶端的token
            String token = request.getHeader("token");
            // 解析客戶端token
            Claims claims = JwtHelper.verifyJwt(token);
            
            // token驗證1-判斷token是否能被解析,解析失敗,返回失敗信息
            if (claims == null) {
                Object json =  JSON.toJSON(new RestfulBody<>(404, "token校驗失敗,請重新登錄"));
                this.responseWriter(json, response);
                return false;
            }
            // 從解析出的客戶端token獲取用戶
            Object object = claims.get("user");
            User user = JSON.parseObject(JSON.toJSONString(object), User.class);
            System.out.println(user.getUserAgent());
            // 根據客戶端token中的信息從redis中查詢token
            String redistoken = redisTemplate.opsForValue().get("token:" + user.getId());
            
            // token驗證2-判斷token是否存在,不存在返回失敗信息
            if (StringUtils.isBlank(redistoken)) {
                Object json =  JSON.toJSON(new RestfulBody<>(404, "token超時,請重新登錄"));
                this.responseWriter(json, response);
                return false;
            }
            // 解析redis中的token,獲取user信息
            Claims redisClaims = JwtHelper.verifyJwt(redistoken);
            Object redisObject = redisClaims.get("user");
            User redisUser = JSON.parseObject(JSON.toJSONString(redisObject), User.class);
            
            // token驗證3-判斷客戶端token中的UA和redis中的token的UA是否一致,不一致則返回錯誤信息
            if (!redisUser.getUserAgent().equals(user.getUserAgent())) {
                Object json =  JSON.toJSON(new RestfulBody<>(404, "token無效,請重新登錄"));
                this.responseWriter(json, response);
                return false;
            }
            return true;
        }
        
        /**
         * 給客戶端響應json對象封裝的方法
         */
        public void responseWriter(Object json, HttpServletResponse response) {
            response.setCharacterEncoding("utf-8");
            PrintWriter writer = null;
            try {
                writer =  response.getWriter();
                writer.print(json);
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                writer.close();
            }
        }
    }
    
  • 攔截器配置類:設置攔截路徑和排除路徑

    /**
     *  添加攔攔截器的截路徑和排除攔截路徑
     * @Author: smile
     * @Date: 2020/2/4
     */
    @Configuration
    public class AuthenticationWebConfig implements WebMvcConfigurer {
        
        /**
         * 注入攔截器
         */
        @Autowired
        private AuthenticationInteceptor authenticationInteceptor;
        
        /**
         * 添加攔截規則
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(authenticationInteceptor)
                    .addPathPatterns("/**")
                    .excludePathPatterns("/login")
                    .excludePathPatterns("/logout");
        }
    }
    

三、JWT適用場景

  • 授權:這是最常見的使用場景,解決單點登錄問題。因爲JWT使用起來輕便,開銷小,服務端不用記錄用戶狀態信息(無狀態),所以使用比較廣泛。
  • 信息交換:JWT是在各個服務之間安全傳輸信息的好方法。因爲JWT可以簽名,例如,使用公鑰/私鑰對兒 - 可以確定請求方是合法的。此外,由於使用標頭和有效負載計算簽名,還可以驗證內容是否未被篡改。
發佈了44 篇原創文章 · 獲贊 11 · 訪問量 8666
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章