zl程序教程

您现在的位置是:首页 >  Python

当前栏目

Ruo-Yi 前后端分离如何不登录直接访问以及一种基于配置文件实现的方式

2023-04-18 15:43:47 时间

Ruo-Yi 前后端分离如何不登录直接访问

问题

​ 我们从若依项目的简介中可以知道,若依前后端分离系统采用了Spring Security作为权限校验框架,那么,如果我们想要不登录就可以访问某些页面应该怎么办?

分析

​ 若依官网的解释:若依官网解释

​ 有关spring security配置的东西若依框架都在SecurityConfig类里面有写。找到configure()方法,一般这个类里面会写。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eQGlma5G-1666317952035)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021092551886.png)]

这里的意思就是 permitAllUrl.getUrls()集合 里面的路径因为后面设置了permitAll()用户可以任意访问。所以我们要想某些路径不登陆就可以访问,其实只需要我们的不登陆像访问的路径在 permitAllUrl.getUrls()集合里面即可。

# 若依原作者的注释
* anyRequest          |   匹配所有请求路径
* access              |   SpringEl表达式结果为true时可以访问
* anonymous           |   匿名可以访问
* denyAll             |   用户不能访问
* fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
* hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
* hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
* hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
* hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
* hasRole             |   如果有参数,参数表示角色,则其角色可以访问
* permitAll           |   用户可以任意访问
* rememberMe          |   允许通过remember-me登录的用户访问
* authenticated       |   用户登录后可访问

SecurityConfig 类里面有如下代码:

/**
 * 允许匿名访问的地址
 */
@Autowired
private PermitAllUrlProperties permitAllUrl;

permitAllUrl 是从这里注入进来的,想要知道怎么不登陆就可以访问,就必须分析类PermitAllUrlProperties

PermitAllUrlProperties 类

这个类有@Configuration注解,代表这个类是已启动就被注册到 spring 容器里面。

package com.ruoyi.framework.config.properties;

import com.ruoyi.common.annotation.Anonymous;
import org.apache.commons.lang3.RegExUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;

/**
 * 设置Anonymous注解允许匿名访问的url
 *      该配置类 项目启动就会被加载
 *      实现了 ApplicationContextAware 类 就可以在本类获取 bean 对象
 *      凡是继承 InitializingBean 的类,在初始化bean的时候都会执行 afterPropertiesSet 方法
 *
 *
 *      注意:1、在Spring初始化bean的时候,如果该bean实现了InitializingBean接口,并且同时在配置文件中指定了init-method,
 *              系统则是先调用afterPropertieSet()方法,然后再调用init-method中指定的方法
 *              Spring为bean提供了两种初始化bean的方式,实现 InitializingBean 接口,
 *              实现afterPropertiesSet方法,或者在配置文件中通过init-method指定,两种方式可以同时使用。
 *           2、实现InitializingBean接口是直接调用afterPropertiesSet方法,
 *              比通过反射调用 init-method 指定的方法效率要高一点,但是init-method方式消除了对spring的依赖。
 *           3、如果调用 afterPropertiesSet 方法时出错,则不调用init-method指定的方法。
 *
 *           BeanPostProcessor,该接口中有两个方法,InitializingBean发挥作用的时机就在这两个方法之间。从语义也很好看出来,
 *              postProcessBeforeInitialization -> initializingBean -> postProcessAfterInitialization
 *              afterPropertiesSet 发生作用的时机是当前类的实例化的时候,而 BeanPostProcessor 则是所有类,这也是为什么 afterPropertiesSet 的函数中没有参数
 *
 *              postProcessBeforeInitialization方法在bean初始化之前执行, postProcessAfterInitialization方法在bean初始化之后执行。
 *
 *    构造函数 -> postProcessBeforeInitialization -> @PostConstruct ->  initializingBean -> init-method  -> postProcessAfterInitialization
 *    由此可见初始化Bean的先后顺序为:
 *          Bean 本身的构造函数
 *          BeanPostProcessor 的 postProcessBeforeInitialization方法
 *          类中添加了注解 @PostConstruct 的方法
 *          InitializingBean 的 afterPropertiesSet 方法
 *          init-method
 *          BeanPostProcessor 的 postProcessAfterInitialization 方法
 *    Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)
 * @author ruoyi
 */
@Configuration
public class PermitAllUrlProperties implements InitializingBean, ApplicationContextAware
{
    private static final Pattern PATTERN = Pattern.compile("\{(.*?)\}");

    private ApplicationContext applicationContext;

    private List<String> urls = new ArrayList<>();

    public String ASTERISK = "*";

    @Override
    public void afterPropertiesSet()
    {
        /**
         * RequestMappingHandlerMapping的作用是在容器启动后将系统中所有控制器方法的请求条件(RequestMappingInfo)和控制器方法(HandlerMethod)的对应关系注册
         * 到RequestMappingHandlerMapping Bean的内存中,待接口请求系统的时候根据请求条件和内存中存储的系统接口信息比对,再执行对应的控制器方法。
         */
        RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
        // HandlerMethod 类用于封装控制器方法信息,包含类信息、方法Method对象、参数、注解等信息,具体的接口请求是可以根据封装的信息调用具体的方法来执行业务逻辑;
        // RequestMappingInfo其实就是将我们熟悉的@RequestMapping注解的信息数据封装到了RequestMappingInfo POJO对象之中,然后和HandlerMethod做映射关系存入缓存之中;
        // 可以参考博客:https://blog.csdn.net/yaomingyang/article/details/107026595
        Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();

        map.keySet().forEach(info -> {
            HandlerMethod handlerMethod = map.get(info);

            // 获取方法上边的注解 替代path variable 为 *
            // 从类或方法中查找某个 Anonymous 注解
            Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
            Optional.ofNullable(method).ifPresent(anonymous -> info.getPatternsCondition().getPatterns()
                    .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));

            // 获取类上边的注解, 替代path variable 为 *
            Anonymous controller = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Anonymous.class);
            Optional.ofNullable(controller).ifPresent(anonymous -> info.getPatternsCondition().getPatterns()
                    .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
        });
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException
    {
        this.applicationContext = context;
    }

    public List<String> getUrls()
    {
        return urls;
    }

    public void setUrls(List<String> urls)
    {
        this.urls = urls;
    }
}

从上面的代码我们知道,只要我们给类上加 @Anonymous注解,就可以将不登陆就可以访问的路径加入urls集合里面。

/**
 * 匿名访问不鉴权注解
 * 
 * @author ruoyi
 */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous
{
}

测试

有了以上的分析,我们接下来测试一波。我们测试缓存监控页面,请求为

http://localhost/dev-api/monitor/cache

不加@Anonymous注解之前:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nOqAddEE-1666317952035)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021094638260.png)]

测试结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wURyqbFA-1666317952035)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021094603553.png)]

加了 @Anonymous注解之后:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HeCxHhwF-1666317952036)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021094742483.png)]

测试结果,访问成功:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GHfwbPa1-1666317952036)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021094804998.png)]

总结

1、在若依的系统里面,如果我们想要实现某一个接口不登陆就可以访问,只需要在方法上添加 @Anonymous注解即可。

2、如果我们想要在我们的项目里面实现不登陆就可以访问,我们可以将若依的PermitAllUrlProperties类和Anonymous类拿过来,然后在SecurityConfigspring security的配置类添加如下代码。

/**
     * 允许匿名访问的地址
     */
@Autowired
private PermitAllUrlProperties permitAllUrl;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
    // 注解标记允许匿名访问的url
    // 自定义@Anonymous注解添加到方法上可以实现跳过权限验证。
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
    permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());

    // ....     
}

拓展:另一种方法基于配置文件

​ 经过上面的分析,我们知道,实现不登陆就可以访问某些接口,若依系统采用的是注解的形式,其实我们还可以将不需要登陆的接口放入配置文件里面。

1、在 SecurityConfig类改为

/**
     * 允许匿名访问的地址
     */
//@Autowired
//private PermitAllUrlProperties permitAllUrl;

@Resource
private IgnoreUrlsConfig ignoreUrlsConfig;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
    // 注解标记允许匿名访问的url
    // 自定义@Anonymous注解添加到方法上可以实现跳过权限验证。
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
    // permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());


    // 白名单
    //不需要保护的资源路径允许访问
    for (String url : ignoreUrlsConfig.getUrls()) {
        registry.antMatchers(url).permitAll();
    }

去掉原有的方式的类的注入,添加IgnoreUrlsConfig

2、添加类 IgnoreUrlsConfig

/**
 * 用于配置白名单资源路径
 */
@Component
@ConfigurationProperties(prefix = "secure.ignore")
public class IgnoreUrlsConfig {
    private List<String> urls = new ArrayList<>();

    public List<String> getUrls() {
        return urls;
    }

    public void setUrls(List<String> urls) {
        this.urls = urls;
    }
}

3、在 application.yml添加白名单

secure:
  ignore:
    urls: #安全路径白名单
      - /monitor/cache

4、测试,还是测试之前的接口

http://localhost/dev-api/monitor/cache

记住,去掉权限的注解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HeZUQ55l-1666317952036)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021100236661.png)]

测试成功:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bx4XtUqK-1666317952037)(Ruo-Yi 前后端分离如何不登录直接访问.assets/image-20221021100248640.png)]

以上的两种方法,我自己还是比较喜欢第二种,想要添加或者删除不需要登陆就可以访问的路径只需要修改配置文件即可,还不会污染业务代码。