ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ruoyi-gateway/src/main/resources/bootstrap.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
ruoyi-common/ruoyi-common-core/src/main/java/com/ruoyi/common/core/constant/TokenConstants.java
@@ -21,5 +21,15 @@ * 令牌秘钥 */ public final static String SECRET = "abcdefghijklmnopqrstuvwxyz"; /** * 参数签名 */ public static final String SING = "sing"; /** * 参数随机字符串 */ public static final String NONCE_STR = "nonce_str"; } ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/AuthFilter.java
@@ -3,6 +3,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; @@ -27,8 +28,7 @@ * @author ruoyi */ @Component public class AuthFilter implements GlobalFilter, Ordered { public class AuthFilter implements GlobalFilter, Ordered { private static final Logger log = LoggerFactory.getLogger(AuthFilter.class); // 排除过滤的 uri 地址,nacos自行添加 @@ -37,56 +37,59 @@ @Autowired private RedisService redisService; @Value("${security.sign}") private boolean parameter_signature; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); ServerHttpRequest.Builder mutate = request.mutate(); String url = request.getURI().getPath(); // 跳过不需要验证的路径 if (StringUtils.matches(url, ignoreWhite.getWhites())) { if (StringUtils.matches(url, ignoreWhite.getWhites())) { return chain.filter(exchange); } String token = getToken(request); if (StringUtils.isEmpty(token)) { return unauthorizedResponse(exchange, "令牌不能为空"); // String token = getToken(request); // if (StringUtils.isEmpty(token)) { // return unauthorizedResponse(exchange, "令牌不能为空"); // } // Claims claims = JwtUtils.parseToken(token); // if (claims == null) { // return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); // } // String userkey = JwtUtils.getUserKey(claims); // boolean islogin = redisService.hasKey(getTokenKey(userkey)); // if (!islogin) { // return unauthorizedResponse(exchange, "登录状态已过期"); // } // String userid = JwtUtils.getUserId(claims); // String username = JwtUtils.getUserName(claims); // if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) { // return unauthorizedResponse(exchange, "令牌验证失败"); // } if(parameter_signature){ String sign = request.getHeaders().getFirst(TokenConstants.SING); String nonce_str = request.getHeaders().getFirst(TokenConstants.NONCE_STR); if(StringUtils.isEmpty(sign) || StringUtils.isEmpty(nonce_str)){ log.error("[鉴权签名异常处理]请求路径:{}", exchange.getRequest().getPath()); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "签名校验失败", HttpStatus.BAD_REQUEST); } } Claims claims = JwtUtils.parseToken(token); if (claims == null) { return unauthorizedResponse(exchange, "令牌已过期或验证不正确!"); } String userkey = JwtUtils.getUserKey(claims); boolean islogin = redisService.hasKey(getTokenKey(userkey)); if (!islogin) { return unauthorizedResponse(exchange, "登录状态已过期"); } String userid = JwtUtils.getUserId(claims); String username = JwtUtils.getUserName(claims); if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) { return unauthorizedResponse(exchange, "令牌验证失败"); } // 设置用户信息到请求 addHeader(mutate, SecurityConstants.USER_KEY, userkey); addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); // addHeader(mutate, SecurityConstants.USER_KEY, userkey); // addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); // addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); // 内部请求来源参数清除 removeHeader(mutate, SecurityConstants.FROM_SOURCE); return chain.filter(exchange.mutate().request(mutate.build()).build()); } private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) { if (value == null) { private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) { if (value == null) { return; } String valueStr = value.toString(); @@ -94,13 +97,11 @@ mutate.header(name, valueEncode); } private void removeHeader(ServerHttpRequest.Builder mutate, String name) { private void removeHeader(ServerHttpRequest.Builder mutate, String name) { mutate.headers(httpHeaders -> httpHeaders.remove(name)).build(); } private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) { private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) { log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath()); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED); } @@ -116,8 +117,7 @@ /** * 获取请求token */ private String getToken(ServerHttpRequest request) { private String getToken(ServerHttpRequest request) { String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION); // 如果前端设置了令牌前缀,则裁剪掉前缀 if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/HMACSHA1.java
New file @@ -0,0 +1,37 @@ package com.ruoyi.gateway.filter; import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; public class HMACSHA1 { private static final String MAC_NAME = "HmacSHA1"; private static final String ENCODING = "UTF-8"; /** * 使用 HMAC-SHA1 签名方法对对encryptText进行签名 * * @param encryptText * 被签名的字符串 * @param encryptKey * 密钥 * @return * @throws Exception */ public static byte[] HmacSHA1Encrypt(String encryptText, String encryptKey) throws Exception { byte[] data = encryptKey.getBytes(ENCODING); // 根据给定的字节数组构造一个密钥,第二参数指定一个密钥算法的名称 Mac mac = Mac.getInstance(MAC_NAME); SecretKey secretKey = new SecretKeySpec(data, MAC_NAME); // 生成一个指定 Mac 算法 的 Mac 对象 // 用给定密钥初始化 Mac 对象 mac.init(secretKey); byte[] text = encryptText.getBytes(ENCODING); // 完成 Mac 操作 return mac.doFinal(text); } } ruoyi-gateway/src/main/java/com/ruoyi/gateway/filter/XssFilter.java
@@ -1,7 +1,19 @@ package com.ruoyi.gateway.filter; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ruoyi.common.core.constant.HttpStatus; import com.ruoyi.common.core.constant.TokenConstants; import com.ruoyi.common.core.utils.ServletUtils; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; @@ -34,9 +46,13 @@ @ConditionalOnProperty(value = "security.xss.enabled", havingValue = "true") public class XssFilter implements GlobalFilter, Ordered { private static final Logger log = LoggerFactory.getLogger(XssFilter.class); // 跨站脚本的 xss 配置,nacos自行添加 @Autowired private XssProperties xss; @Value("${security.sign}") private boolean parameter_signature; @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) @@ -65,6 +81,10 @@ return chain.filter(exchange); } ServerHttpRequestDecorator httpRequestDecorator = requestDecorator(exchange); if(parameter_signature && !authSign(httpRequestDecorator)){ log.error("[鉴权签名异常处理]请求路径:{}", exchange.getRequest().getPath()); return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "签名校验失败", HttpStatus.BAD_REQUEST); } return chain.filter(exchange.mutate().request(httpRequestDecorator).build()); } @@ -120,7 +140,100 @@ String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE); } /** * 签名校验 * @param httpRequestDecorator * @return */ private boolean authSign(ServerHttpRequestDecorator httpRequestDecorator) { HttpHeaders headers = httpRequestDecorator.getHeaders(); AtomicReference<JSONObject> jsonObject = new AtomicReference<>(new JSONObject()); httpRequestDecorator.getBody().buffer().map(dataBuffers -> { DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); DataBuffer join = dataBufferFactory.join(dataBuffers); byte[] content = new byte[join.readableByteCount()]; join.read(content); DataBufferUtils.release(join); String bodyStr = new String(content, StandardCharsets.UTF_8); jsonObject.set(JSON.parseObject(bodyStr)); // 防xss攻击过滤 bodyStr = EscapeUtil.clean(bodyStr); // 转成字节 byte[] bytes = bodyStr.getBytes(); NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT); DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length); buffer.write(bytes); return buffer; }); JSONObject params = jsonObject.get(); String sign = headers.getFirst(TokenConstants.SING); if(StringUtils.isEmpty(sign)){ return false; } String nonce_str = headers.getFirst(TokenConstants.NONCE_STR); if(StringUtils.isEmpty(nonce_str)){ return false; } String signUrlEncode = localSignUrl(params, nonce_str); signUrlEncode = signUrlEncode.replaceAll("& #40;", "\\(") .replaceAll("& #41;", "\\)") .replaceAll("\\+", " "); if(sign.equals(signUrlEncode)){ return true; } return false; } /** * 组装签名路径 * @param params * @return */ public static String localSignUrl(JSONObject params, String key) { List<String> keySet = new ArrayList<>(params.keySet()); // 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序) Collections.sort(keySet, new Comparator<String>() { @Override public int compare(String o1, String o2) { return o1.compareTo(o2); } }); // 构造签名键值对的格式 StringBuilder sb = new StringBuilder(); for (String k : keySet) { String v = params.getString(k); if(StringUtils.isNotEmpty(v)){ sb.append(k + "=" + v + "&"); } } String signUrl = sb.substring(0, sb.length() - 1); return signUrlEncode(signUrl, key); } /** * 签名字符串加密 * @param signUrl * @param encryptKey * @return */ public static String signUrlEncode(String signUrl, String encryptKey) { byte[] signByte = new byte[0]; try { signByte = HMACSHA1.HmacSHA1Encrypt(signUrl, encryptKey); } catch (Exception e) { throw new RuntimeException(e); } String localSign = Base64.encodeBase64String(signByte); return localSign; } @Override public int getOrder() { ruoyi-gateway/src/main/resources/bootstrap.yml
@@ -41,12 +41,12 @@ eager: true transport: # 控制台地址 dashboard: 192.168.110.34:8718 dashboard: 192.168.110.169:8718 # nacos配置持久化 datasource: ds1: nacos: server-addr: 127.0.0.1:8848 server-addr: 192.168.110.169:8848 dataId: sentinel-ruoyi-gateway groupId: DEFAULT_GROUP data-type: json