package cn.iocoder.yudao.framework.common.util.http;
|
|
import cn.hutool.core.codec.Base64;
|
import cn.hutool.core.net.url.UrlBuilder;
|
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.http.HttpRequest;
|
import cn.hutool.http.HttpResponse;
|
import jakarta.servlet.http.HttpServletRequest;
|
import org.springframework.util.StringUtils;
|
import org.springframework.web.util.UriComponents;
|
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriUtils;
|
|
import java.net.URI;
|
import java.net.URLDecoder;
|
import java.net.URLEncoder;
|
import java.nio.charset.Charset;
|
import java.nio.charset.StandardCharsets;
|
import java.util.Map;
|
|
/**
|
* HTTP 工具类
|
*
|
* @author 芋道源码
|
*/
|
public class HttpUtils {
|
|
/**
|
* 编码 URL 参数
|
*
|
* @param value 参数
|
* @return 编码后的参数
|
*/
|
public static String encodeUtf8(String value) {
|
return URLEncoder.encode(value, StandardCharsets.UTF_8);
|
}
|
|
/**
|
* 解码 URL 参数(query parameter)
|
* 注意:此方法会将 + 解码为空格,适用于 query parameter,不适用于 URL path
|
*
|
* @see #decodeUrlPath(String)
|
* @param value 参数
|
* @return 解码后的参数
|
*/
|
public static String decodeUtf8(String value) {
|
return URLDecoder.decode(value, StandardCharsets.UTF_8);
|
}
|
|
/**
|
* 解码 URL 路径
|
* 与 {@link #decodeUtf8(String)} 不同,此方法不会将 + 解码为空格,保持 + 为字面字符
|
* 适用于 URL path 部分的解码
|
*
|
* @param path URL 路径
|
* @return 解码后的路径
|
*/
|
public static String decodeUrlPath(String path) {
|
if (StrUtil.isEmpty(path)) {
|
return path;
|
}
|
// 先将 + 替换为 %2B,避免被 URLDecoder 解码为空格
|
String encoded = path.replace("+", "%2B");
|
return URLDecoder.decode(encoded, StandardCharsets.UTF_8);
|
}
|
|
/**
|
* 编码 URL 路径,按路径段编码,保留 / 分隔符
|
*
|
* @param path URL 路径,例如 20250602/xxx.pdf
|
* @return 编码后的路径
|
*/
|
public static String encodeUrlPath(String path) {
|
if (StrUtil.isEmpty(path)) {
|
return path;
|
}
|
String[] segments = path.split(StrUtil.SLASH, -1);
|
StringBuilder result = new StringBuilder(path.length());
|
for (int i = 0; i < segments.length; i++) {
|
if (i > 0) {
|
result.append(StrUtil.SLASH);
|
}
|
result.append(encodeUrlPathSegment(segments[i]));
|
}
|
return result.toString();
|
}
|
|
/**
|
* 编码 URL 路径段
|
*
|
* @param segment URL 路径段
|
* @return 编码后的路径段
|
*/
|
public static String encodeUrlPathSegment(String segment) {
|
return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8);
|
}
|
|
public static String removeUrlPathQueryAndFragment(String path) {
|
if (StrUtil.isEmpty(path)) {
|
return path;
|
}
|
int endIndex = path.length();
|
int queryIndex = path.indexOf('?');
|
if (queryIndex >= 0) {
|
endIndex = queryIndex;
|
}
|
int fragmentIndex = path.indexOf('#');
|
if (fragmentIndex >= 0 && fragmentIndex < endIndex) {
|
endIndex = fragmentIndex;
|
}
|
return path.substring(0, endIndex);
|
}
|
|
public static String replaceUrlQuery(String url, String key, String value) {
|
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
// 先移除;再添加
|
builder.getQuery().remove(key);
|
builder.addQuery(key, value);
|
return builder.build();
|
}
|
|
public static String removeUrlQuery(String url) {
|
if (!StrUtil.contains(url, '?')) {
|
return url;
|
}
|
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
// 移除 query、fragment
|
builder.setQuery(null);
|
builder.setFragment(null);
|
return builder.build();
|
}
|
|
/**
|
* 拼接 URL
|
*
|
* copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法
|
*
|
* @param base 基础 URL
|
* @param query 查询参数
|
* @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射
|
* @param fragment URL 的 fragment,即拼接到 # 中
|
* @return 拼接后的 URL
|
*/
|
public static String append(String base, Map<String, ?> query, Map<String, String> keys, boolean fragment) {
|
UriComponentsBuilder template = UriComponentsBuilder.newInstance();
|
UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base);
|
URI redirectUri;
|
try {
|
// assume it's encoded to start with (if it came in over the wire)
|
redirectUri = builder.build(true).toUri();
|
} catch (Exception e) {
|
// ... but allow client registrations to contain hard-coded non-encoded values
|
redirectUri = builder.build().toUri();
|
builder = UriComponentsBuilder.fromUri(redirectUri);
|
}
|
template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost())
|
.userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath());
|
|
if (fragment) {
|
StringBuilder values = new StringBuilder();
|
if (redirectUri.getFragment() != null) {
|
String append = redirectUri.getFragment();
|
values.append(append);
|
}
|
for (String key : query.keySet()) {
|
if (values.length() > 0) {
|
values.append("&");
|
}
|
String name = key;
|
if (keys != null && keys.containsKey(key)) {
|
name = keys.get(key);
|
}
|
values.append(name).append("={").append(key).append("}");
|
}
|
if (values.length() > 0) {
|
template.fragment(values.toString());
|
}
|
UriComponents encoded = template.build().expand(query).encode();
|
builder.fragment(encoded.getFragment());
|
} else {
|
for (String key : query.keySet()) {
|
String name = key;
|
if (keys != null && keys.containsKey(key)) {
|
name = keys.get(key);
|
}
|
template.queryParam(name, "{" + key + "}");
|
}
|
template.fragment(redirectUri.getFragment());
|
UriComponents encoded = template.build().expand(query).encode();
|
builder.query(encoded.getQuery());
|
}
|
return builder.build().toUriString();
|
}
|
|
public static String[] obtainBasicAuthorization(HttpServletRequest request) {
|
String clientId;
|
String clientSecret;
|
// 先从 Header 中获取
|
String authorization = request.getHeader("Authorization");
|
authorization = StrUtil.subAfter(authorization, "Basic ", true);
|
if (StringUtils.hasText(authorization)) {
|
authorization = Base64.decodeStr(authorization);
|
clientId = StrUtil.subBefore(authorization, ":", false);
|
clientSecret = StrUtil.subAfter(authorization, ":", false);
|
// 再从 Param 中获取
|
} else {
|
clientId = request.getParameter("client_id");
|
clientSecret = request.getParameter("client_secret");
|
}
|
|
// 如果两者非空,则返回
|
if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) {
|
return new String[]{clientId, clientSecret};
|
}
|
return null;
|
}
|
|
/**
|
* HTTP post 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
|
*
|
* 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
|
*
|
* @param url URL
|
* @param headers 请求头
|
* @param requestBody 请求体
|
* @return 请求结果
|
*/
|
public static String post(String url, Map<String, String> headers, String requestBody) {
|
try (HttpResponse response = HttpRequest.post(url)
|
.addHeaders(headers)
|
.body(requestBody)
|
.execute()) {
|
return response.body();
|
}
|
}
|
|
/**
|
* HTTP get 请求,基于 {@link cn.hutool.http.HttpUtil} 实现
|
*
|
* 为什么要封装该方法,因为 HttpUtil 默认封装的方法,没有允许传递 headers 参数
|
*
|
* @param url URL
|
* @param headers 请求头
|
* @return 请求结果
|
*/
|
public static String get(String url, Map<String, String> headers) {
|
try (HttpResponse response = HttpRequest.get(url)
|
.addHeaders(headers)
|
.execute()) {
|
return response.body();
|
}
|
}
|
|
/**
|
* WebSocket URL 切换成 HTTP URL:ws:// → http://;wss:// → https://;其它格式原样保留
|
*
|
* @param url 原始 URL
|
* @return 切换协议后的 URL
|
*/
|
public static String wsUrlToHttp(String url) {
|
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;
|
}
|
|
}
|