Spring Cloud 學(xué)習(xí)筆記(6) gateway 結(jié)合 JWT 實(shí)現(xiàn)身份認(rèn)證

1. 背景

Spring cloud gateway 是一個(gè)api網(wǎng)關(guān)倦始,可以作為 api 接口的統(tǒng)一入口點(diǎn)敬特。實(shí)際使用過程中往往需要 對 一個(gè) URL 進(jìn)行身份認(rèn)證厘肮,比如必須攜帶token令牌才能訪問具體的URL等转砖,這個(gè)過程可以統(tǒng)一在 gateway 網(wǎng)關(guān)實(shí)現(xiàn)恢筝。

JWT 是一種數(shù)字簽名(令牌)的格式蔫仙。借助于 java 類庫的 JWT 實(shí)現(xiàn)我們可以很方便的實(shí)現(xiàn) 生成token料睛,和驗(yàn)證,解析token摇邦。

gateway 集合 JWT 可以實(shí)現(xiàn)基礎(chǔ)的身份認(rèn)證功能恤煞。

2.知識

spring-cloud-gateway 提供了一個(gè)建立在Spring生態(tài)系統(tǒng)之上的API網(wǎng)關(guān),旨在提供一種簡單而有效的方法路由到api施籍,并為它們提供橫切關(guān)注點(diǎn)居扒,如:安全性、監(jiān)控/指標(biāo)和彈性等丑慎。

JWT : JWT 是一種數(shù)字簽名(令牌)的格式喜喂。 JSON Web Token (JWT)是一個(gè)開放標(biāo)準(zhǔn),它定義了一種緊湊的竿裂、自包含的方式玉吁,用于作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗(yàn)證和信任腻异,因?yàn)樗菙?shù)字簽名的进副。

實(shí)現(xiàn)思路

  • 1、寫一個(gè) gateway 網(wǎng)關(guān)悔常,它是對外的 訪問接入點(diǎn)影斑。任何URL 都要先經(jīng)過這個(gè) 網(wǎng)關(guān)。
  • 2机打、我們還需要一個(gè) 接口用于生成token矫户,比如 /login ,它接收賬戶和秘密,如何驗(yàn)證通過姐帚,則返回一個(gè)有效的 token吏垮。
  • 3、上面的 有效的 token 借助于 JWT 來生成罐旗。
  • 4膳汪、后續(xù) 再次訪問 其他資源時(shí),都要在請求頭包含 上一步生成的 token九秀,可以理解為一個(gè)令牌遗嗽,鑰匙。
  • 5鼓蜒、當(dāng)一個(gè)請求進(jìn)來時(shí)痹换,檢查是否有 token征字,這個(gè)token是否合法,借助于 JWT 來實(shí)現(xiàn)娇豫。
  • 6匙姜、我們將 借助于JWT 生成token和校驗(yàn)token 的類寫在一個(gè)名字叫做 auth-service 的微服務(wù)里。

用一張圖來看:


image.png

3. 示例

(1) 實(shí)現(xiàn)需要一個(gè) gateway 的過濾器 AuthorizationFilter冯痢,它會(huì)截獲所有的 請求氮昧。

@Slf4j
@Component
public class AuthorizationFilter extends AbstractGatewayFilterFactory<AuthorizationFilter.Config> {
    @Autowired
    private AuthorizationClient1 authorizationClient;

    @Autowired
    private IgnoreAuthorizationConfig ignoreAuthorizationConfig;

    public AuthorizationFilter() {
        super(AuthorizationFilter.Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            log.info("## 觸發(fā)在 過濾器:AuthorizationFilter2");
            String targetUriPath = exchange.getRequest().getURI().getPath();
            if (isSkipAuth(targetUriPath)) {
                log.info("## 跳過 身份驗(yàn)證, targetUriPath={}", targetUriPath);
                return goNext(exchange, chain);
            }
            String token = exchange.getRequest().getHeaders().getFirst("token");
            if (token == null || token.isEmpty()) {
                log.info("## 無效的token = {}, targetUriPath= {}", token, targetUriPath);
                return responseInvalidToken(exchange, chain);
            }
            if (!verifyToken(token)) {
                log.info("## token 校驗(yàn)失敗,參數(shù) token = {}, targetUriPath= {}", token, targetUriPath);
                return responseInvalidToken(exchange, chain);
            }
            log.info("## token 校驗(yàn)通過! 參數(shù) token = {}, targetUriPath= {}", token, targetUriPath);
            return chain.filter(exchange);
        };
    }

修改配置文件:

spring:
  application:
    name: api-gateway

  cloud:
    gateway:
      default-filters:
        - AuthorizationFilter
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
      globalcors:
        corsConfigurations:
          '[/auth/**]':
            allowedOrigins: '*'
            allowedHeaders:
              - x-auth-token
              - x-request-id
              - Content-Type
              - x-requested-with
              - x-request-id
            allowedMethods:
              - GET
              - POST
              - OPTIONS
      routes:
        - id: auth-service
          uri: lb://auth-service
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
        - id: hello-service-1
          uri: lb://hello-service
          predicates:
            - Path=/hello/**
          filters:
            - StripPrefix=1

(2)過濾到特殊的 不需要校驗(yàn)的URL

    @Autowired
    private IgnoreAuthorizationConfig ignoreAuthorizationConfig;


    /**
     * 是否跳過 認(rèn)證檢查
     *
     * @param targetUriPath 請求的資源 URI
     * @return
     */
    private boolean isSkipAuth(String targetUriPath) {
        boolean isSkip = ignoreAuthorizationConfig.getUrlList().contains(targetUriPath);
        log.info("## isSkip={}, ignoreAuthorizationConfig={}, targetUriPath={}", isSkip, ignoreAuthorizationConfig, targetUriPath);
        return isSkip;
    }

@Data
@Component
@ConfigurationProperties(prefix = "ignore.authorization")
public class IgnoreAuthorizationConfig {

    /**
     * 忽略 身份認(rèn)證的 url列表
     */
    private Set<String> urlList;
}

還要修改配置文件:

ignore:
  authorization:
    urlList:
      - /auth/login
      - /auth/logout

(3) 通過調(diào)用 auth 服務(wù)來進(jìn)行 校驗(yàn) token 合法性

    /**
     * 驗(yàn)證 token 的合法性
     *
     * @param token
     * @return
     */
    private boolean verifyToken(String token) {
        try {
            String verifyToken = authorizationClient.verifyToken(token);
            log.info("## verifyToken, 參數(shù)token={}, result = {}", token, verifyToken);
            return verifyToken != null && !verifyToken.isEmpty();
        } catch (Exception ex) {
            ex.printStackTrace();
            log.info("## verifyToken,參數(shù)token={}, 發(fā)生異常 = {}", token, ex.toString());
            return false;
        }
    }

AuthorizationClient1 類 負(fù)責(zé)發(fā)起網(wǎng)絡(luò)請求到 auth 微服務(wù)浦楣。

/**
 * @author zhangyunfei
 * @date 2019/2/20
 */
@Slf4j
@Service
public class AuthorizationClient1 {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 備注:
     * 1袖肥、如果使用 RestTemplate LoadBalanced, 則觸發(fā)異常: blockLast() are blocking, which is not supported in thread reactor-http-nio-3
     * 2、so振劳,只能 停止 LoadBalanced椎组,寫死一個(gè) ip
     */

//        private static final String URL_VERIFY_TOKEN = "http://auth-service/verifytoken";
    private static final String URL_VERIFY_TOKEN = "http://127.0.0.1:8082/verifytoken";

    public String verifyToken(String token) {
        log.info("## verifyToken 準(zhǔn)備執(zhí)行:verifyToken");

        HttpHeaders headers = new HttpHeaders();
        LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        HttpEntity entity = new HttpEntity<>(paramMap, headers);
        paramMap.add("token", token);
        String url = URL_VERIFY_TOKEN;
        ResponseEntity<String> forEntity = restTemplate
                .exchange(url, HttpMethod.POST, entity, new ParameterizedTypeReference<String>() {
                });
        HttpStatus statusCode = forEntity.getStatusCode();
        String res = forEntity.getBody();
        log.info("## verifyToken 執(zhí)行結(jié)束:verifyToken,statusCode={}, 結(jié)果={}", statusCode, res);
        return res;
    }

}

(4)寫一個(gè) auth 身份認(rèn)證的微服務(wù)

職責(zé):

  • 1历恐、/login 生成token
  • 2寸癌、校驗(yàn)token是否合法
@RestController()
public class AuthController {
    private Logger logger = LoggerFactory.getLogger("AuthController");

    /**
     * 鑒權(quán): 通過token 獲得用戶的信息。
     * - 成功:返回用戶信息
     * - 失敿泄:返回 401
     * - 失敗的情形: 1灵份、token 過期仁堪。2哮洽、token 為空或無效。
     *
     * @param token
     * @return
     */
    @RequestMapping(value = {"/authority"}, method = RequestMethod.POST)
    public String authority(@RequestParam String token, @RequestParam String resource) {
        logger.info("## auth" + token);
        return "{ userId:123, userName:\"zhang3\" }";
    }

    /**
     * 驗(yàn)證 token 的合法性
     *
     * @param token
     * @return
     */
    @RequestMapping(value = {"/verifytoken"}, method = RequestMethod.POST)
    public ResponseEntity<String> verifyToken(@RequestParam String token) {
        logger.info("## verifyToken 參數(shù) token={}", token);
        String userName = JwtUtils.decode(token);
        if (userName == null || userName.isEmpty()) {
            logger.info("## verifyToken 參數(shù) token={}弦聂, 失敗", token);
            return new ResponseEntity<>("internal error", HttpStatus.UNAUTHORIZED);
        }
        UserInfo user = new UserInfo(userName, "", 18);
        logger.info("## verifyToken 參數(shù) token={}鸟辅, 成功,用戶信息={}", token, user);
        return new ResponseEntity<>(JSON.toJSONString(user), HttpStatus.OK);
    }


    /**
     * 根據(jù)token 獲得我的個(gè)人信息
     *
     * @param token
     * @param resource
     * @return
     */
    @RequestMapping(value = "/mine", method = RequestMethod.POST)
    public String mine(@RequestParam String token, @RequestParam String resource) {
        logger.info("## auth" + token);
        return "{ userId:123, userName:\"zhang3\", group:\"zh\", country:\"china\" }";
    }

    /**
     * 身份認(rèn)證:即 通過賬戶密碼獲得 token
     *
     * @param name
     * @param password
     * @return
     */
    @RequestMapping(value = {"/authorization", "/login"})
    public String authorization(@RequestParam String name, @RequestParam String password) {
        String token = JwtUtils.sign(name);
        logger.info("## authorization name={}, token={}", name, token);
        return token;

    }
}

(5) 訪問

可以在 postman 里發(fā)起請求訪問:
登錄
http://localhost:9000/auth/login?name=wang5&password=1
訪問業(yè)務(wù)
http://localhost:9000/hello/hi?name=zhang3

4. 擴(kuò)展

我的 demo : https://github.com/vir56k/demo/tree/master/springboot/auth_jwt_demo

JWT輔助類

package eureka_client.demo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;

public class JwtUtils {
    private static final String SECRET = "zhangyunfei789!@";
    private static final long EXPIRE = 1000 * 60 * 60 * 24 * 7;  //過期時(shí)間莺葫,7天

    /**
     * 構(gòu)建一個(gè) token
     * 傳入 userID
     *
     * @param userID
     * @return
     */
    public static String sign(String userID) {
        try {
            Date now = new Date();
            long expMillis = now.getTime() + EXPIRE;
            Date expDate = new Date(expMillis);

            Algorithm algorithmHS = Algorithm.HMAC256(SECRET);
            String token = JWT.create()
                    .withIssuer("auth0")
                    .withJWTId(userID)
                    .withIssuedAt(now)
                    .withExpiresAt(expDate)
                    .sign(algorithmHS);
            return token;
        } catch (JWTCreationException exception) {
            //Invalid Signing configuration / Couldn't convert Claims.
            return null;
        }
    }

    /**
     * 解析 token
     * 返回  是否有效
     * @param token
     * @return
     */
    public static boolean verify(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("auth0")
                    .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);
            String userID = jwt.getId();
            return userID != null && !"".equals(userID);
        } catch (JWTVerificationException exception) {
            //Invalid signature/claims
            return false;
        }
    }

    /**
     * 解析 token
     * 返回  userid
     * @param token
     * @return
     */
    public static String decode(String token) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withIssuer("auth0")
                    .build(); //Reusable verifier instance
            DecodedJWT jwt = verifier.verify(token);
            return jwt.getId();
        } catch (JWTVerificationException exception) {
            //Invalid signature/claims
            return null;
        }
    }
}

5.參考:

《Spring Cloud微服務(wù)實(shí)戰(zhàn)》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末匪凉,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捺檬,更是在濱河造成了極大的恐慌再层,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,525評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堡纬,死亡現(xiàn)場離奇詭異聂受,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)烤镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評論 3 395
  • 文/潘曉璐 我一進(jìn)店門蛋济,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人炮叶,你說我怎么就攤上這事碗旅《纱Γ” “怎么了?”我有些...
    開封第一講書人閱讀 164,862評論 0 354
  • 文/不壞的土叔 我叫張陵祟辟,是天一觀的道長医瘫。 經(jīng)常有香客問我,道長旧困,這世上最難降的妖魔是什么登下? 我笑而不...
    開封第一講書人閱讀 58,728評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮叮喳,結(jié)果婚禮上被芳,老公的妹妹穿的比我還像新娘。我一直安慰自己馍悟,他們只是感情好畔濒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锣咒,像睡著了一般侵状。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上毅整,一...
    開封第一講書人閱讀 51,590評論 1 305
  • 那天趣兄,我揣著相機(jī)與錄音,去河邊找鬼悼嫉。 笑死艇潭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的戏蔑。 我是一名探鬼主播蹋凝,決...
    沈念sama閱讀 40,330評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼总棵!你這毒婦竟也來了鳍寂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,244評論 0 276
  • 序言:老撾萬榮一對情侶失蹤情龄,失蹤者是張志新(化名)和其女友劉穎迄汛,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體骤视,經(jīng)...
    沈念sama閱讀 45,693評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鞍爱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了尚胞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片硬霍。...
    茶點(diǎn)故事閱讀 40,001評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖笼裳,靈堂內(nèi)的尸體忽然破棺而出唯卖,到底是詐尸還是另有隱情粱玲,我是刑警寧澤,帶...
    沈念sama閱讀 35,723評論 5 346
  • 正文 年R本政府宣布拜轨,位于F島的核電站抽减,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏橄碾。R本人自食惡果不足惜卵沉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望法牲。 院中可真熱鬧史汗,春花似錦、人聲如沸拒垃。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,919評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽悼瓮。三九已至戈毒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間横堡,已是汗流浹背埋市。 一陣腳步聲響...
    開封第一講書人閱讀 33,042評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留命贴,地道東北人道宅。 一個(gè)月前我還...
    沈念sama閱讀 48,191評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像套么,于是被迫代替她去往敵國和親培己。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評論 2 355

推薦閱讀更多精彩內(nèi)容