前言
在springCloud的世界中,zuul网关服务是一个不得不提的服务。
在实际的开发中,当前端要调用后台的服务的时候,可以直接通过ip+端口+接口名的形式调用,但是这样调用有不好的地方,这样会暴露我们的Api,别人可以轻而易举的知道我们的实体ip的地址,以及知道我们的端口,和我们的服务名,还有我们的接口名。
而且当我们在服务中做登录校验和权限校验的时候,如果没有zuul或者gateway网关等服务,那么我们就需要在每一个实际的业务服务中编写我们的权限校验和登录校验的代码,也就是说,这些和业务无关,但是又必须需要的代码,我们会多次的频繁的重复编写,这无疑是不合理的行为。
也因此,我考虑到了这些不合理的地方,决定使用springCloud的zuul网关服务来统合这些代码,提炼出来。
zuul的逻辑图
正文
首先,我搭建的服务是使用了服务治理eureka的,所以我的zuul服务也需要注册到eureka上,而eureka的实际搭建过程,我不再一一讲述,如果有兴趣的话,可以去翻看我之前的博文,是由讲述的。
本次为了描述zuul的一个使用,我使用了三个服务,一个eureka服务治理的服务,一个permission用来登录以及和用户相关的一些服务,另外一个就是zuul服务,zuul服务的目的是为了实现登录校验和权限检验,以及路由跳转。
首先,我会打开eureka服务,启动。
因为我在eureka服务中的配置是如下图所示:
所以,我打开谷歌浏览器,输入地址:localhost:1111,就可以得到如下图所示的界面:接下来,我要启动perimission服务,permission服务同样要注册到eureka上。启动成功。
可以看到,permission服务已经注册到了eureka上面了。好的,接下来的话,我需要开始创建zuul服务。
因为我是已经创建好zuul服务了,所以我直接把zuul服务的整体架构都贴出来,给到大家查看。
zuul服务的整体配置
大家可以看到,我的zuul服务,主要就是这些配置文件,接下来,我将这些配置文件全部都贴出来。
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.blog</groupId>
<artifactId>zuulService</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuulService</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</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>
<!--redis依赖-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<!-- 引入Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!--json依赖-->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.4</version>
<classifier>jdk15</classifier>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 引入spring cloud的依赖,不能少,主要用来管理Spring Cloud生态各组件的版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Edgware.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
application.properties文件
spring.application.name=zuul-service
server.port=8000
#这个是自己设置的,让外部环境来访问的API路由,请求经由这个定向到真实注册eureka上的服务
zuul.routes.permission.path=/permissionApi/**
#这个是真实的注册到了Eureka上门的服务名字
zuul.routes.permission.serviceId=permission
#允许敏感头,设置为空
zuul.sensitive-headers=
#注册到eureka上的服务的名字
eureka.instance.instance-id=http://${spring.cloud.client.ipAddress}:${server.port}/${spring.application.name}
#表示本服务发送给eureka服务端的心跳时间,设置时间不能太长,不然会导致eureka检测不到服务从而会驱逐服务下线
eureka.instance.leaseRenewalIntervalInSeconds: 2
#当本服务掉线后,eureka服务端取掉本服务的缓存的时间
eureka.instance.leaseExpirationDurationInSeconds: 4
# 注册中心地址
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka
#redis数据库索引(默认为0)
spring.redis.database=0
#redis所在的服务器
spring.redis.host=localhost
#redis的端口
spring.redis.port=6379
#redis的密码
spring.redis.password=123456
#连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=8
#连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
#连接池中的最大空闲连接
spring.redis.pool.max-idle=8
#连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0
ZuulServiceApplication.java启动文件
package com.blog.zuulService;
import com.blog.zuulService.config.AccessFilter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
@EnableZuulProxy
public class ZuulServiceApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServiceApplication.class, args);
}
@Bean
public AccessFilter tokenFilter() {
return new AccessFilter();
}
}
第一个过滤器AccessFilter.java文件——token过滤器
当接口进来zuul服务的时候,最先经过的过滤器是AccessFilter过滤器。因为在这个过滤器中有一个方法,这个方法中如下:
/**
* filter执行顺序,通过数字指定,数字越小,执行顺序越先
* @return
*/
@Override
public int filterOrder() {
return 2;
}
很明显,我设置的数字是2,而执行完AccessFilter过滤器后,继续执行的过滤器有LoginFilter过滤器,我设置的数字是3,再然后要经过的过滤器是PermissionFilter,我设置的数字是4。
这个过滤器的目的是为了过滤没有token的请求,我会将整个类的代码都贴出来,而且在代码上面,我是在整体代码上添加了非常详细的注释,在看代码的时候结合注释一起观看。
package com.blog.zuulService.config;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class AccessFilter extends ZuulFilter {
/**
* 过滤器类型,前置过滤器
* filter类型,分为pre、error、post、 route
* pre: 请求执行之前filter
* route: 处理请求,进行路由
* post: 请求处理完成后执行的filter
* error: 出现错误时执行的filter
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* filter执行顺序,通过数字指定,数字越小,执行顺序越先
* @return
*/
@Override
public int filterOrder() {
return 2;
}
/**
* 过滤器是否生效
* 返回true代表需要本过滤器进行过滤,返回false代表不需要本过滤器进行过滤
*/
@Override
public boolean shouldFilter() {
//共享RequestContext,上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
System.out.println(request.getRequestURI());
//不需要token校验的URL
if ("permissionApi/user/login".equalsIgnoreCase(request.getRequestURI())) {
//登录接口不需要校验
return false;
} else if ("permissionApi/user/logOut".equalsIgnoreCase(request.getRequestURI())) {
//登出接口不需要校验
return false;
}else if ("permissionApi/user/register".equalsIgnoreCase(request.getRequestURI())) {
//注册接口不需要校验
return false;
}
return true;
}
/**
* token校验
* 只有上面返回true的时候,才会进入到该方法
*/
@Override
public Object run() {
//JWT
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//token对象,有可能在请求头传递过来,也有可能是通过参数传过来,实际开发一般都是请求头方式
String token = request.getHeader("token");
if (StringUtils.isBlank((token))) {
token = request.getParameter("token");
}
System.out.println("页面传来的token值为:" + token);
//登录校验逻辑 如果token为null,则直接返回客户端,而不进行下一步接口调用
if (StringUtils.isBlank(token)) {
// 过滤该请求,不对其进行路由
requestContext.setSendZuulResponse(false);
//返回错误代码
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
//设置返回值
requestContext.setResponseBody("{\"result\":\"token is not correct!\"}");
}else{
// 对其进行路由
requestContext.setSendZuulResponse(true);
//返回正确代码
requestContext.setResponseStatusCode(200);
}
return null;
}
}
第二个过滤器LoginFilter.java文件——登录过滤器
package com.blog.zuulService.config;
import com.blog.zuulService.method.CookieUtils;
import com.blog.zuulService.method.RedisTemplate;
import com.blog.zuulService.vo.UserInfo;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import net.sf.json.JSONObject;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
/**
* 登录校验
*/
@Component
public class LoginFilter extends ZuulFilter {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 过滤器类型,前置过滤器
* filter类型,分为pre、error、post、 route
* pre: 请求执行之前filter
* route: 处理请求,进行路由
* post: 请求处理完成后执行的filter
* error: 出现错误时执行的filter
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* filter执行顺序,通过数字指定,数字越小,执行顺序越先
* @return
*/
@Override
public int filterOrder() {
return 3;
}
/**
* 过滤器是否生效
* 返回true代表需要本过滤器进行过滤,返回false代表不需要本过滤器进行过滤
*/
@Override
public boolean shouldFilter() {
//共享RequestContext,上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
System.out.println(request.getRequestURI());
//不需要登录校验的URL
//不需要token校验的URL
if ("permissionApi/user/login".equalsIgnoreCase(request.getRequestURI())) {
//登录接口不需要校验
return false;
} else if ("permissionApi/user/logOut".equalsIgnoreCase(request.getRequestURI())) {
//登出接口不需要校验
return false;
}else if ("permissionApi/user/register".equalsIgnoreCase(request.getRequestURI())) {
//注册接口不需要校验
return false;
}
return true;
}
/**
* 业务逻辑
* 只有上面返回true的时候,才会进入到该方法
*/
@Override
public Object run() {
HttpServletRequest request=((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response=requestContext.getResponse();
//取用户token
String token = null;
try {
token = CookieUtils.getCookieValue(request, "cookie_token_key");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if (token==null){
token=request.getHeader("token");
}
//判断是否为空
try {
if (StringUtils.isBlank(token)) {
//跳转到登录页面
response.sendRedirect(request.getRequestURL() + "?redirect=" + getBaseURL(request));
requestContext.setSendZuulResponse(false);
//返回错误代码
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
return false;
} else {
//如果能取到token说明用户可能已经登录
//从sso中取用户信息,判断用户是否登录
UserInfo userInfo = this.getUserByToken(token, request);
//判断用户是否过期
if (userInfo == null) {
//跳转到登录页面
response.sendRedirect(request.getRequestURL() + "?redirect=" + getBaseURL(request));
requestContext.setSendZuulResponse(false);
//返回错误代码
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
} else {
request.setAttribute("blog_userInfo",userInfo);
//将登陆信息放到请求信息中中,目的是为了将用户信息传递到具体的业务服务中去
JSONObject jsonString = JSONObject.fromObject(userInfo);
RequestContext.getCurrentContext().addZuulRequestHeader("blog_userInfo", URLEncoder.encode(jsonString.toString(), "UTF-8"));
// 对其进行路由
requestContext.setSendZuulResponse(true);
//返回正确代码
requestContext.setResponseStatusCode(200);
}
}
}catch (Exception e){
e.printStackTrace();
}
return null;
}
private UserInfo getUserByToken(String token, HttpServletRequest request) {
String url=getIP(request);
UserInfo userInfo= (UserInfo) redisTemplate.get("USER_INFO:"+token+url,new UserInfo());
if (userInfo==null){
return userInfo;
}
//刷新一下token的过期时间
redisTemplate.set("USER_INFO:"+token+request.getRemoteAddr(),userInfo,600);
return userInfo;
}
/**
* 这是获取需要传递的url,以便后续用户登录后直接跳转到这个页面
* @param request
* @return
*/
private String getBaseURL(HttpServletRequest request) {
String url = request.getScheme()
+ "://"
+ request.getServerName()
+ ":"
+ request.getServerPort()
+ request.getContextPath()
+ request.getRequestURI();
return url;
}
public static String getIP(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (!checkIP(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (!checkIP(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (!checkIP(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
private static boolean checkIP(String ip) {
if (ip == null || ip.length() == 0 || "unkown".equalsIgnoreCase(ip)
|| ip.split(".").length != 4) {
return false;
}
return true;
}
}
第三个过滤器PermissionFilter.java文件——权限过滤器
package com.blog.zuulService.config;
import com.blog.zuulService.method.RedisTemplate;
import com.blog.zuulService.vo.ListSerirlizable;
import com.blog.zuulService.vo.UserInfo;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
/**
* 权限校验
*/
@Component
public class PermissionFilter extends ZuulFilter {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 过滤器类型,前置过滤器
* filter类型,分为pre、error、post、 route
* pre: 请求执行之前filter
* route: 处理请求,进行路由
* post: 请求处理完成后执行的filter
* error: 出现错误时执行的filter
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* filter执行顺序,通过数字指定,数字越小,执行顺序越先
* @return
*/
@Override
public int filterOrder() {
return 4;
}
/**
* 过滤器是否生效
* 返回true代表需要本过滤器进行过滤,返回false代表不需要本过滤器进行过滤
*/
@Override
public boolean shouldFilter() {
//共享RequestContext,上下文对象
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
System.out.println(request.getRequestURI());
//不需要权限校验的URL
if ("permissionApi/user/login".equalsIgnoreCase(request.getRequestURI())) {
//登录接口不需要校验
return false;
} else if ("permissionApi/user/logOut".equalsIgnoreCase(request.getRequestURI())) {
//登出接口不需要校验
return false;
}else if ("permissionApi/user/register".equalsIgnoreCase(request.getRequestURI())) {
//注册接口不需要校验
return false;
}
return true;
}
/**
* 业务逻辑
* 只有上面返回true的时候,才会进入到该方法
*/
@Override
public Object run() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletResponse response=requestContext.getResponse();
HttpServletRequest request=((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
UserInfo userInfo= (UserInfo) request.getAttribute("blog_userInfo");
//获取存储到redis中的角色对应的权限信息
List<String> interfaceNameList= ((ListSerirlizable) redisTemplate.get(userInfo.getRoleCode(),new ListSerirlizable())).getInterfaceNameList();
if (interfaceNameList!=null&&interfaceNameList.size()>0){
//获取http中获取的,接口的地址url
String interfaceName=getInterfaceName(request);
//判断该角色是否有该接口的权限
if (interfaceNameList.contains(interfaceName)){
// 对其进行路由
requestContext.setSendZuulResponse(true);
//返回正确代码
requestContext.setResponseStatusCode(200);
}else{
//跳转到登录页面
try {
response.sendRedirect(request.getRequestURL() + "?redirect=" + getBaseURL(request));
} catch (IOException e) {
e.printStackTrace();
}
requestContext.setSendZuulResponse(false);
//返回错误代码
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
}else{
//跳转到登录页面
try {
response.sendRedirect(request.getRequestURL() + "?redirect=" + getBaseURL(request));
} catch (IOException e) {
e.printStackTrace();
}
requestContext.setSendZuulResponse(false);
//返回错误代码
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}
return null;
}
private String getInterfaceName(HttpServletRequest request) {
String permission=request.getRequestURI();
permission=permission.substring(1,permission.length());
String interfaceName=permission.substring(permission.indexOf("/"),permission.length());
return interfaceName;
}
/**
* 这是获取需要传递的url,以便后续用户登录后直接跳转到这个页面
* @param request
* @return
*/
private String getBaseURL(HttpServletRequest request) {
String url = request.getScheme()
+ "://"
+ request.getServerName()
+ ":"
+ request.getServerPort()
+ request.getContextPath()
+ request.getRequestURI();
return url;
}
}
基本的情况就是这样,因为我是用redis来实现单点登录的,因此我有用到redis的一些工具类,这些类,我就不一一贴出来了,大家有兴趣的,可以私信我。
在第二个过滤器,LoginFilter登录过滤器中,可以看到有如下一段代码:
//获取用户信息,判断用户是否登录
UserInfo userInfo = this.getUserByToken(token, request);
//判断用户是否过期
if (userInfo == null) {
//跳转到登录页面
response.sendRedirect(request.getRequestURL() + "?redirect=" + getBaseURL(request));
requestContext.setSendZuulResponse(false);
//返回错误代码
requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
} else {
request.setAttribute("blog_userInfo",userInfo);
//将登陆信息放到请求信息中中,目的是为了将用户信息传递到具体的业务服务中去
JSONObject jsonString = JSONObject.fromObject(userInfo);
RequestContext.getCurrentContext().addZuulRequestHeader("blog_userInfo", URLEncoder.encode(jsonString.toString(), "UTF-8"));
// 对其进行路由
requestContext.setSendZuulResponse(true);
//返回正确代码
requestContext.setResponseStatusCode(200);
}
具体到就是以下这两句代码,可以看出我将用户信息转换为了utf-8编码规格的json字符串数据,存在了head中。
//将登陆信息放到请求信息中中,目的是为了将用户信息传递到具体的业务服务中去
JSONObject jsonString = JSONObject.fromObject(userInfo);
RequestContext.getCurrentContext().addZuulRequestHeader("blog_userInfo", URLEncoder.encode(jsonString.toString(), "UTF-8"));
目的,就是要在permission服务中,添加一个过滤器,将这个用户信息取出来,放到threadLocal中使用。
permission服务中的过滤器
package com.blog.permission.config;
import com.blog.permission.method.SsoSession;
import com.blog.permission.vo.other.UserInfo;
import net.sf.json.JSONObject;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLDecoder;
@Component
public class GetUserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String user=((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest()
.getHeader("blog_userInfo");
user = URLDecoder.decode(user, "UTF-8");//解码
JSONObject jsonObject=JSONObject.fromObject(user);
UserInfo userInfo= (UserInfo) JSONObject.toBean(jsonObject,UserInfo.class);
//将登陆信息放到ThreadLocal中
SsoSession.set(userInfo);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
//去除掉在ThreadLocl中的用户信息
SsoSession.remove();
}
}
这样的话,就基本实现了我们想要做的权限校验和登录校验了。
总结
启动eureka服务,再启动permission服务,再启动zuul服务。
先进行登录获取一个token。
打开postman,写好借口,如下图。
可以看到,我填写的url是:
localhost:8000/permissionApi/user/selectAllUser?pageNum=1&pageSize=10
在head中,我也填写了token,调用接口,我发现成功了。
。其他的,就不一一说明了,大家有兴趣可以自己自行探索。