一. 背景
移除/profile下静态资源访问权限后,富文本等组件中的图片加载失败!!!
使用ruoyi-vue3.8.9过程中发现上传的在ruoyi.profile下的文件未登录直接使用链接就可以访问下载,感觉这样不太安全,所以想对其进行鉴权限制,修改为只有用户登陆的时候才可以访问
二. 步骤
1. 创建配置类
package com.ruoyi.framework.config;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;import java.util.List;/*** @author ning* @create 2025-08-25-10:17*/
@Configuration
public class ReferConfig {/*** 是否开启防盗链拦截*/@Value("${refer.enabled:false}")private boolean enabled;/*** 允许的Referer请求*/@Value("#{'${refer.allowed-origins}'.split(',')}")private List<String> allowedOrigins;/*** 策略*/// TODO-ning at 2025/8/25 for 策略方式暂时不使用,后续考虑增加策略@Value("${refer.policy:same-origin}")private String policy;/*** 是否开启token验证*/@Value("${refer.token-enabled:false}")private boolean tokenEnabled;public boolean isEnabled() {return enabled;}public void setEnabled(boolean enabled) {this.enabled = enabled;}public List<String> getAllowedOrigins() {return allowedOrigins;}public void setAllowedOrigins(List<String> allowedOrigins) {this.allowedOrigins = allowedOrigins;}public String getPolicy() {return policy;}public void setPolicy(String policy) {this.policy = policy;}public boolean isTokenEnabled() {return tokenEnabled;}public void setTokenEnabled(boolean tokenEnabled) {this.tokenEnabled = tokenEnabled;}
}
2. 新增配置
ruoyi-admin/src/main/resources/application.yml
# 防盗链配置
refer:# 防盗链开关enabled: true# 白名单(多个用逗号分隔),新增前台路由即可allowed-origins: http://localhost:8080,http://127.0.0.1:8080# 校验token,开启后使用上面的token配置进行校验token-enabled: true
3. 新增拦截器
1)代码
路径:ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor
package com.ruoyi.framework.interceptor;import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.framework.config.ReferConfig;
import com.ruoyi.framework.web.service.TokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;/*** @author ning* @create 2025-08-25-10:13*/
@Component
public class StaticResourceInterceptor implements HandlerInterceptor {// Cookie中保存的Token前缀private static final String COOKIE_TOKEN_PREFIX = "Admin-Token";// Redis中保存的登录Token前缀private static final String LOGIN_TOKEN_KEY = "login_tokens:";@Autowiredprivate ReferConfig referConfig;@Autowiredprivate TokenService tokenService;@Autowiredprivate RedisCache redisCache;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 如果未开启防盗链功能,直接放行if (!referConfig.isEnabled()) {return true;}// 2. 检查Referer是否合法boolean isRefererValid = checkReferer(request);// 3. 检查Token是否有效(若开启Token校验)if (isRefererValid && referConfig.isTokenEnabled()) {if (checkTokenValid(request)) {return true;}} else {if (isRefererValid) {return true;}}// 4. 验证失败,返回403错误response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getWriter().write("Access denied: Invalid referer or token");return false;}/*** 检查Referer是否在允许的列表中*/private boolean checkReferer(HttpServletRequest request) {String referer = request.getHeader("Referer");List<String> allowedOrigins = referConfig.getAllowedOrigins();// 如果允许的来源列表为空,放行if (allowedOrigins == null || allowedOrigins.isEmpty()) {return true;}// 处理空Referer情况if (referer == null || referer.isEmpty()) {return allowedOrigins.contains("*") || allowedOrigins.contains("");}// 检查Referer是否匹配允许的来源return allowedOrigins.stream().anyMatch(origin ->"*".equals(origin) || referer.startsWith(origin) ||getDomainFromUrl(referer).equals(getDomainFromUrl(origin)));}/*** 检查Token是否有效(从Cookie获取并验证)*/private boolean checkTokenValid(HttpServletRequest request) {try {// 从Cookie获取Token(优化:找到后立即退出循环)String token = getTokenFromCookie(request.getCookies());if (token == null) {return false;}// 解密Token(捕获JWT解析异常,避免500错误)Claims claims = tokenService.parseToken(token);if (claims == null) {return false;}// 验证Redis中的用户登录状态String userTokenId = claims.get(Constants.LOGIN_USER_KEY, String.class);return userTokenId != null && redisCache.hasKey(LOGIN_TOKEN_KEY + userTokenId);} catch (JwtException e) {// Token解析失败(过期、篡改等)return false;} catch (Exception e) {// 其他异常(如Redis连接失败)return false;}}/*** 从Cookie中提取Admin-Token(优化遍历逻辑)*/private String getTokenFromCookie(Cookie[] cookies) {if (cookies == null) {return null;}for (Cookie cookie : cookies) {if (COOKIE_TOKEN_PREFIX.equals(cookie.getName())) {return cookie.getValue(); // 找到后直接返回,减少循环}}return null;}/*** 从URL中提取域名*/private String getDomainFromUrl(String url) {try {if (!url.startsWith("http")) {url = "http://" + url;}java.net.URL urlObj = new java.net.URL(url);String host = urlObj.getHost();// 去除www.前缀,统一比较if (host.startsWith("www.")) {host = host.substring(4);}return host;} catch (Exception e) {return url;}}
}
2)解释
为啥会从Cookie里获取Token?
分析代码发现,在用户登录后,会将加密后的token存到Cookie中
代码路径:ruoyi-ui/src/store/modules/user.js
ruoyi-ui/src/utils/auth.js
所以后续所有链接都会携带该加密token
token生成代码:
com.ruoyi.framework.web.service.TokenService#createToken(com.ruoyi.common.core.domain.model.LoginUser)
同类里已经有解密方法:
所以我拦截器里面我直接使用的
4. 配置拦截器
方法路径:com.ruoyi.framework.config.ResourcesConfig#addInterceptors
5. 修改/profile/**鉴权
代码路径:org.springframework.security.web.SecurityFilterChain
移除原有的,新增鉴权