网关服务的作用:
身份认证、路由服务、为前端服务的后端—数据聚合
- 身份认证
如果我们的微服务和终端通信,势必要考虑身份认证,如果我们的微服务都与每个终端用户打交道,那么这些代码就需要拷贝多份,
并且植入到每个微服务业务代码中,这就造成业务代码和身份认证代码耦合,降低代码的复用性。
- 路由服务
由运维人员手动维护路由规则和服务实例列表是非常费工夫的且容易出错。
- 为前端服务的后端
比如将多个服务的数据聚合在一起返回给前端
为了解决上面的架构问题,API网关的概念应运而生,它的定义类似于面向对象设计模式中的Facade模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。
由它来实现请求路由、负载均衡、校验过滤等功能,以及与服务治理框架的结合,请求转发的熔断机制、服务的聚合等。
SpringCloud Zuul组件能非常好的解决这些问题,在服务路由的时候,我们看它如何方便的解决了这个问题。
创建一个API服务网关工程
简单使用:
1、添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
2、配置文件
zuul.routes.api-a-url.path=/hello/**
zuul.routes.api-a-url.url=http://localhost:9000/
所有符合/hello/**规则的访问都将被路由转发到http://localhost:9000/地址上,其中api-a-url是路由的名字,可以任意定义。
3、启动类
@SpringBootApplication
@EnableZuulProxy
public class GatewayServiceZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceZuulApplication.class, args);
}
}
启动类添加@EnableZuulProxy
,支持网关路由。
面向服务的路由
实际上上面那种方式同样需要运维人员花费大量的时间来维护各个路由path和url的关系。为了解决这个问题,Zuul实现了于Eureka的无缝结合,我们可以让路哟的path不是映射具体的url,而是让它映射到某个具体的服务,而
具体的url则交给Eureka的服务发现机制去自动维护。
1、添加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
增加spring-cloud-starter-eureka
包,添加对eureka的支持。
2、配置文件
配置修改为:
spring.application.name=gateway-service-zuul
server.port=8888
zuul.routes.api-a.path=/producer/**
zuul.routes.api-a.serviceId=spring-cloud-producer
eureka.client.serviceUrl.defaultZone=http://localhost:8000/eureka/
项目采取方案
由于项目采取的并不是前后端分离的架构,所有的请求到达API 服务网关,zuul进行路由,可是并不能将各服务返回页面的进行聚合再返回给浏览器。
而是采用了一种次等的策略,将所有的静态资源放在API网关中,API网关接收请求,调用各个服务接口,将返回数据的数据进行聚合,然后给API网关的页面进行渲染。
这里身份认证JWT可以单独作为一个认证服务被调用。基于JWT的token身份认证方案
以一个登陆请求为例:
API网关的UserController中的方法:
@RequestMapping(value="/accounts/signin",method={RequestMethod.POST,RequestMethod.GET})
public String loginSubmit(HttpServletRequest req){
String username = req.getParameter("username");
String password = req.getParameter("password");
if (username == null || password == null) {
req.setAttribute("target", req.getParameter("target"));
return "/user/accounts/signin";
}
User user = accountService.auth(username, password);
if (user == null) {
return "redirect:/accounts/signin?" + "username=" + username + "&" + ResultMsg.errorMsg("用户名或密码错误").asUrlParams();
}else {
UserContext.setUser(user);
return StringUtils.isNotBlank(req.getParameter("target")) ? "redirect:" + req.getParameter("target") : "redirect:/index";
}
}
}
API网关的UserService中的方法
public User auth(String username, String password) {
if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
return null;
}
User user = new User();
user.setEmail(username);
user.setPasswd(password);
try {
user = userDao.authUser(user);
} catch (Exception e) {
return null;
}
return user;
}
API网关的UserDao中的方法
@HystrixCommand
public User authUser(User user) {
String url = "http://" + userServiceName + "/user/auth";
ResponseEntity<RestResponse<User>> responseEntity = rest.post(url, user, new ParameterizedTypeReference<RestResponse<User>>() {});
RestResponse<User> response = responseEntity.getBody();
if (response.getCode() == 0) {
return response.getResult();
}{
throw new IllegalStateException("Can not add user");
}
}
这里的方法添加了@HystrixCommand用于进行服务的熔断,这个后面会介绍它。
经过RestTemplate拦截请求,转发到某个服务实例上。注意它返回的是ResponseEntity<T>泛型对象,其中T是由ParameterizedTypeReference<T>中的T指定。
responseEntity.getBody()就能获取到实际返回的对象。
User-Service服务UserController中的auth方法:
注意所有的服务API返回的数据都是RestResponse的json数据,RestResponse包含状态码,状态信息,返回的数据
RestResponse
public class RestResponse<T> {
private int code;
private String msg;
private T result;
public static <T> RestResponse<T> success() {
return new RestResponse<T>();
}
public static <T> RestResponse<T> success(T result) {
RestResponse<T> response = new RestResponse<T>();
response.setResult(result);
return response;
}
public static <T> RestResponse<T> error(RestCode code) {
return new RestResponse<T>(code.code,code.msg);
}
public RestResponse(){
this(RestCode.OK.code, RestCode.OK.msg);
}
public RestResponse(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getResult() {
return result;
}
public void setResult(T result) {
this.result = result;
}
@Override
public String toString() {
return "RestResponse [code=" + code + ", msg=" + msg + ", result=" + result + "]";
}
}
View Code
@RequestMapping("auth")
public RestResponse<User> auth(@RequestBody User user){
User finalUser = userService.auth(user.getEmail(),user.getPasswd());
return RestResponse.success(finalUser);
}
User-Service服务UserService中的auth方法:
/**
* 校验用户名密码、生成token并返回用户对象
* @param email
* @param passwd
* @return
*/
public User auth(String email, String passwd) {
if (StringUtils.isBlank(email) || StringUtils.isBlank(passwd)) {
throw new UserException(Type.USER_AUTH_FAIL,"User Auth Fail");
}
User user = new User();
user.setEmail(email);
user.setPasswd(HashUtils.encryPassword(passwd));
//user.setEnable(1);
List<User> list = getUserByQuery(user);
if (!list.isEmpty()) {
User retUser = list.get(0);
onLogin(retUser);
return retUser;
}
throw new UserException(Type.USER_AUTH_FAIL,"User Auth Fail");
}
//生成token的操作
private void onLogin(User user) {
//最后一个是时间戳
String token = JwtHelper.genToken(ImmutableMap.of("email", user.getEmail(), "name", user.getName(),"ts",Instant.now().getEpochSecond()+""));
renewToken(token,user.getEmail());
user.setToken(token);
}
//重新设置缓存过期时间
private String renewToken(String token, String email) {
redisTemplate.opsForValue().set(email, token);
redisTemplate.expire(email, 30, TimeUnit.MINUTES);
return token;
}