如何在Spring Resource抽象中使用基础认证访问HTTPS资源
首先得明确:Spring默认的UrlResource(也就是你用@Value注入HTTPS URL时得到的资源类型)依赖JDK原生的URLConnection,而JDK的URLConnection不会自动解析URL中的用户名密码并添加Basic Auth请求头——这就是为什么wget能正常工作,但Spring注入的Resource请求返回401的核心原因。
下面给你几个可行的解决方案:
方案1:手动在Resource使用时添加认证信息
如果你的场景比较简单,只需要在构造方法里处理这个Resource,可以强制转换为UrlResource,然后手动设置Basic Auth头:
public class MyClass { public MyClass(@Value("https://myserver.com/myfile") Resource resource) throws IOException { if (resource instanceof UrlResource urlResource) { URLConnection connection = urlResource.getURL().openConnection(); String auth = "username:password"; String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); connection.setRequestProperty("Authorization", "Basic " + encodedAuth); // 现在可以通过connection获取输入流了 try (InputStream is = connection.getInputStream()) { // 处理文件内容 } } } }
为了避免硬编码凭证,你可以把用户名密码也通过@Value注入进来:
public class MyClass { public MyClass( @Value("https://myserver.com/myfile") Resource resource, @Value("${my.server.username}") String username, @Value("${my.server.password}") String password ) throws IOException { if (resource instanceof UrlResource urlResource) { URLConnection connection = urlResource.getURL().openConnection(); String auth = username + ":" + password; String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); connection.setRequestProperty("Authorization", "Basic " + encodedAuth); try (InputStream is = connection.getInputStream()) { // 处理逻辑 } } } }
方案2:自定义ProtocolResolver全局处理HTTPS认证
如果你的项目中有多个地方需要用Spring Resource访问带Basic Auth的HTTPS资源,可以自定义一个ProtocolResolver,让Spring在加载HTTPS资源时自动添加认证信息:
首先创建自定义的ProtocolResolver:
@Component public class HttpsAuthProtocolResolver implements ProtocolResolver { private final String username; private final String password; public HttpsAuthProtocolResolver( @Value("${my.server.username}") String username, @Value("${my.server.password}") String password ) { this.username = username; this.password = password; } @Override public Resource resolve(String location, ResourceLoader resourceLoader) throws IOException { if (location.startsWith("https://")) { URL url = new URL(location); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); String auth = username + ":" + password; String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); connection.setRequestProperty("Authorization", "Basic " + encodedAuth); // 返回自定义的Resource,包装这个带认证的连接 return new InputStreamResource(connection.getInputStream()) { @Override public String getDescription() { return "Authenticated HTTPS resource: " + location; } }; } // 非HTTPS资源交给默认处理 return null; } }
然后在Spring配置类中注册这个ProtocolResolver:
@Configuration public class ResourceConfig implements ApplicationContextInitializer<GenericApplicationContext> { private final HttpsAuthProtocolResolver httpsAuthProtocolResolver; public ResourceConfig(HttpsAuthProtocolResolver httpsAuthProtocolResolver) { this.httpsAuthProtocolResolver = httpsAuthProtocolResolver; } @Override public void initialize(GenericApplicationContext context) { context.getDefaultResourceLoader().addProtocolResolver(httpsAuthProtocolResolver); } }
这样之后,你原来的@Value注入代码就可以正常工作了,不需要修改:
public class MyClass { public MyClass(@Value("https://myserver.com/myfile") Resource resource) { // 直接使用resource即可,认证已经自动处理 } }
方案3:使用RestTemplate封装Resource
如果你的项目已经在用RestTemplate,也可以用它来获取资源并封装成Resource:
首先配置带Basic Auth的RestTemplate:
@Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate( @Value("${my.server.username}") String username, @Value("${my.server.password}") String password ) { RestTemplate restTemplate = new RestTemplate(); restTemplate.getInterceptors().add((request, body, execution) -> { String auth = username + ":" + password; String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); request.getHeaders().add("Authorization", "Basic " + encodedAuth); return execution.execute(request, body); }); return restTemplate; } }
然后在你的类中注入RestTemplate,直接请求资源:
public class MyClass { private final Resource resource; public MyClass(RestTemplate restTemplate, @Value("${my.server.file.url}") String fileUrl) throws IOException { ResponseEntity<Resource> response = restTemplate.getForEntity(fileUrl, Resource.class); this.resource = response.getBody(); } }
这种方式更灵活,还能处理更多HTTP相关的场景(比如超时、重试等)。
总结一下:Spring本身不支持直接在URL中嵌入用户名密码来自动处理Basic Auth,所以你需要通过上述几种方式手动添加认证信息。如果是单个场景用方案1即可,全局场景推荐方案2,已经用RestTemplate的话方案3最顺手。
内容的提问来源于stack exchange,提问作者Tobias Kremer

