wanglizhong
2025-05-01 d1f994c3a56b66cfe453b5dfaaff81d90fac6590
fix:增加 @anonymouse注解
2个文件已添加
12个文件已修改
323 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysClientAppController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 54 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/AnonymousInterceptor.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysClientApp.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysClientAppMapper.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysClientAppService.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysClientAppServiceImpl.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/SysClientAppMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-ui/src/views/system/clientApp/index.vue 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/sql_client_app_menu.sql 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/SysClientAppController.java
@@ -20,6 +20,8 @@
import com.ruoyi.system.service.ISysClientAppService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.utils.SecurityUtils;
/**
 * 客户应用配置Controller
@@ -95,5 +97,39 @@
    public AjaxResult remove(@PathVariable Long[] appIds) {
        return toAjax(sysClientAppService.deleteSysClientAppByAppIds(appIds));
    }
    @Anonymous(needSign=true)
    @GetMapping("/testSign")
    public AjaxResult testSign(){
        return AjaxResult.success("成功");
    }
    /**
     * 生成签名
     */
    @Anonymous
    @GetMapping("/generateSign/{appId}")
    public AjaxResult generateSign(@PathVariable("appId") String appId)
    {
        // 获取当前系统时间戳
        long timestamp = System.currentTimeMillis();
        // 查询应用信息获取securityKey
        SysClientApp clientApp = sysClientAppService.selectSysClientAppByAppKey(appId);
        if (clientApp == null)
        {
            return AjaxResult.error("应用不存在");
        }
        // 生成签名
        String signStr = appId + timestamp + clientApp.getSecurityKey();
        String sign = SecurityUtils.md5(signStr);
        AjaxResult ajax = AjaxResult.success();
        ajax.put("appId", appId);
        ajax.put("timestamp", String.valueOf(timestamp));
        ajax.put("sign", sign);
        //ajax.put("signStr", signStr);  // 用于调试,显示拼接的字符串
        return ajax;
    }
 
ruoyi-admin/src/main/resources/application.yml
@@ -74,7 +74,7 @@
    # 地址
    host: localhost
    # 端口,默认为6379
    port: 16379
    port: 6379
    # 数据库索引
    database: 0
    # 密码
ruoyi-common/src/main/java/com/ruoyi/common/annotation/Anonymous.java
@@ -16,4 +16,9 @@
@Documented
public @interface Anonymous
{
    /**
     * 是否需要签名验证及时间戳验证
     */
    boolean needSign() default false;
}
ruoyi-common/src/main/java/com/ruoyi/common/utils/SecurityUtils.java
@@ -1,5 +1,7 @@
package com.ruoyi.common.utils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@@ -113,7 +115,29 @@
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
    /**
     * MD5加密
     *
     * @param str 需要加密的字符串
     * @return 加密后的字符串
     */
    public static String md5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] bytes = md.digest(str.getBytes());
            StringBuilder result = new StringBuilder();
            for (byte b : bytes) {
                String temp = Integer.toHexString(b & 0xff);
                if (temp.length() == 1) {
                    temp = "0" + temp;
                }
                result.append(temp);
            }
            return result.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5加密失败", e);
        }
    }
    /**
     * 是否为管理员
     * 
ruoyi-framework/src/main/java/com/ruoyi/framework/config/ResourcesConfig.java
@@ -14,6 +14,7 @@
import com.ruoyi.common.config.RuoYiConfig;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.framework.interceptor.RepeatSubmitInterceptor;
import com.ruoyi.framework.interceptor.AnonymousInterceptor;
/**
 * 通用配置
@@ -25,6 +26,9 @@
{
    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;
    @Autowired
    private AnonymousInterceptor anonymousInterceptor;
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry)
@@ -40,12 +44,16 @@
    }
    /**
     * 自定义拦截规则
     * 添加拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        // 重复提交拦截器
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
        // 匿名访问拦截器
        registry.addInterceptor(anonymousInterceptor).addPathPatterns("/**");
    }
    /**
@@ -55,18 +63,12 @@
    public CorsFilter corsFilter()
    {
        CorsConfiguration config = new CorsConfiguration();
        // 设置访问源地址
        config.addAllowedOriginPattern("*");
        // 设置访问源请求头
        config.addAllowedHeader("*");
        // 设置访问源请求方法
        config.addAllowedMethod("*");
        // 有效期 1800秒
        config.setMaxAge(1800L);
        // 添加映射路径,拦截一切请求
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        // 返回新的CorsFilter
        return new CorsFilter(source);
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -20,6 +20,14 @@
import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter;
import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl;
import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
 * spring security配置
@@ -66,6 +74,26 @@
    @Autowired
    private PermitAllUrlProperties permitAllUrl;
    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;
    /**
     * 获取所有标注了@Anonymous的URL
     */
    private Set<String> getAnonymousUrls() {
        Set<String> urls = new HashSet<>();
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = requestMappingHandlerMapping.getHandlerMethods();
        for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
            HandlerMethod handlerMethod = entry.getValue();
            Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class);
            if (anonymous != null) {
                Set<String> patterns = entry.getKey().getPatternValues();
                urls.addAll(patterns);
            }
        }
        return urls;
    }
    /**
     * 身份验证实现
     */
@@ -96,6 +124,9 @@
    @Bean
    protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception
    {
        // 获取所有标注了@Anonymous的URL
        Set<String> anonymousUrls = getAnonymousUrls();
        return httpSecurity
            // CSRF禁用,因为不使用session
            .csrf(csrf -> csrf.disable())
@@ -107,17 +138,18 @@
            .exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
            // 基于token,所以不需要session
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            // 注解标记允许匿名访问的url
            .authorizeHttpRequests((requests) -> {
                permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
                // 对于登录login 注册register 验证码captchaImage 允许匿名访问
                requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
                    // 静态资源,可匿名访问
                    .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                    .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
            })
            // 过滤请求
            .authorizeRequests()
            // 对于登录login 注册register 验证码captchaImage 允许匿名访问
            .antMatchers("/login", "/register", "/captchaImage").permitAll()
            // 添加标注了@Anonymous的URL到匿名访问列表
            .antMatchers(anonymousUrls.toArray(new String[0])).permitAll()
            // 静态资源,可匿名访问
            .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
            .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated()
            .and()
            // 添加Logout filter
            .logout(logout -> logout.logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler))
            // 添加JWT filter
ruoyi-framework/src/main/java/com/ruoyi/framework/interceptor/AnonymousInterceptor.java
New file
@@ -0,0 +1,68 @@
package com.ruoyi.framework.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.util.StringUtils;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.service.ISysClientAppService;
/**
 * 匿名访问拦截器
 */
@Component
public class AnonymousInterceptor implements HandlerInterceptor {
    @Autowired
    private ISysClientAppService clientAppService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果不是映射到方法,直接通过
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        // 获取方法上的注解
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Anonymous anonymous = handlerMethod.getMethodAnnotation(Anonymous.class);
        // 如果方法上没有注解,则获取类上的注解
        if (anonymous == null) {
            anonymous = handlerMethod.getBeanType().getAnnotation(Anonymous.class);
        }
        // 如果没有注解,直接通过
        if (anonymous == null) {
            return true;
        }
        // 获取请求参数
        String appId = request.getParameter("appId");
        String sign = request.getParameter("sign");
        String timestamp = request.getParameter("timestamp");
        if(anonymous.needSign()){
            if(appId == null || sign == null || timestamp == null){
                throw new ServiceException("缺少必要参数");
            }
        }
        // 验证必要参数
        if (StringUtils.hasText(appId) && StringUtils.hasText(sign) && StringUtils.hasText(timestamp)) {
            // 验证签名
            if (clientAppService.validateSign(appId, sign, timestamp)) {
                return true;
            }
            throw new ServiceException("签名验证失败");
        }
        // 如果没有验证参数,也允许通过(适用于不需要验证的匿名接口)
        return true;
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/SysClientApp.java
@@ -4,6 +4,7 @@
import com.ruoyi.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.util.Date;
@@ -30,10 +31,12 @@
    /** 有效期开始时间 */
    @Excel(name = "有效期开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date validStartTime;
    /** 有效期结束时间 */
    @Excel(name = "有效期结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date validEndTime;
    /** 状态(0正常 1停用) */
ruoyi-system/src/main/java/com/ruoyi/system/mapper/SysClientAppMapper.java
@@ -54,4 +54,12 @@
     * @return 结果
     */
    public int deleteSysClientAppByAppIds(Long[] appIds);
    /**
     * 根据应用标识查询应用信息
     *
     * @param appKey 应用标识
     * @return 应用信息
     */
    public SysClientApp selectSysClientAppByAppKey(String appKey);
ruoyi-system/src/main/java/com/ruoyi/system/service/ISysClientAppService.java
@@ -54,4 +54,22 @@
     * @return 结果
     */
    public int deleteSysClientAppByAppId(Long appId);
    /**
     * 验证签名
     *
     * @param appId 应用ID
     * @param sign 签名
     * @param timestamp 时间戳
     * @return 验证结果
     */
    public boolean validateSign(String appId, String sign, String timestamp);
    /**
     * 通过应用标识查询客户应用配置
     *
     * @param appKey 应用标识
     * @return 客户应用配置
     */
    public SysClientApp selectSysClientAppByAppKey(String appKey);
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/SysClientAppServiceImpl.java
@@ -8,6 +8,8 @@
import com.ruoyi.system.service.ISysClientAppService;
import com.ruoyi.common.utils.SecurityUtils;
import static com.ruoyi.common.utils.SecurityUtils.md5;
/**
 * 客户应用配置 服务层实现
 */
@@ -25,6 +27,18 @@
    @Override
    public SysClientApp selectSysClientAppByAppId(Long appId) {
        return sysClientAppMapper.selectSysClientAppByAppId(appId);
    }
    /**
     * 通过应用标识查询客户应用配置
     *
     * @param appKey 应用标识
     * @return 客户应用配置
     */
    @Override
    public SysClientApp selectSysClientAppByAppKey(String appKey)
    {
        return sysClientAppMapper.selectSysClientAppByAppKey(appKey);
    }
    /**
@@ -118,4 +132,42 @@
    public int deleteSysClientAppByAppId(Long appId) {
        return sysClientAppMapper.deleteSysClientAppByAppId(appId);
    }
    @Override
    public boolean validateSign(String appId, String sign, String timestamp) {
        // 根据appId获取应用信息
        SysClientApp clientApp = sysClientAppMapper.selectSysClientAppByAppKey(appId);
        if (clientApp == null) {
            return false;
        }
        // 验证应用是否有效
        if (!"0".equals(clientApp.getStatus())) {
            return false;
        }
        // 验证有效期
        if (clientApp.getValidStartTime() != null && clientApp.getValidEndTime() != null) {
            long currentTime = System.currentTimeMillis();
            if (currentTime < clientApp.getValidStartTime().getTime()
                || currentTime > clientApp.getValidEndTime().getTime()) {
                return false;
            }
        }
        // 生成签名
        String serverSign = generateSign(appId, clientApp.getSecurityKey(), timestamp);
        // 比较签名
        return sign.equals(serverSign);
    }
    /**
     * 生成签名
     * 签名规则:MD5(appId + timestamp + securityKey)
     */
    private String generateSign(String appId, String securityKey, String timestamp) {
        String signStr = appId + timestamp + securityKey;
        return md5(signStr);
    }
ruoyi-system/src/main/resources/mapper/system/SysClientAppMapper.xml
@@ -40,6 +40,11 @@
        where app_id = #{appId}
    </select>
        
    <select id="selectSysClientAppByAppKey" parameterType="String" resultMap="SysClientAppResult">
        <include refid="selectSysClientAppVo"/>
        where app_key = #{appKey} and del_flag = '0'
    </select>
    <insert id="insertSysClientApp" parameterType="SysClientApp" useGeneratedKeys="true" keyProperty="appId">
        insert into sys_client_app
        <trim prefix="(" suffix=")" suffixOverrides=",">
ruoyi-ui/src/views/system/clientApp/index.vue
@@ -309,6 +309,14 @@
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          // 处理日期格式
          if (this.form.validStartTime) {
            this.form.validStartTime = this.parseTime(this.form.validStartTime, '{y}-{m}-{d} {h}:{i}:{s}');
          }
          if (this.form.validEndTime) {
            this.form.validEndTime = this.parseTime(this.form.validEndTime, '{y}-{m}-{d} {h}:{i}:{s}');
          }
          if (this.form.appId != null) {
            updateClientApp(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
sql/sql_client_app_menu.sql
New file
@@ -0,0 +1,22 @@
_-- 菜单 SQL
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES('客户应用配置', '1', '1', 'clientApp', 'system/clientApp/index', 1, 0, 'C', '0', '0', 'system:clientApp:list', 'app', 'admin', SYSDATE(), '', NULL, '客户应用配置菜单');
-- 按钮父菜单ID
SELECT @parentId := LAST_INSERT_ID();
-- 按钮 SQL
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES('客户应用配置查询', @parentId, '1',  '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:query',        '#', 'admin', SYSDATE(), '', NULL, '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES('客户应用配置新增', @parentId, '2',  '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:add',          '#', 'admin', SYSDATE(), '', NULL, '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES('客户应用配置修改', @parentId, '3',  '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:edit',         '#', 'admin', SYSDATE(), '', NULL, '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES('客户应用配置删除', @parentId, '4',  '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:remove',       '#', 'admin', SYSDATE(), '', NULL, '');
INSERT INTO sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, STATUS, perms, icon, create_by, create_time, update_by, update_time, remark)
VALUES('客户应用配置导出', @parentId, '5',  '#', '', 1, 0, 'F', '0', '0', 'system:clientApp:export',       '#', 'admin', SYSDATE(), '', NULL, '');