Dotnet微服务:spring cloud gateway实现Api网关及jwt统一认证
当你的项目中服务越来越多,每个服务都有自己的监听地址而又需要把这些服务提供给各式的客户端或第三方使用,那么需要把每个服务地址都暴露出来吗?如果某个服务有多个运行实例,如果进行负载均衡?用户认证和授权需要在每个服务上都做吗,能否统一做?要解决这些问题,就需要用到Api网关,Api网关提供Api请求转发服务并可与Eureka结合实现路由转发和负载均衡,同时利用AOP特性可以实现微服务用户统一认证和授权。目前比较流行的Api网关有Zuul、springcloud gateway以及支持dotnetcore的ocelot。本文使用的是springcloud。
一,搭建springcloud gateway
1,新建一个springboot项目,引入eureka-client和stater-gateway
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
2,项目配置文件:Application.yml
server: port: 8020 spring: application: name: gateway-server cloud: gateway: discovery: locator: enabled: true //启用网关服务发关 lower-case-service-id: true //支持小写字母的服务名称(注册到eureka的服务) ek: username: eureka password: 123456 eureka-url: 127.0.0.1 eureka-port: 8765 eureka: client: service-url: defaultZone: http://${ek.username}:${ek.password}@${ek.eureka-url}:${ek.eureka-port}/eureka //Eureka注册地址 instance: prefer-ip-address: true //启用优先IP地址注册 instance-id: ${spring.application.name}@${spring.cloud.client.ip-address}@${server.port} //本网关注册到Eureka的实例id
3,启用项目注册到Eureka,并通过网关访问Api
验证Api
二,DotnetCore JWT证书颁布服务
1,新建一个web api项目,Nuget添加:System.IdentityModel.Tokens.Jwt、Microsoft.IdentityModel.Tokens两个包。配置文件添加jwt配置信息。
appsetting.json 注意:Key用于jwt的签名,长度不能少于16位。可用openssl生成一个32位的密钥。
"Identity": { "Jwt": { "Key": "1234567890123456", "Domain": "kingsun.mico" } }
使用IOptions读取配置信息并写入依赖
Startup.cs
public void ConfigureServices(IServiceCollection services) { services.Configure<JwtConfig>(Configuration.GetSection("Identity")); services.AddControllers(); }
2,新建一个名为Token的Api控制器并创建接口GetToken
[Route("api/[controller]")] [ApiController] public class TokenController : ControllerBase { JwtConfig config; public TokenController(IOptions< JwtConfig > config) { this.config = config.Value; } public class GetTokenRequest { [Required] public string Name { get; set; } [Required] public string Password { get; set; } } public class GetTokenRespone { public int Code { get; set; } public string Msg { get; set; } public string Data { get; set; } } [HttpPost("GetToken")] public object GetToken([FromBody] GetTokenRequest data) { GetTokenRespone respone = new GetTokenRespone() { Code = 0 }; if (!ModelState.IsValid) { respone.Msg = "用户名或密码不能为空"; return respone; } //验证密码逻辑 //.... //jwt中payload键值对内容 List<Claim> claims = new List<Claim>() { //用户名 new Claim("name",data.Name) }; var key = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(config.Jwt.Key)); var creds = new SigningCredentials(key,SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: "liujb", audience: config.Jwt.Domain, claims: claims, signingCredentials: creds, expires:DateTime.Now.AddDays(1),notBefore:DateTime.Now ); respone.Code = 1; respone.Msg = "请求成功"; respone.Data = new JwtSecurityTokenHandler().WriteToken(token); return respone; } }
3,获取jwt令牌
可拿此令牌验证后内容如下:
4,将此服务注册到Eureka后用api网关转发服务获取token:http://localhost:8020/eureka-identity/api/token/gettoken
{ "code": 1, "msg": "请求成功", "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibGl1amIiLCJuYmYiOiIxNjAyODE3NjY1MjMwIiwiZXhwIjoiMTYwMjkwNDA2NTIzMCIsImlzcyI6ImxpdWpiIiwiYXVkIjoia2luZ3N1bi5taWNvIn0.5pIrNumEEy280-sCWp4d0K05Skc2Ptn1sK732Rw-L-A" }
三,在api网关统一做接口认证
1,在第一步建立的网关项目中maven加入java-jwt依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency>
配置文件加入jwt密钥:jwt.key=1234567890123456。值与第二步的中密钥一致。
2,在该项目新建一个全局过滤器。
@Component public class JwtFilter implements GlobalFilter, Ordered { @Value("${jwt.key}") public String jwtKey; Logger logger=null; public JwtFilter(){ logger= LoggerFactory.getLogger("JwtFilter"); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String requestUrl=exchange.getRequest().getPath().toString(); ServerHttpResponse response = exchange.getResponse(); /*过滤掉获取token的接口*/ if(requestUrl.toLowerCase().equals("/eureka-identity/api/token/gettoken")){ return chain.filter(exchange); } //获取token String token=exchange.getRequest().getHeaders().getFirst("Authorization"); //没有token返回未认证 if(token==null||token==""){ response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } try{ this.getJwtByToken(token,jwtKey,null); return chain.filter(exchange); } catch(Exception ex){ logger.error(ex.getMessage()); response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } } @Override public int getOrder() { return 0; } private DecodedJWT getJwtByToken(String token, String key, Map<String,String> claims) throws Exception{ Algorithm algorithm = Algorithm.HMAC256(key); Verification verifier= JWT.require(algorithm) .withIssuer("liujb"); if(claims!=null){ claims.forEach((mapKey,mapValue)->{ verifier.withClaim(mapKey,mapValue); });} JWTVerifier ver= verifier.build(); DecodedJWT jwt=ver.verify(token); return jwt; } }
这里只做了简单的认证,以此基础进行深化,如根据不同的服务名称要求不同的claim。
3,测试
加入Authorization关,值为之前获取的jwt token
如果token不对或没有token都将返回401状态值