本文基于Nacos作为注册中心,进行搭建,所以前提是已经搭建了Nacos
Nacos单机版搭建教程
Nacos集群版搭建教程
1.导入依赖
注意
:gateway的pom文件不要引用MVC的依赖包,不然会报错。
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-openfeign
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.wf
api-commons
${project.version}
org.projectlombok
lombok
true
2.编写启动类
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @Description: 网关微服务
*/
@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class GatewayServiceApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayServiceApplication.class, args);
log.info("网关微服务启动成功");
}
}
3.编写配置文件
server:
port: 9005
spring:
application:
name: gateway-service
cloud:
#nacos相关配置-------------------------------------------------
nacos:
discovery:
#配置Nacos服务注册地址
server-addr: 192.168.31.78:8848
gateway:
discovery:
locator:
#开启注册中心路由功能
enabled: true
3.通过Gateway访问9004微服务
请求:http://localhost:9005/message-services/test
注:本次请求携带了9004微服务的服务名称且端口号已经换成了9005
yml
文件内容server:
port: 9005
spring:
application:
name: gateway-service
cloud:
#nacos相关配置-------------------------------------------------
nacos:
discovery:
#配置Nacos服务注册地址
server-addr: 192.168.31.78:8848
gateway:
discovery:
locator:
#开启注册中心路由功能
enabled: true
routes:
#该组配置的一个id值,需要保证他的唯一,可以设置为和服务名一致
- id: message-services
#通过条件匹配之后需要路由到的新的服务地址,lb:// 表示开启负载均衡策略去路由
uri: lb://message-services
#url的匹配条件,及断言,规则为url中必须携带 /user/management才能进行转发
predicates:
- Path=/message/board/**
#在路由前对请求的地址进行额外的其他操作,例如拼接或者裁减等。 此处的作用在于去除掉 predicates中配置的路径,因为该路径只是断言,并无实际地址
filters:
#表示删除第二个路径,即删除predicates中配置的/message/board
- StripPrefix=2
2.再次使用上文中的测试地址测试
请求:http://localhost:9005/message-services/test
返回结果:
提示404访问不到资源,因为此处使用了自定义路由,那么就应该携带自定义的请求路径
3.使用自定义的请求路由访问
请求:http://localhost:9005/message/board/test
注:此处使用的/message/board在9004微服务中并不存在该资源,此处只是在gateway中yml文件中predicates配置的自定义路由,然后通过filters中的配置去除掉/message/board得到9004微服务的真实资源地址
返回结果:
解析请求过程:
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.symmetric.SymmetricCrypto;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.deploy.nativesandbox.comm.Response;
import com.wf.apicommons.utils.CodeEnum;
import com.wf.apicommons.utils.CommonResult;
import com.wf.apicommons.utils.JWTUtils;
import com.wf.apicommons.utils.MD5Util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @Description: token鉴权过滤
*/
@Component
@Slf4j
public class AuthJwtFilter implements GlobalFilter, Ordered {
/**
* 无需鉴权的URL
*/
private static final String[] skipAuthUrls={"/user/login","/user/register","/user/getCode/**"};
@Override
public Mono<Void>
filter(ServerWebExchange exchange,GatewayFilterChain chain) {
//获取请求url地址
String url =exchange.getRequest().getURI().getPath();
//跳过不需要验证的路径
if (null != skipAuthUrls && isSkipUrl(url)) {
return chain.filter(exchange);
}
//从请求头中取得token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
//标识当前请求来源于网关,屏蔽非法请求
exchange.getRequest().mutate().header("RequestSource", MD5Util.encryption("from_gateway")).build();
CommonResult<String> result=new CommonResult<>();
//判断token是否为null
if(!StrUtil.hasBlank(token)){
try{
//验证令牌
JWTUtils.verify(token);
//放行请求
return chain.filter(exchange);
} catch (SignatureVerificationException e) {
//无效签名
result.setCode(CodeEnum.JWT_INVALID.getCode());
result.setData(CodeEnum.JWT_INVALID.getMessage());
} catch (TokenExpiredException e) {
//token过期
result.setCode(CodeEnum.JWT_OVERDUE.getCode());
result.setData(CodeEnum.JWT_OVERDUE.getMessage());
} catch (AlgorithmMismatchException e) {
//token算法不一致\
result.setCode(CodeEnum.JWT_ALGORITHM_INCONSISTENCY.getCode());
result.setData(CodeEnum.JWT_ALGORITHM_INCONSISTENCY.getMessage());
} catch (Exception e) {
//token失效
result.setCode(CodeEnum.JWT_LOSE_EFFECT.getCode());
result.setData(CodeEnum.JWT_LOSE_EFFECT.getMessage());
}
}else{
//无效签名
result.setCode(CodeEnum.JWT_INVALID.getCode());
result.setData(CodeEnum.JWT_INVALID.getMessage());
}
return getFailResponse(exchange.getResponse(),result);
}
@Override
public int getOrder() {
return 0;
}
/**
* 判断当前访问的url是否开头URI是在配置的忽略
* url列表中
*
* @param url
* @return
*/
public boolean isSkipUrl(String url) {
for (String skipAuthUrl : skipAuthUrls) {
if (url.startsWith(skipAuthUrl)) {
return true;
}
}
return false;
}
/**
* 获取失败返回信息
* @param response
* @param result
* @return
*/
private Mono<Void> getFailResponse(ServerHttpResponse response, CommonResult<String> result) {
DataBuffer buffer = null;
try {
//将map转化成json,response使用的是Jackson
String resultStr = new ObjectMapper().writeValueAsString(result);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.getHeaders().set("Access-Control-Allow-Origin","*");
response.setStatusCode(HttpStatus.OK);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
buffer = response.bufferFactory().wrap(resultStr.getBytes("UTF-8"));
} catch (Exception e) {
log.info("gateway鉴权错误,{}",e.getMessage());
e.printStackTrace();
}
return response.writeWith(Flux.just(buffer));
}
}
此处的使用到的自定义工具类:
CodeEnum
import lombok.Data;
/**
* 状态码枚举
*/
public enum CodeEnum {
/**操作成功**/
SUCCESS(200,"操作成功"),
/**服务调用异常**/
SERVICE_CALL_EXCEPTION(400,"服务调用异常"),
/**操作失败**/
ERROR(500,"操作失败"),
/**参数不合法**/
ILLEGAL_PARAMETER(5001,"参数不合法"),
/**验证码已失效**/
VERIFICATION_CODE_FAILURE(5002,"验证码已失效"),
/**用户昵称重复**/
DUPLICATE_NICKNAME(5003,"用户昵称重复"),
/**用户名或密码错误**/
LOGIN_FAILED(5004,"用户名或密码错误"),
/**文件上传失败**/
FILE_UPLOAD_FAILED(5005,"文件上传失败"),
/**资源不存在*/
RESOURCE_DOES_NOT_EXIST(5006,"资源不存在"),
/**无效签名**/
JWT_INVALID(2001,"无效签名"),
/**token过期**/
JWT_OVERDUE(2002,"token过期"),
/**token算法不一致**/
JWT_ALGORITHM_INCONSISTENCY(2003,"token算法不一致"),
/**token失效**/
JWT_LOSE_EFFECT(2004,"token失效"),
/**非法请求**/
ILLEGAL_REQUEST(2005,"非法请求,请求来源不合法");
/**
* 自定义状态码
**/
private Integer code;
/**自定义描述**/
private String message;
CodeEnum(Integer code, String message){
this.code = code;
this.message = message;
}
public Integer getCode() {
return code;
}
public String getMessage() {
return message;
}
}
CommonResult
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 请求信息类,用于返回请求是否成功
* @param
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T>{
/**
* 响应状态码
*/
private int code;
/**
* 响应结果描述
*/
private String message;
/**
* 返回的数据
*/
private T data;
/**
* 成功返回
* @param data
* @param
* @return
*/
public static <T> CommonResult<T> success(T data) {
CommonResult<T> response= new CommonResult<>();
response.setCode(CodeEnum.SUCCESS.getCode());
response.setMessage(CodeEnum.SUCCESS.getMessage());
response.setData(data);
return response;
}
/**
* 失败返回,自定义code
* @param code
* @param message
* @param
* @return
*/
public static <T> CommonResult<T> fail(Integer code, String message) {
CommonResult<T> response = new CommonResult<>();
response.setCode(code);
response.setMessage(message);
return response;
}
/**
* 失败返回
* @param codeEnum
* @param
* @return
*/
public static <T> CommonResult<T> fail(CodeEnum codeEnum) {
CommonResult<T> response = new CommonResult<>();
response.setCode(codeEnum.getCode());
response.setMessage(codeEnum.getMessage());
return response;
}
/**
* 失败返回
* @param message
* @param
* @return
*/
public static <T> CommonResult<T> fail(String message) {
CommonResult<T> response = new CommonResult<>();
response.setCode(CodeEnum.ERROR.getCode());
response.setMessage(message);
return response;
}
}
JWTUtils
import cn.hutool.jwt.JWTUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: JWT工具类
*/
public class JWTUtils {
//秘钥
private static final String SIGNATURE = "323123213123213123313";
//过期时间为1天
public static final Integer EXPIRATION_TIME= 1* 24 * 60 * 60;
/**
* 生成token
* @param payload token需要携带的信息
* @return token字符串
*/
public static String getToken(Map<String,String> payload){
// 指定token过期时间为1天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.SECOND, EXPIRATION_TIME);
JWTCreator.Builder builder = JWT.create();
// 构建payload
payload.forEach((k,v) -> builder.withClaim(k,v));
// 指定过期时间和签名算法
return builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(SIGNATURE));
}
/**
* 验证token
* @param token
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
}
/**
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(SIGNATURE)).build().verify(token);
}
}
MD5Util
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
/**
* @Description: MD5工具类
*/
public class MD5Util {
/**
* 加密
* @param content 需要加密的内容
* @return
*/
public static String encryption(String content) {
if(StrUtil.isNotBlank(content)){
return SecureUtil.md5(content);
}
return null;
}
/**
* 验证
*
* @param ciphertext 密文
* @return
*/
public static boolean verifyingCiphertext(String ciphertext,String content) {
if(StrUtil.isNotBlank(ciphertext)&&StrUtil.isNotBlank(content)){
return ciphertext.equals(encryption(content));
}
return false;
}
}
注:以上代码需要引入胡图工具包
<!--胡图工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.2</version>
</dependency>
真实场景中,往往是把微服务放到各个服务器,并不会对外暴露出来的,仅仅会对网关处的服务器进行开放,因此最好的做法就是不会暴露服务,nginx反向代理到网关,网关转发到对应的服务,真正对外提供的仅有nginx
此处我们在gateway转发请求是在Header中存入一个加密标识(from_gateway
),等转发到微服务后再将其获取出来,进行解密,比对,只有正确的才放行
上述AuthJwtFilter
类添加如下代码:
//标识当前请求来源于网关,屏蔽非法请求
exchange.getRequest().mutate().header("RequestSource", MD5Util.encryption("from_gateway")).build();
在9004微服务中新增如下代码:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.wf.apicommons.utils.CodeEnum;
import com.wf.apicommons.utils.CommonResult;
import com.wf.apicommons.utils.MD5Util;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Description: JWT验证拦截器
*/
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
CommonResult<String> result=new CommonResult<>();
//获取请求来源
String requestSource = request.getHeader("RequestSource");
boolean decrypt = MD5Util.verifyingCiphertext(requestSource,"request_from_gateway");
//验证请求来源是否为网关转发
if(decrypt){
//放行请求
return true;
}else{
result.setCode(CodeEnum.ILLEGAL_REQUEST.getCode());
result.setData(CodeEnum.ILLEGAL_REQUEST.getMessage());
}
//将map转化成json,response使用的是Jackson
String json = new ObjectMapper().writeValueAsString(result);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin","*");
response.getWriter().print(json);
return false;
}
}
测试:
请求:http://localhost:9004/test
返回结果:
通过网关测试:
请求:http://localhost:9005/message/board/test