1. 背景
Spring cloud gateway 是一个api网关,可以作为 api 接口的统一入口点。实际使用过程中往往需要 对 一个 URL 进行身份认证,比如必须携带token令牌才能访问具体的URL等,这个过程可以统一在 gateway 网关实现。
JWT 是一种数字签名(令牌)的格式。借助于 java 类库的 JWT 实现我们可以很方便的实现 生成token,和验证,解析token。
gateway 集合 JWT 可以实现基础的身份认证功能。
2.知识
spring-cloud-gateway 提供了一个建立在Spring生态系统之上的API网关,旨在提供一种简单而有效的方法路由到api,并为它们提供横切关注点,如:安全性、监控/指标和弹性等。
JWT : JWT 是一种数字签名(令牌)的格式。 JSON Web Token (JWT)是一个开放标准,它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
实现思路
- 1、写一个 gateway 网关,它是对外的 访问接入点。任何URL 都要先经过这个 网关。
- 2、我们还需要一个 接口用于生成token,比如 /login ,它接收账户和秘密,如何验证通过,则返回一个有效的 token。
- 3、上面的 有效的 token 借助于 JWT 来生成。
- 4、后续 再次访问 其他资源时,都要在请求头包含 上一步生成的 token,可以理解为一个令牌,钥匙。
- 5、当一个请求进来时,检查是否有 token,这个token是否合法,借助于 JWT 来实现。
- 6、我们将 借助于JWT 生成token和校验token 的类写在一个名字叫做 auth-service 的微服务里。
用一张图来看:
3. 示例
(1) 实现需要一个 gateway 的过滤器 AuthorizationFilter,它会截获所有的 请求。
@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("## 触发在 过滤器:AuthorizationFilter2");
String targetUriPath = exchange.getRequest().getURI().getPath();
if (isSkipAuth(targetUriPath)) {
log.info("## 跳过 身份验证, 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 校验失败,参数 token = {}, targetUriPath= {}", token, targetUriPath);
return responseInvalidToken(exchange, chain);
}
log.info("## token 校验通过! 参数 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)过滤到特殊的 不需要校验的URL
@Autowired
private IgnoreAuthorizationConfig ignoreAuthorizationConfig;
/**
* 是否跳过 认证检查
*
* @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 {
/**
* 忽略 身份认证的 url列表
*/
private Set<String> urlList;
}
还要修改配置文件:
ignore:
authorization:
urlList:
- /auth/login
- /auth/logout
(3) 通过调用 auth 服务来进行 校验 token 合法性
/**
* 验证 token 的合法性
*
* @param token
* @return
*/
private boolean verifyToken(String token) {
try {
String verifyToken = authorizationClient.verifyToken(token);
log.info("## verifyToken, 参数token={}, result = {}", token, verifyToken);
return verifyToken != null && !verifyToken.isEmpty();
} catch (Exception ex) {
ex.printStackTrace();
log.info("## verifyToken,参数token={}, 发生异常 = {}", token, ex.toString());
return false;
}
}
AuthorizationClient1 类 负责发起网络请求到 auth 微服务。
/**
* @author zhangyunfei
* @date 2019/2/20
*/
@Slf4j
@Service
public class AuthorizationClient1 {
@Autowired
private RestTemplate restTemplate;
/**
* 备注:
* 1、如果使用 RestTemplate LoadBalanced, 则触发异常: blockLast() are blocking, which is not supported in thread reactor-http-nio-3
* 2、so,只能 停止 LoadBalanced,写死一个 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 准备执行: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 执行结束:verifyToken,statusCode={}, 结果={}", statusCode, res);
return res;
}
}
(4)写一个 auth 身份认证的微服务
职责:
- 1、/login 生成token
- 2、校验token是否合法
@RestController()
public class AuthController {
private Logger logger = LoggerFactory.getLogger("AuthController");
/**
* 鉴权: 通过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\" }";
}
/**
* 验证 token 的合法性
*
* @param token
* @return
*/
@RequestMapping(value = {"/verifytoken"}, method = RequestMethod.POST)
public ResponseEntity<String> verifyToken(@RequestParam String token) {
logger.info("## verifyToken 参数 token={}", token);
String userName = JwtUtils.decode(token);
if (userName == null || userName.isEmpty()) {
logger.info("## verifyToken 参数 token={}, 失败", token);
return new ResponseEntity<>("internal error", HttpStatus.UNAUTHORIZED);
}
UserInfo user = new UserInfo(userName, "", 18);
logger.info("## verifyToken 参数 token={}, 成功,用户信息={}", token, user);
return new ResponseEntity<>(JSON.toJSONString(user), HttpStatus.OK);
}
/**
* 根据token 获得我的个人信息
*
* @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\" }";
}
/**
* 身份认证:即 通过账户密码获得 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 里发起请求访问:
登录
http://localhost:9000/auth/login?name=wang5&password=1
访问业务
http://localhost:9000/hello/hi?name=zhang3
4. 扩展
我的 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; //过期时间,7天
/**
* 构建一个 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微服务实战》