基础使用
创建一个maven模块
在pom中加入
<!--引入gateway 网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
创建启动类额外添加注解@EnableEurekaClient
@SpringBootApplication
@EnableEurekaClient
public class GatewayServer {
public static void main(String[] args) {
SpringApplication.run(GatewayServer.class,args);
}
}
修改配置文件
server:
port: 80
eureka:
instance:
hostname: localhost # 主机名
lease-renewal-interval-in-seconds: 10 #间隔多久向eureka从新发送心跳
lease-expiration-duration-in-seconds: 60 #如果90秒eureka没有收到客户端的心跳就让注册中心剔除该服务
client:
service-url:
# 这里务必记得配置这个,gateway本身也是要注册到eureka的,否则会报错:所有程序都无法正确路由[其实还是能找到,这就很诡异]
defaultZone: http://localhost:8090/eureka # eureka服务端地址通信
spring:
application:
name: gateway #应用名称
cloud:
# 网关配置
gateway:
globalcors:
cors-configurations:
'[/**]': # 匹配所有请求
allowedOrigins: "*" #跨域处理 允许所有的域
allowedMethods: # 支持的方法
- GET
- POST
- PUT
- DELETE
# 路由配置:转发规则
routes: #集合。
# id: 唯一标识。默认是一个UUID
# uri: 转发路径
# predicates: 条件,用于请求网关路径的匹配规则
- id: gateway-consumer
#uri: http://localhost:8089/ #这里是静态的写法直接填写匹配成功后转发的目的地址
uri: lb://eureka-consumer/ #这里则是动态的写法,填写需要转发到的应用名称[由此可以进行负载均衡,因为同名应用可以有好几个]
predicates:
# 这里要注意匹配规则,比如想匹配/user前缀的域名需要使用/user/**,而不是/user或者/user/不同于nginx的配置方法
- Path=/consumer/**,/asster/** #可以匹配多个,以','分隔
Gateway过滤器的配置与使用
例1:
在网关完成token的认证并将个人标识放入请求头进行转发
先补齐netty缺失的一个bean
@Configuration
public class Config {
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}
创建Filter类实现GlobalFilter
, Ordered
接口,
import com.alibaba.fastjson.JSON;
import com.pria.common.ad_enum.CodeEnum;
import com.pria.common.ad_enum.MsgEnum;
import com.pria.common.pojo.vo.HttpResult;
import com.pria.gateway.feign.SSOAuthControllerFeign;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
@Slf4j
@Configuration
public class MyFilter implements GlobalFilter, Ordered {
private final SSOAuthControllerFeign ssoAuthControllerFeign;
public MyFilter(SSOAuthControllerFeign ssoAuthControllerFeign) {
this.ssoAuthControllerFeign = ssoAuthControllerFeign;
}
private final String[] whiteList=
{"/auth/login",
"/auth/sendLoginCode",
"/auth/refreshToken"
};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("全局过滤器执行");
ServerHttpResponse response = exchange.getResponse();
ServerHttpRequest request = exchange.getRequest();
// 白名单检测
List<String> uris = Arrays.asList(whiteList);
String uri = request.getURI().getPath();
if (uris.contains(uri))
return chain.filter(exchange);
//未通过白名单的进行token校验
List<String> tokens = request.getHeaders().get("Authorization");
if (tokens == null || tokens.size() == 0)
//如果不在百名单并且不含token,调用方法获取与一个默认对象写入响应体返回
return getVoidMon(response, false, CodeEnum.TOKEN_NOT_EXIST, MsgEnum.TOKEN_NOT_EXIST);
// sso中的服务我们使用httpResult进行响应,data是用户信息暂时包含id和language
HttpResult httpResult = ssoAuthControllerFeign.checkToken(tokens.get(0));
if (httpResult.getFlag()){
// 执行放行,Token校验通过,将用户信息转成Json放入请求头
request=exchange.getRequest().mutate().header("user", JSON.toJSONString(httpResult.getData())).build(
return chain.filter(exchange.mutate().request(request).build());
}
// 拦截
return getVoidMon(response, false,httpResult.getCode(),httpResult.getMsg());
}
// 配置过滤器级别,也就是顺序级别,数字越小越优先,我们也可以使用注解@Order(1)@WebFilter进行排序
// 启动类需要添加@ServletCompontScan注解
@Override
public int getOrder() {
return 0;
}
/**
* 获取一个空的事务
*
* @param response
* @return
*/
public Mono<Void> getVoidMon(ServerHttpResponse response, Boolean flag, Integer code, String msg) {
// 配置响应头
response.getHeaders().add("Content-type", "application/json;charset=UTF-8");
// 配置响应体 TODO 这里记得改
HttpResult httpResult = new HttpResult(flag, code, msg);
// 开始蜜汁操作,看不懂了
DataBuffer dbf = response.bufferFactory().wrap(
JSON.toJSONString(httpResult).getBytes());
return response.writeWith(Flux.just(dbf));
}
/**
* 获取一个空的事务
*
* @param response
* @return
*/
public Mono<Void> getVoidMon(ServerHttpResponse response, Boolean flag, CodeEnum codeEnum, MsgEnum msgEnum) {
response.getHeaders().add("Content-type", "application/json;charset=UTF-8");
HttpResult httpResult = new HttpResult(flag, codeEnum, msgEnum);
DataBuffer dbf = response.bufferFactory().wrap(
JSON.toJSONString(httpResult).getBytes());
return response.writeWith(Flux.just(dbf));
}
}
我们需要另外准备两个类,
一个为sso负责接收网关的调用,
/**
* 解析token
*
* @param token
* @return
*/
@Override
public HttpResult checkToken(String token) throws ExpiredJwtException, TokenFailureException {
try {
User user = jwtUtils.parseToken(token);
return new HttpResult(true, CodeEnum.SUCCESS, MsgEnum.SUCCESS, user);
} catch (ExpiredJwtException e) {
e.printStackTrace();
// 过期
return new HttpResult(false, CodeEnum.INVALID_TOKEN, MsgEnum.TOKEN_EXPIRED);
} catch (Exception e) {
e.printStackTrace();
// 版本失效
return new HttpResult(false, CodeEnum.INVALID_TOKEN, MsgEnum.INVALID_TOKEN);
}// 解析出错
}
另一个为jjwt工具类,进行token的校验以及数据的封装;
import com.pria.common.ad_enum.RedisKeyEnum;
import com.pria.common.exception.TokenFailureException;
import com.pria.common.pojo.po.User;
import com.pria.common.pojo.vo.MyToken;
import com.pria.sso.config.SaltParamConfig;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;
/**
* token工具,token的成以及校验
*/
@Service
@Slf4j
public class JwtUtils {
private final SaltParamConfig saltParamConfig;
private final RedisTemplate<String, String> redisTemplate;
public JwtUtils(SaltParamConfig saltParamConfig, RedisTemplate<String, String> redisTemplate) {
this.saltParamConfig = saltParamConfig;
this.redisTemplate = redisTemplate;
}
protected String tokenVersionPrefix = RedisKeyEnum.ADEVAR_TOKEN_VERSION.getKey();
/**
* 通过用户对象生成MyToken
*
* @param user
* @return
*/
public MyToken getToken(User user, boolean isNew) {
String tokenVersion = UUID.randomUUID().toString().substring(0,8);
String token = this.getAccessToken(user,tokenVersion);
String refreshToken = this.getRefreshToken(user,tokenVersion);
MyToken myToken = new MyToken();
myToken.setToken(token);
myToken.setRefreshToken(refreshToken);
myToken.setExpired(saltParamConfig.getExpiredTime());
myToken.setIsNew(isNew);
return myToken;
}
/**
* 传入用户生成token
*
* @param user
* @return
*/
protected String getAccessToken(User user,String tokenVersion) {
// 载荷
HashMap<String, Object> claimsMap = new HashMap<>();
claimsMap.put("language", user.getLanguage());
claimsMap.put("id", user.getId());
claimsMap.put("tokenVersion", tokenVersion);
String strId = user.getId().toString();
JwtBuilder jwtBuilder = Jwts.builder()
.setIssuedAt(new Date())
.setClaims(claimsMap)
//设置加密类型和加密盐【HS256,secretKey:xxxxxxx】
.signWith(SignatureAlgorithm.HS256, saltParamConfig.getSalt())
.setExpiration(new Date(saltParamConfig.getExpiredTime()));
String tokenVersionKey = tokenVersionPrefix + strId;
// 版本有效期和token有效期保持一致
redisTemplate.opsForValue().set(tokenVersionKey,
tokenVersion, Duration.ofMillis(saltParamConfig.getTokenTimeOut()));
return jwtBuilder.compact();
}
/**
* 传入用户生成刷新token
*
* @param user
* @return
*/
protected String getRefreshToken(User user,String tokenVersion) {
// 载荷
HashMap<String, Object> claimsMap = new HashMap<>();
claimsMap.put("language", user.getLanguage());
claimsMap.put("id", user.getId());
claimsMap.put("tokenVersion", tokenVersion);
String strId = user.getId().toString();
JwtBuilder jwtBuilder = Jwts.builder()
.setIssuedAt(new Date())
.setClaims(claimsMap)
//设置加密类型和加密盐【HS256,secretKey:xxxxxxx】
.signWith(SignatureAlgorithm.HS256, saltParamConfig.getRefreshSalt())
.setExpiration(new Date(saltParamConfig.getRefreshExpiredTime()));
String tokenVersionKey = tokenVersionPrefix + strId;
// 版本有效期和token有效期保持一致
redisTemplate.opsForValue().set(tokenVersionKey,
tokenVersion, Duration.ofMillis(saltParamConfig.getRefreshTokenTimeOut()));
return jwtBuilder.compact();
}
/**
* 解析token
*
* @return
*/
public User parseToken(String token) throws TokenFailureException {
Jws<Claims> claimsJws = Jwts.parser()
//设置盐
.setSigningKey(saltParamConfig.getSalt())
.parseClaimsJws(token);
//可以通过claimsJws拿到body以及heard
Claims body = claimsJws.getBody();
Long id = Long.parseLong(body.get("id").toString());
String tokenVersion = (String) body.get("tokenVersion");
String language = (String) body.get("language");
String redisTokenVersion = redisTemplate.opsForValue().get(tokenVersionPrefix + id);
if (StringUtils.isBlank(tokenVersion) || !tokenVersion.equals(redisTokenVersion)) {
throw new TokenFailureException();
}
User user = new User();
user.setId(id);
user.setLanguage(language);
return user;
}
/**
* 刷新token
*
* @return
*/
public MyToken refreshToken(String token) throws TokenFailureException{
Jws<Claims> claimsJws = Jwts.parser()
//设置盐
.setSigningKey(saltParamConfig.getRefreshSalt())
.parseClaimsJws(token);
//可以通过claimsJws拿到body以及head
Claims body = claimsJws.getBody();
Long id = Long.parseLong(body.get("id").toString());
String language = (String) body.get("language");
String refreshTokenVersion = (String) body.get("tokenVersion");
String redisRefreshTokenVersion = redisTemplate.opsForValue().get(tokenVersionPrefix + id);
if (StringUtils.isBlank(redisRefreshTokenVersion) || !refreshTokenVersion.equals(redisRefreshTokenVersion)){
throw new TokenFailureException("刷新令牌过期");
}
return this.getToken(new User(id, language), false);
}
//public static void main(String[] args) {
// JwtBuilder jwtBuilder = Jwts.builder()
// //设置id【唯一标识,也就是"jti":556】
// .setId("556")
// //设置签发给谁【"sub":556】
// .setSubject("YiJia")
// //设置签发时间【"iat":xxxx】
// .setIssuedAt(new Date())
// //设置加密类型和加密盐【HS256,secretKey:xxxxxxx】
// .signWith(SignatureAlgorithm.HS256, "JKK&K2jhsld%ajbm4$lkz3")
// .setExpiration(new Date(System.currentTimeMillis()+1000));
// String token = jwtBuilder.compact();
// System.out.println("token:" + token);
//
// Jws<Claims> claimsJws = Jwts.parser()
// .setSigningKey("JKsdasaK&K2jhsldasd%ajbmasd4$lkz3")
// .parseClaimsJws(token);
// Claims body = claimsJws.getBody();
// System.out.println(body.getId());
// System.out.println(body.getSubject());
//}
}
然后其他微服务中我们写一个过滤器来从请求头中获取用户标识id和language并放入Threadlocal完成了身份的识别并共享
首先准备一个工具类,获取Threadlocal,以及提供user的写入和删除功能
public class WmThreadLocalUtils {
private final static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
/**
* 设置当前线程中的用户
* @param user
*/
public static void setUser(User user){
userThreadLocal.set(user);
}
/**
* 获取线程中的用户
* @return
*/
public static User getUser( ){
return userThreadLocal.get();
}
/**
* 清除线程threadLocal信息
*/
public static void removeUser(){
userThreadLocal.remove();
}
}
创建过滤器从请求头获取用户标识id放入线程对象[Spring也可以使用拦截器完成]
创建过滤器继承GenericFilterBean
@Order(1)
@WebFilter(filterName = "wmTokenFilter",urlPatterns = "/*")
@Log4j2
public class WmTokenFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//得到header中的信息
String userId = request.getHeader("userId");
if(userId != null){
WmUser wmUser = new WmUser();
wmUser.setId(Integer.valueOf(userId));
WmThreadLocalUtils.setUser(wmUser);
}
filterChain.doFilter(request,response);
}
}
这里同步演示一下使用拦截器释放线程中的线程对象
首先创建一个拦截器类,继承HandlerInterceptorAdapter
并重写两个方法
preHandle:在请求前做些什么(可以完成如上过滤器的内容)同时还能根据返回的boolean决定是否放行
afterCompletion:在请求响应结束响应前做些什么,可以在这里清除线程对象中的数据
/**
* 拦截器
*/
@Component
public class UserThreadLocalInterceptor extends HandlerInterceptorAdapter {
public UserThreadLocalInterceptor() {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}
然后我们新建一个配置类,来配置拦截器执行顺序,从上向下执行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器加载配置类
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserThreadLocalInterceptor userThreadLocalInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//考虑拦截器的顺序
//registry.addInterceptor(this.userTokenInterceptor).addPathPatterns("/**");
//token优先级高于redis,因为redis响应才需要
//注册自定义拦截器,添加拦截路径和排除拦截路径
registry.addInterceptor(userThreadLocalInterceptor).addPathPatterns("/**").excludePathPatterns("/doc.html").excludePathPatterns("/webjars/**");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//配置拦截器访问静态资源
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/favicon.ico").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
如此这般我们就可以在请求中直接使用UerThreadLocal.get()获取User对象了;