當你的項目中服務越來越多,每個服務都有自己的監聽地址而又需要把這些服務提供給各式的客戶端或第三方使用,那么需要把每個服務地址都暴露出來嗎?如果某個服務有多個運行實例,如果進行負載均衡?用戶認證和授權需要在每個服務上都做嗎,能否統一做?要解決這些問題,就需要用到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狀態值