文章目录
前言
前面我们讲述了通过网关层(Gateway)增加全局过滤器,完成了对服务的校验,确保服务的安全。
但是我们把请求地址从localhost:8888/provider/hello
,修改为localhost:9000/provider/hello
,直接请求到提供者服务,还是可以拿到数据。
什么阿猫阿狗都来访问我的接口,数据都被搞走了,那我饭碗岂不是要不保!
上科技,不对!上狠活,还不对!上代码,做猛男!
先说方案:
- 第一种是从网络层面,我们的服务一般会部署到Linux服务器,通过服务器防火墙或者网络管理员只对外开通网关的端口,这种方法的实现是最简单的。但是,服务器不只你一个系统在用,很多服务的话,就不太可控。
- 第二种是增加通用的微服务拦截器,微服务验证之后增加标志位,后台服务根据标志位判断请求来源,有标志位则继续执行,无标志位给出错误提示并返回。
一、公共模块
1. 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. 响应实体
package org.example.common.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
//统一响应结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;//业务状态码 0-成功 1-失败
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
public static <E> Result<E> success(String message,E data) {
return new Result<>(0, message, data);
}
public static Result success(String message) {
return new Result<>(0, message, null);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}
public static Result error(String message) {
return new Result(1, message, null);
}
public static Result error(int code,String message) {
return new Result(code, message, null);
}
}
3. 启用注解
package org.example.common.annotation;
import org.example.common.config.LoginInterceptorConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 启用拦截器
* Create by zjg on 2024/7/24
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(LoginInterceptorConfiguration.class)
@Documented
public @interface EnableLoginInterceptor {
}
4. 登录拦截器配置类
package org.example.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 登录拦截器配置类
* Create by zjg on 2024/7/24
*/
@Configuration(proxyBeanMethods = false)
public class LoginInterceptorConfiguration {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new CommonWebMvcConfigurer();
}
}
5. 添加拦截器
package org.example.common.config;
import org.example.common.interceptor.LoginInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
/**
* 添加拦截器
* Create by zjg on 2024/7/24
*/
public class CommonWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
List<String> patterns=new ArrayList<>();
patterns.add("/user/login");
registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(patterns);
}
}
6. 登录标识加密
package org.example.common;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* Create by zjg on 2024/7/27
*/
public class CommonTest {
public static void main(String[] args) {
System.out.println(new String(Base64.getEncoder().encode("gateway".getBytes(StandardCharsets.UTF_8))));
System.out.println(new String(Base64.getEncoder().encode("feign".getBytes(StandardCharsets.UTF_8))));
}
}
7. 登录拦截器
package org.example.common.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.example.common.model.Result;
import org.springframework.http.HttpStatus;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.List;
/**
* 登录拦截器
* Create by zjg on 2024/7/24
*/
public class LoginInterceptor implements HandlerInterceptor {
private final List<String> marks=List.of("Z2F0ZXdheQ==","ZmVpZ24=");
private final List<String> whiteList=List.of("/v3/api-docs");
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//白名单
for (String white:whiteList) {
if(request.getRequestURI().endsWith(white))
return true;
}
//这里分为两种mark: gateway、feign
// 1.从网关过来的(外部来源),肯定携带了token值且网关层已经验证过了
// 2.从微服务内部(内部来源)注册中心访问
String mark = request.getHeader("Source-Mark");
if(Boolean.FALSE.equals(StringUtils.hasText(mark))||Boolean.FALSE.equals(marks.contains(mark))){
response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
response.setContentType("application/json;charset=UTF-8");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.writerFor(Result.class);
String message = objectMapper.writeValueAsString(Result.error(HttpStatus.NOT_ACCEPTABLE.value(),"未知的来源,禁止访问!"));
response.getWriter().println(message);
return false;
}
return true;
}
}
二、提供者模块
提供者作为我们的核心模块,首先肯定要登录才能访问,接下来我们来演示使用公共模块的拦截器
关于公共模块的引入之前已经做过了,这里不再展示
1. 启用注解
package org.example.nacos.provider;
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure;
import org.example.common.annotation.EnableLoginInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.ConfigurableApplicationContext;
import java.util.concurrent.TimeUnit;
@EnableLoginInterceptor
@EnableDiscoveryClient
@SpringBootApplication
public class NacosDiscoveryProviderApplication {
public static void main(String[] args) throws InterruptedException {
ConfigurableApplicationContext applicationContext = SpringApplication.run(NacosDiscoveryProviderApplication.class, args);
System.out.println(applicationContext);
// while(true) {
// //当动态配置刷新时,会更新到 Enviroment中,因此这里每隔一秒中从Enviroment中获取配置
// String userName = applicationContext.getEnvironment().getProperty("user.name");
// String userAge = applicationContext.getEnvironment().getProperty("user.age");
// System.err.println("user name :" + userName + "; age: " + userAge);
// TimeUnit.SECONDS.sleep(1);
// }
}
}
2. 启动服务
3. 拦截器测试
到这里我们的拦截器已经成功引入,并生效了,是不是很简单。
三、网关模块(外部来源)
网关模块需要添加登录标识,可以使用
AddRequestHeader
网关过滤器,也可以通过之前的LoginGlobalFilter
登录过滤器添加
1. 调整前
2. 增加请求头
这里我们在登录过滤器安全认证之后,请求转发之前,给所有的请求添加请求头标识
package org.example.gateway.filter;
import org.example.common.util.JwtUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Create by zjg on 2024/7/21
*/
@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String uri = request.getURI().getPath();
if(uri.equals("/user/login")||uri.equals("/user/login/")){
MultiValueMap<String, String> queryParams = request.getQueryParams();
if(queryParams.containsKey("username")&&queryParams.containsKey("password")){
return chain.filter(exchange);
}else {
response.setStatusCode(HttpStatus.BAD_REQUEST);
return response.writeWith(Flux.just(response.bufferFactory().wrap("用户名和密码不能为空".getBytes())));
}
}
HttpHeaders headers = request.getHeaders();
String authorization = headers.getFirst("Authorization");
if(Boolean.FALSE.equals(StringUtils.hasText(authorization))||Boolean.FALSE.equals(JwtUtils.verify(authorization.startsWith("Bearer")?authorization.substring(authorization.indexOf("Bearer")+7):authorization))){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String message="token验证失败,请重新获取token后重试!";
return response.writeWith(Flux.just(response.bufferFactory().wrap(message.getBytes())));
}
request = exchange.getRequest().mutate()
.headers(httpHeaders -> httpHeaders.add("Source-Mark", "Z2F0ZXdheQ==")).build();
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return -1;
}
}
3. 调整后
四、消费者(内部来源)
1. RestTemplate
package org.example.nacos.consumer.config;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestTemplate;
/**
* Create by zjg on 2024/7/16
*/
@Configuration
public class ConsumerConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.defaultHeader("Source-Mark", "ZmVpZ24=").build();
}
}
2. feign
package org.example.nacos.consumer.feign.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
/**
* Create by zjg on 2024/7/27
*/
@Component
public class LoginRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("Source-Mark", "ZmVpZ24=");
}
}
总结
这种模式可以很方便地启用拦截器,极大地提升了系统的灵活性,增加了微服务的安全保障;
核心模块可进一步验证token值,以保证核心服务的安全。