1. 程式人生 > >spring cloud進階七 [jwt服務間的鑑權]

spring cloud進階七 [jwt服務間的鑑權]

一、為什麼要使用jwt?


在微服務架構下的服務基本都是無狀態的,傳統的使用session的方式不再適用,如果使用的話需要做同步session機制,所以產生了了一些技術來對微服務架構進行保護,例如常用的鑑權框架Spring Security OAuth2和用Jwt來進行保護,相對於框架而言,jwt較輕,且可以自包含一些使用者資訊和設定過期時間,省去了Spring Security OAuth2繁瑣的步驟。

二、什麼是JWT?


jwt(JSON WEB TOKEN)是一種用來在網路上宣告某種身份的令牌(TOKEN),它的特點是緊湊且自包含並且基於JSON,通過一些常用的演算法對包含的主體資訊進行加密,安全性很高。它通常有三個部分組成:頭資訊(Header),訊息體(Payload),簽名(Signature)。
Header通常用來宣告令牌的型別和使用的演算法,Payload主要用來包含使用者的一些資訊,Signature部分則是將Base64編碼後的Header和Payload進行簽名。

三、在Spring Cloud 下如何使用 jwt?


在SC(Spring Cloud簡稱,以下將都採用這種方式)下通常使用需要安全保護的有兩處,分別為系統認證和服務內部鑑權。
1系統認證
(1)基本流程
jwt基本使用方式如下圖

使用者在提交登入資訊後,伺服器校驗資料後將通過金鑰的方式來生成一個字串token返回給客戶端,客戶端在之後的請求會把token放在header裡,在請求到達伺服器後,伺服器會檢驗和解密token,如果token被篡改或者失效將會拒絕請求,如果有效則伺服器可以獲得使用者的相關資訊並執行請求內容,最後將結果返回。
在微服務架構下,通常有單獨一個服務Auth去管理相關認證,為了安全不會直接讓使用者訪問某個服務,會開放一個入口服務作為閘道器gateway,只允許外網閘道器,所有請求首先訪問gateway,有gateway將請求路由到各個服務,spring cloud下通常使用zuul來實現閘道器,整個基本過程如下圖所示

客戶端請求閘道器後,閘道器會根據路徑過濾請求,是登入獲取token操作的路徑則直接放行,請求直接到達auth服務進行登入操作,之後進行JWT私鑰加密生成token返回給客戶端;是其他請求將會進行token私鑰解密校驗,如果token被篡改或者失效則直接拒絕訪問並返回錯誤資訊,如果驗證成功經過路由到達請求服務,請求服務響應並返回資料。
(2)如何實現登入、重新整理、登出等?
登入比較簡單,在驗證身份資訊後可以使用工具包例如jjwt根據使用者資訊生成token並設定有效時長,最後將token返回給客戶端儲存即可,客戶端只需要每次訪問時將token加在請求頭裡即可,然後在zuul增加一個filter,此filter來過濾請求,如果是登入獲取token則放行,其他的話用公鑰解密驗證token是否有效。
如果要實現重新整理,則需要在生成token時生成一個refreshKey,在登入時和token一併返回給客戶端,然後由客戶端儲存定時使用refreshKey和token來重新整理獲取新的有效時長的token,這個refreshKey可自定義生成,為了安全起見,伺服器可能需要快取refreshKey,可使用redis來進行儲存,每次重新整理token都將生成新的refreshKey和token,伺服器需要將老refreshKey替換,客戶端儲存新的token和refreshKey來進行之後的訪問和重新整理。
如果要實現登出,並使得舊的token即便在有效期內也不能通過驗證,則需要修改登入、重新整理、和優化zuul的filter。首先在登入時生成token和refreshKey後,需要將token也進行快取,如果通過redis進行快取可以直接放一個Set下,此Set儲存所有未過期的token。其次,在重新整理時在這個Set中刪除舊的token並放入新的。最後對zuulFilter進行優化,在解密時先從redis裡存放token的Set查詢此token是否存在(redis的Set有提供方法),如果沒有則直接拒絕,如果有再進行下一步解密驗證有效時長,驗證有效時長是為了防止重新整理機制失效、沒有重新整理機制、網路異常強行退出等事件出現,在這種情況下舊的token沒有被刪除,導致了舊的token一直可以訪問(如果只驗證是否token是否在快取中)。在登出時只需要刪除redis中Set的token記錄就好,最後寫個定時器去定時刪除redis中Set裡面過時的token,原因也是重新整理機制失效、沒有重新整理機制、網路異常強行退出等事件出現導致舊的token沒有被刪除。

四、JWT存在的問題


jwt第一次生成token 的時候會比較慢,而且因為採用了加密演算法保證安全,所以比較耗CPU,在高併發的情況下需要考慮CPU佔用問題。還有一個問題,jwt生成的token比較長,可能需要考慮流量問題。
 

五、程式碼示例

JwtUtil.java

@Component
public class JwtUtil {

    private static UserRepository userRepository;

    @Autowired
    public JwtUtil(UserRepository userRepository) {
        JwtUtil.userRepository = userRepository;
    }

    public static final long EXPIRATION_TIME = 3600_000_000L; // 1000 hour
    static final String SECRET = "ThisIsASecret";
    static final String TOKEN_PREFIX = "Bearer";
    static final String HEADER_STRING = "Authorization";

    public static String generateToken(String username,Date generateTime) {
        HashMap<String, Object> map = new HashMap<>();
        //可以把任何安全的資料放到map裡面
        map.put("username", username);
        map.put("generateTime",generateTime);
        String jwt = Jwts.builder()
                .setClaims(map)
                .setExpiration(new Date(generateTime.getTime() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
        return jwt;
    }

    /**
     * @param token
     * @return
     */
    public static Map<String,Object> validateToken(String token) {
        Map<String,Object> resp = new HashMap<String,Object>();
        if (token != null) {
            // 解析token
            try {
                Map<String, Object> body = Jwts.parser()
                        .setSigningKey(SECRET)
                        .parseClaimsJws(token.replace(TOKEN_PREFIX, ""))
                        .getBody();
                String username = (String) (body.get("username"));
                Date generateTime = new Date((Long)body.get("generateTime"));

                if(username == null || username.isEmpty()){
                    resp.put("ERR_MSG",Constants.ERR_MSG_USERNAME_EMPTY);
                    return resp;
                }
                //賬號在別處登入
                if(userRepository.findByUsername(username).getLastLoginTime().after(generateTime)){
                    resp.put("ERR_MSG",Constants.ERR_MSG_LOGIN_DOU);
                    return resp;
                }
                resp.put("username",username);
                resp.put("generateTime",generateTime);
                return resp;
            }catch (SignatureException | MalformedJwtException e) {
                // TODO: handle exception
                // don't trust the JWT!
                // jwt 解析錯誤
                resp.put("ERR_MSG",Constants.ERR_MSG_TOKEN_ERR);
                return resp;
            } catch (ExpiredJwtException e) {
                // TODO: handle exception
                // jwt 已經過期,在設定jwt的時候如果設定了過期時間,這裡會自動判斷jwt是否已經過期,如果過期則會丟擲這個異常,我們可以抓住這個異常並作相關處理。
                resp.put("ERR_MSG",Constants.ERR_MSG_TOKEN_EXP);
                return resp;
            }
        }else {
            resp.put("ERR_MSG",Constants.ERR_MSG_TOKEN_EMPTY);
            return resp;
        }
    }
}

UserController.java

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserRepository userRepository;


    //註冊或登入
    @RequestMapping("/login")
    @Transactional
    public UserResponse login(User user){

        String username = user.getUsername();
        String password = user.getPassword();
        //TODO  檢驗引數的完整性

        UserResponse userResponse = new UserResponse();
        User tUser = userRepository.findByUsername(username);
        //檢驗username是否存在
        user.setLastLoginTime(new Date());
        if(tUser!=null){
            //檢驗密碼是否正確
            if(!tUser.getPassword().equals(password)) {
                userResponse.setErrorNum(Constants.ERR_NUM_PWD_ERR);
                userResponse.setErrorMsg(Constants.ERR_MSG_PWD_ERR);
                return userResponse;
            }
            userRepository.updateLastLoginTimeByUserName(user.getLastLoginTime(),username);

        }else {
            try {
                tUser = userRepository.save(user);
            } catch (Exception e) {
                userResponse.setErrorNum(Constants.ERR_NUM_SERVER_ERR);
                userResponse.setErrorMsg(Constants.ERR_MSG_SERVER_ERR);
                return userResponse;
            }
        }
        userResponse.setErrorNum(Constants.ERR_NUM_OK);
        userResponse.setErrorMsg(Constants.ERR_MSG_OK);
        userResponse.setUserName(username);
        userResponse.setUserId(tUser.getId());
        userResponse.setToken(JwtUtil.generateToken(username,user.getLastLoginTime()));

        return userResponse;
    }
}

 

HelloController.java

@RestController
public class HelloController {
    @RequestMapping("/hello")
    public Map login(HttpServletRequest request){
        String token = request.getParameter("token");
        return  JwtUtil.validateToken(token);
    }
}