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ù)里。
用一張圖來看:
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)》