掘金 后端 ( ) • 2024-05-07 13:33

highlight: androidstudio theme: channing-cyan

本文主要有以下内容:

  • SpringBoot 整合 spring- security
  • spring-security 在 RuoYi 中的使用

SpringBoot整合Spring-Security

spring-security简介

Spring Security 是一个 Java 框架,用于保护应用程序的安全性。它提供了一套全面的安全解决方案,包括身份验证、授权、防止攻击等功能。Spring Security 基于过滤器链的概念,可以轻松地集成到任何基于Spring的应用程序中。以上内容来自 spring- security 官网。

shiro 相比,spring- securityspring boot 的整合更为简单、方便。

spring boot 整合 spring security

首先创建 springboot 工程,在 pom 文件中引入如下依赖

  <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
      <version>2.7.18</version>
  </dependency>

不需要进行任何配置,启动项目即可。

项目启动后,在控制台就会打印出默认账户 user 的登录密码(临时的)。通过 localhost:port 就可以看到默认的登录页面。

在密码框输入上面的密码即可登录。与整合 shiro 相比,简单不少。

ps:Spring Boot 整合 Shiro + Jwt 实现简单权鉴功能请参看这篇文章

在项目中如上功能肯定是无法满足需要的,因此就需要思考一个问题,如何与项目本身的用户模块整合在一起?

spring-security的用户配置

spring-security 框架中,支持四种方式的用户配置,我们需要编写一个配置类,用于配置我们想要的用户配置方式;配置类需要继承 WebSecurityConfigurerAdapter 类、重写 configure() 方法用于配置。

spring-security支持如下的四种配置:

  • 内存用户存储:基于内存的
  • 数据库用户存储:基于数据库的
  • LDAP用户存储:基于LADP的
  • 自定义用户存储:用户自定义的

前三种基本上都很少用,这里以内存为例,代码配置如下:


package com.tutorial.security.config;
/**
 * @author: suchao
 * 创建时间: 2024年05月05日 15:36
 * 文件描述:
 */
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        // 注解标记允许匿名访问的url
        httpSecurity.authorizeRequests()
                .antMatchers("/user/getUser.do").hasRole("ADMIN")
                .antMatchers("/sec/login.do").permitAll()
                .anyRequest().authenticated();            
    }
​
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
​
        auth.inMemoryAuthentication().passwordEncoder(passwordEncoder())
                .withUser("supersist").password(passwordEncoder().encode("123456")).authorities("ADMIN")
                .and()
                .withUser("superman").password(passwordEncoder().encode("123456")).authorities("ORDINARY");
    }
   // 密码加密方式
    private PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
​

在实际的开发中,最常用的还是用户自定义的方式,spring-security框架给我们提供了一个接口UserDetailsService、我们只需要实现loadUserByUsername()方法即可,示例代码如下:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
​
    @Resource
    private UserRepository userRepository;
​
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
​
        if (userRepository.findUserEntityByUsername(username) != null) {
            return userRepository.findUserEntityByUsername(username);
        }
​
        return null;
    }
}

UserDetails是一个接口,我们返回的查询对象需要实现此接口,在这里图简单、在DO就实现了此接口。实体类代码如下:

@Entity
@Data
@Table(name = "user")
public class UserEntity implements UserDetails {
​
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long userId;
    private String username;
    private String password;
    private String email;
    private String phone;
    private String address;
    private String avatar;
    private String role;
    private String status;
    private Date createTime;
    private Date updateTime;
​
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // 获取用户权限
       return null;
    }
​
    @Override
    public boolean isAccountNonExpired() {
        return false;
    }
​
    @Override
    public boolean isAccountNonLocked() {
        return false;
    }
​
    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }
​
    @Override
    public boolean isEnabled() {
        return false;
    }
}
​

接下来就需要修改 SecurityConfig 的代码,在修改之前我们需要思考这几个问题:

  • security 是基于 filter 的,用户认证和用户授权是在哪里执行的?
  • 认证和授权失败该如何响应客户端?
  • 现在的项目大多都为前后端分离,权限大都采用 JWT 进行 token 的创建与验证,如何与 JWT连用?

基于第一个问题:

UsernamePasswordAuthenticationFilter: 是 Spring Security 中用于处理基于用户名和密码进行身份验证的过滤器。当用户尝试通过提交用户名和密码的方式进行身份验证时,这个过滤器会拦截请求并处理相应的身份验证逻辑。

UsernamePasswordAuthenticationToken: 是 Spring Security 中用于表示基于用户名和密码进行身份验证的对象。当用户使用用户名和密码进行身份验证时,通常会创建一个 UsernamePasswordAuthenticationToken 对象,并通过 Spring Security 的认证流程进行验证。

第二个问题:

认证未通过的情况处理:

AuthenticationEntryPointSpring Security 中用于处理未经身份验证的用户尝试访问受保护资源的接口。当用户尝试访问需要进行身份验证的资源,但未提供有效的凭据时,AuthenticationEntryPoint 负责返回适当的响应,提示用户进行身份验证。

主要功能包括:

  1. Commence 方法AuthenticationEntryPoint 接口只有一个方法 commence,该方法接收 HttpServletRequest、HttpServletResponseAuthenticationException 对象作为参数。在此方法中,开发人员可以自定义响应行为,例如返回自定义的错误页面、JSON 响应或重定向到登录页面等。
  2. 处理未认证的请求:当未经认证的用户尝试访问需要身份验证的资源时,AuthenticationEntryPoint 负责响应,提示用户进行身份验证。
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable
{
    private static final long serialVersionUID = -8970718410437077606L;
​
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
            throws IOException
    {
        HttpStatus unauthorized = HttpStatus.UNAUTHORIZED;
        String msg = String.format("请求访问,认证失败,无法访问系统资源");
        
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("code", String.valueOf(unauthorized.value()));
        hashMap.put("msg", msg);
        response.setStatus(200);
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");
        response.getWriter().print(hashMap);
    }
}

认证通过但是授权未通过的情况:

AccessDeniedHandlerSpring Security 中用于处理已经认证但无权访问资源的用户尝试访问受保护资源的接口。AccessDeniedHandler 负责返回适当的响应,通常是一个访问拒绝页面或错误消息。

主要功能包括:

  1. Handle 方法AccessDeniedHandler 接口只有一个方法 handle,该方法接收 HttpServletRequest、HttpServletResponse 和 AccessDeniedException 对象作为参数。在此方法中,开发人员可以自定义拒绝访问的响应行为,例如返回自定义的错误页面、JSON 响应或重定向到错误页面等。
  2. 处理访问被拒绝的情况:当已认证的用户尝试访问其没有权限的资源时,AccessDeniedHandler 负责响应,提示用户访问被拒绝。
 private AccessDeniedHandler accessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            // 设置响应的状态码为 403 Forbidden
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            // 返回自定义的错误消息或重定向到访问拒绝页面等
            String msg = "权限不足,请联系管理员!" + "Access denied: " + accessDeniedException.getMessage();
            HashMap<String, String> hashMap = new HashMap<>();
            hashMap.put("code", "401");
            hashMap.put("msg", msg);
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().write(String.valueOf(hashMap));
        };
    }

第三个问题:

JWT token 通常会包含用户的唯一性标识,用于判断用户是否为系统用户进而判断用户的一些基本信息,如权限、token 是否过期等。因此 JWTFilter 必定配置在 UsernamePasswordAuthenticationFilter 之前。

这里就需要考虑另外的问题:

  • 如何写这个 JWTFilter
  • 如何在 SecurityConfig 中配置

第一个小问题:这里需要使用到另外的Filter: OncePerRequestFilterSpring Security 中的一个过滤器基类,它确保在请求处理过程中只执行一次过滤逻辑。即使请求经过多个过滤器链,也只会执行一次该过滤器的逻辑。

@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
​
    @Resource
    IUserService userService;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        log.info("JwtAuthenticationTokenFilter");
        // 模拟验证:没有实现具体的jwtToken的创建和验证,可在这里进行判断token是否合法
        String token = request.getHeader("Authorization");
        UserEntity loginUser = new UserEntity();
        loginUser.setUsername("admin");
        loginUser.setUserId(1L);
        // 合法就需要创建一个authenticationToken给UsernamePasswordAuthenticationFilter使用。如果没有则进入AuthenticationEntryPointImpl的逻辑
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("admin", loginUser, loginUser.getAuthorities());
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
​
        chain.doFilter(request, response);
    }
}
// 自定义的IUserService接口
public interface IUserService {
    void createUser(String username,String password);
    UserEntity findUserByUsername(String username);
}

接着在配置类中修改代码如下:

package com.tutorial.security.config;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
​
    @Resource
    private UserDetailsServiceImpl userDetailsService;
    @Resource
    private AuthenticationEntryPointImpl unauthorizedHandler;
​
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable().and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler)
                .accessDeniedHandler(accessDeniedHandler())
                .and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 允许匿名访问
                .antMatchers("/sec/login.do").permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                //.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                .antMatchers("/user/getUser.do").hasAuthority("ADMIN")
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
      // JWT Filter配置
        httpSecurity.addFilterBefore(new JwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
    }
​
    // userDetailService 配置
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
    }
​
    private PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
​
    private AccessDeniedHandler accessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            // 设置响应的状态码为 403 Forbidden
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            // 返回自定义的错误消息或重定向到访问拒绝页面等
            String msg = "权限不足,请联系管理员!" + "Access denied: " + accessDeniedException.getMessage();
            HashMap<String, String> hashMap = new HashMap<>();
            hashMap.put("code", "401");
            hashMap.put("msg", msg);
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            //response.getWriter().print(hashMap);
            response.getWriter().write(String.valueOf(hashMap));
        };
    }
​
}

编写如下的两个 controller:

@RestController
@Slf4j
@RequestMapping("user/")
@CrossOrigin(allowedHeaders = "*",origins = "*")
public class UserController {
    @Resource
    private IUserService userService;
​
    @GetMapping("getUser.do")
    //@PreAuthorize("hasRole('ROLE_ADMIN')")
    public UserEntity register(String username)
    {
        log.info("username:{}",username);
        return userService.findUserByUsername(username);
    }
​
    @GetMapping("getUser2.do")
    public UserEntity register2(String username)
    {
        log.info("username:{}",username);
        return userService.findUserByUsername(username);
    }
}
@RestController
@Slf4j
@RequestMapping("sec/")
@CrossOrigin(allowedHeaders = "*",origins = "*")
public class SecurityController {
​
    @Resource
    private IUserService userService;
​
    @PostMapping("login.do")
    public UserEntity login(@RequestBody UserEntity loginUser)
    {
        log.info("username:{},password:{}",loginUser.getUsername(),loginUser.getPassword());
        return userService.findUserByUsername(loginUser.getUsername());
    }
    
}

路由说明:

  • /sec/login.do:是不需要权鉴的可直接访问。
  • /user/getUser.do:需要admin的权限才能访问。
  • /user/getUser2.do: 不需要admin权限就可访问。

启动项目:通过postman进行验证,可以发现在访问getUser.do的接口时返回如下的信息:

{msg=权限不足,请联系管理员!Access denied: Access is denied, code=401}:说明验证已通过但是权限不足。

接着修改 UserEntity 的这部分代码如下:

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    // 获取用户权限 假定用户有admin权限。
    ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
    grantedAuthorities.add(new GrantedAuthority() {
        @Override
        public String getAuthority() {
            return "ADMIN";
        }
    });
    return grantedAuthorities;
}

继续访问 getUser.do 就可以得到正常的访问结果。以上便是整合的简单示例代码

需要说明的是如果在配置类使用的 hasRole("ADMIN"),则 UserEntity 需要修改为ROLE_ADMIN;需要一个 ROLE 的前缀。

RuoYi是如何使用spring-security的

有了上面的理论基础,就看看实际项目中是如何使用的,首先便是看一下相关的配置文件SecurityConfig,这里只展示主要配置代码:

@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)@EnableGlobalMethodSecurity 是 Spring Security 中用于启用方法级别的安全性的注解。通过该注解,可以在方法级别上进行安全性配置,例如控制哪些方法需要特定的权限、启用或禁用方法级别的安全注解等。

  • securedEnabled = true 启用了 @Secured 注解,允许在方法上使用 @Secured 进行安全配置。
  • prePostEnabled = true 启用了 @PreAuthorize@PostAuthorize 注解,允许在方法上使用 @PreAuthorize@PostAuthorize 进行安全配置。
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
    // 注解标记允许匿名访问的url
    ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
    permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
​
    httpSecurity
            // CSRF禁用,因为不使用session
            .csrf().disable()
            // 禁用HTTP响应标头
            .headers().cacheControl().disable().and()
            // 认证失败处理类
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).
            // 认证成功,授权失败处理类: 这里是我我自己加的!
            accessDeniedHandler(accessDeniedHandler()).
            and()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 过滤请求
            .authorizeRequests()
            // 对于登录login 注册register 验证码captchaImage 允许匿名访问
            .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()
            .and()
            .headers().frameOptions().disable();
    // 添加Logout filter
    httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
    // 添加JWT filter
    httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter
    httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}

首先是Anonymous注解配置的可以匿名访问URL:

//PermitAllUrlProperties.java 
@Override
  public void afterPropertiesSet()
  {
      RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
      Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
​
      map.keySet().forEach(info -> {
          HandlerMethod handlerMethod = map.get(info);
​
          // 获取方法上边的注解 替代path variable 为 *
          Anonymous method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), Anonymous.class);
          Optional.ofNullable(method).ifPresent(anonymous -> Objects.requireNonNull(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 -> Objects.requireNonNull(info.getPatternsCondition().getPatterns())
                  .forEach(url -> urls.add(RegExUtils.replaceAll(url, PATTERN, ASTERISK))));
      });
  }

接着看 JwtAuthenticationTokenFilter:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
  private static final Logger log = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class);
  @Autowired
  private TokenService tokenService;
​
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException
  {
      // 得到loginUser的信息
      LoginUser loginUser = tokenService.getLoginUser(request);
      // 如果没有authenticationToken 则UsernamePasswordAuthenticationFilter验证失败就会进入认证失败的处理类中
      if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
      {
          tokenService.verifyToken(loginUser);// 验证通过 刷新redis缓存的过期时间
          log.info("新建authenticationToken: {}", JSON.toJSONString(loginUser));
          UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
          authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
          SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
      chain.doFilter(request, response);
  }
}

接着看一下 RuoYi 的登录逻辑:登录之后然后创建一个 token 并给前端返回。前端在请求时会携带此token。

@PostMapping("/login")
  public AjaxResult login(@RequestBody LoginBody loginBody)
  {
      log.info("登录请求:" + loginBody.getUsername() + " " + loginBody.getPassword());
      AjaxResult ajax = AjaxResult.success();
      // 生成令牌
      String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
              loginBody.getUuid());
    // 添加创建的token
      ajax.put(Constants.TOKEN, token);
      return ajax;
  }
​
 public String login(String username, String password, String code, String uuid)
    {
        // 验证码校验
        validateCaptcha(username, code, uuid);
        // 登录前置校验
        loginPreCheck(username, password);
        // 用户验证
        Authentication authentication = null;
        try
        {
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
            AuthenticationContextHolder.setContext(authenticationToken);
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager.authenticate(authenticationToken);
        }
        catch (Exception e)
        {
            if (e instanceof BadCredentialsException)
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
                throw new UserPasswordNotMatchException();
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, e.getMessage()));
                throw new ServiceException(e.getMessage());
            }
        }
        finally
        {
            AuthenticationContextHolder.clearContext();
        }
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        recordLoginInfo(loginUser.getUserId());
        // 生成token
        return tokenService.createToken(loginUser);
    }

ps:在新增用户或者修改用户时,都是调用了对应密码的加密的。

public AjaxResult add(@Validated @RequestBody SysUser user)
{
    if (!userService.checkUserNameUnique(user))
    {
        return error("新增用户'" + user.getUserName() + "'失败,登录账号已存在");
    }
    else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user))
    {
        return error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
    }
    else if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(user))
    {
        return error("新增用户'" + user.getUserName() + "'失败,邮箱账号已存在");
    }
    user.setCreateBy(getUsername());
    user.setPassword(SecurityUtils.encryptPassword(user.getPassword()));
    return toAjax(userService.insertUser(user));
}
// SecurityUtils.java
public static String encryptPassword(String password)
{
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    return passwordEncoder.encode(password);
}

这上面是认证的过程,接着看一下授权相关的逻辑,首先看是如何使用的:

  @PreAuthorize("@ss.hasPermi('system:dept:list')")
  @GetMapping("/list")
  public AjaxResult list(SysDept dept)
  {
      List<SysDept> depts = deptService.selectDeptList(dept);
      return success(depts);
  }

在配置类上开启了相关的注解的使用,这里通过 spel 表达式调用ss.hasPermi()方法。hasPermi 在类 PermissionService 实现的,逻辑如下:

public boolean hasPermi(String permission)
{
    if (StringUtils.isEmpty(permission))
    {
        return false;
    }
    LoginUser loginUser = SecurityUtils.getLoginUser();
    if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
    {
        return false;
    }
    PermissionContextHolder.setContext(permission);
    return hasPermissions(loginUser.getPermissions(), permission);
}
/**
 * 判断是否包含权限
 * 
 * @param permissions 权限列表
 * @param permission 权限字符串
 * @return 用户是否具备某权限
 */
private boolean hasPermissions(Set<String> permissions, String permission)
{
    return permissions.contains(Constants.ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}

以上便是 RuoYi 中对权鉴相关功能的实现。

参考资料: