SpringSecurity介绍

(这里是结合若依仅做了SpringSecurity核心配置的简单介绍,如果想要系统性掌握SpringSecurity请移步更专业的文章或官方文档)

Spring Security是一个功能强大的Java安全框架,它提供了全面的安全认证和授权的支持。

与RBAC模型结合使用时,Spring Security能够实现灵活的权限控制。

我们来看下它的二大核心概念,认证和授权。让我们用一个简单的例子来领会它们的意义。

1)认证(Authentication)想象一下,小智同学去图书馆借书。门口的图书管理员会要求小智出示借书证。这个借书证上有小智的照片和姓名,管理员通过它来确认小智的身份。这个过程,就是一个认证的实例。在Spring Security的世界里,认证就像用户登录时提交的用户名和密码,系统通过这些信息来验证“你是谁”。

Spring Security不仅支持传统的用户名和密码认证,还支持OAuth2、JWT等现代认证方式。

2)授权(Authorization) 现在,假设小智同学已经踏入了图书馆的大门,但当小智想进入只有持特定权限才能进入的参考书籍区时,管理员会再次检查小智的借书证,看是否有相应的权限标记。如果有,那么小智就能进入这个区域;如果没有,那么小智只能遗憾地止步。这就是授权的精髓。在Spring Security中,授权是确认用户在通过认证之后,是否有权限执行某些操作或访问特定资源。

SpringSecurity配置

Spring Security的配置类是实现安全控制的核心部分

开启Spring Security各种功能,以确保Web应用程序的安全性,包括认证、授权、会话管理、过滤器添加等。

package com.dkd.framework.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.web.filter.CorsFilter;
import com.dkd.framework.config.properties.PermitAllUrlProperties;
import com.dkd.framework.security.filter.JwtAuthenticationTokenFilter;
import com.dkd.framework.security.handle.AuthenticationEntryPointImpl;
import com.dkd.framework.security.handle.LogoutSuccessHandlerImpl;

/**
 * spring security配置
 *
 * @author ruoyi
 */
// 开启方法级别的权限控制 ==> @PreAuthorize
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 认证失败处理类
     */
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;

    /**
     * 退出处理类
     */
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;

    /**
     * token认证过滤器
     */
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;

    /**
     * 跨域过滤器
     */
    @Autowired
    private CorsFilter corsFilter;

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

    /**
     * 解决 无法直接注入 AuthenticationManager
     *
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception
    {
        return super.authenticationManagerBean();
    }

    /**
     * anyRequest          |   匹配所有请求路径
     * access              |   SpringEl表达式结果为true时可以访问
     * anonymous           |   匿名可以访问
     * denyAll             |   用户不能访问
     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
     * permitAll           |   用户可以任意访问
     * rememberMe          |   允许通过remember-me登录的用户访问
     * authenticated       |   用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception
    {
        // 配置URL访问授权规则
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();
        // 遍历无需认证即可访问的URL列表,设置这些URL对所有用户可访问
        permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());
        // 配置Web应用程序规则
        httpSecurity
                // CSRF(跨站请求伪造)禁用,因为不使用session
                .csrf().disable()
                // 禁用HTTP响应标头
                .headers().cacheControl().disable().and()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).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);
    }

    /**
     * 强散列哈希加密实现
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()
    {
        return new BCryptPasswordEncoder();
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception
    {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
    }
}

用户登录流程

管理员在登录页面,输入用户名和密码以及验证码后,点击登录按钮,向后端发送请求,后端通过springSecurity认证管理器进行登录校验

此功能涉及前端相关的代码位于views/login.vue这个视图组件中,负责实现用户登录界面和交互逻辑。

后端处理逻辑则在dkd-admin模块的SysLoginController中,负责接收前端的请求,处理登录逻辑,并返回token令牌

前端

  1. 点击login.vue中的登录按钮

  2. 调用login.vue中的handleLogin方法

  3. 调用store/mondles/user.js中的login方法,将返回结果存入useUserStore对象中(用于管理用户相关的状态和操作)

  4. 调用api/login.js中的login方法

  5. 调用utils/request.js中的service实例基于axios发送ajax请求(.env.development文件设置了统一请求路径前缀)

后端

SysLoginController

ruoyi-admin模块中com.ruoyi.web.controller.system.SysLoginController类的login方法接收前端登录请求

SysLoginService

ruoyi-framework模块中com.ruoyi.framework.web.service.SysLoginService类的login方法处理登录逻辑

  1. 验证码校验

  2. 登录前置校验

  3. SS认证管理器用户校验,调用执行UserDetailsServiceImpl.loadUserByUsername

    1. 认证通过后,创建登录用户对象LoginUser包括用户ID、部门ID、用户信息和用户权限信息

  4. 登录成功,记录日志

  5. 修改用户表更新登录信息

  6. 生成token

具体的执行的流程如下图:

获取用户角色和权限

超级管理员登录帝可得系统后,可以查看区域管理所有的权限按钮,实现增删改查操作

而财务人员登录帝可得系统后,仅可以查看区域列表,其他权限没有

1)首先创建一个财务人员的角色,权限字符串 Accountant,仅设置区域管理查询权限

2)再创建一个用户,昵称为黑马彭于晏,用户名hmpyy,密码admin123 角色为财务人员

前端

  1. 在全局permission.js中的router.beforeEach方法用于在用户导航到不同路由之前进行一些预处理

  2. 调用store/mondles/user.js中的getInfo方法,将返回结果存入useUserStore对象中(用于管理用户相关的状态和操作)

  3. 调用api/login.js中的getInfo方法

后端

SysLoginController

ruoyi-admin模块中com.ruoyi.web.controller.system.SysLoginController类的getInfo方法接收前端获取用户信息请求

SysPermissionService

ruoyi-framework模块中com.ruoyi.framework.web.service.SysPermissionService

  1. getRolePermission查询该用户角色集合

  2. getMenuPermission查询该用户权限(菜单)集合

具体的执行的流程如下图:

页面权限

前端封装了一个指令权限,能简单快速的实现按钮级别的权限判断。

使用权限字符串 v-hasPermi@/directive/permission/hasPermi.js

// 单个
<el-button v-hasPermi="['system:user:add']">存在权限字符串才能看到</el-button>
// 多个
<el-button v-hasPermi="['system:user:add', 'system:user:edit']">包含权限字符串才能看到</el-button>

使用角色字符串 v-hasRole@/directive/permission/hasRole.js

// 单个
<el-button v-hasRole="['admin']">管理员才能看到</el-button>
// 多个
<el-button v-hasRole="['role1', 'role2']">包含角色才能看到</el-button>

获取动态菜单路由

在上个小节我们创建了“黑马彭于晏”这个用户,并为他分配了特定的角色权限。这意味着,当他登录帝可得系统时,看到的侧边栏菜单将根据他的角色权限而有所不同。而超级管理员是可以查看所有菜单的

实现此功能的前端代码位于src/permission.js文件。它在登录成功后,会在跳转到新路由之前,去查询当前用户有权访问的动态菜单的路由列表。

后端处理逻辑则在dkd-admin模块的SysLoginController中,它负责接收前端发来的请求,处理查询,并构建起一个完整的菜单树结构,然后返回给前端。

前端

  1. 在全局permission.js中的router.beforeEach方法用于在用户导航到不同路由之前进行一些预处理

  2. 调用store/mondles/permission.js中的generateRoutes方法,将返回结果存入usePermissionStore对象中

  3. 调用api/menu.js中的getRouters方法

后端

SysLoginController

ruoyi-admin模块中com.ruoyi.web.controller.system.SysLoginController类的getRouters方法接收前端获取路由信息请求

ISysMenuService

ruoyi-system模块中com.ruoyi.web.system.service.ISysMenuService

  1. selectMenuTreeByUserId根据用户ID查询菜单树信息(递归生成父子菜单)

  2. buildMenus构建前端路由所需要的菜单路由格式RouterVo

具体的执行的流程如下图:

路由菜单加载

  1. 用户登录成功后,通过路由router/index.js跳转到首页并加载layout布局组件

  2. layout/index.vue中加载sidbar侧边栏

  3. layout/components/Sidebar/index.vue中遍历动态路由菜单在页面显示

  4. 用户点击菜单后会根据路由的path跳转到对应的视图组件在<app-main />显示

权限注解

源码分析

在若依框架中,权限的验证最核心的是使用的Spring Security的提供的权限注解@PreAuthorize

  • @PreAuthorize 是 Spring Security 框架中提供的一个安全注解,用于实现基于注解的访问控制。它允许开发者在方法级别上声明特定的安全约束,以确保只有满足指定条件的用户才能调用该方法

  • 当 @PreAuthorize 注解被应用于某个方法时,Spring Security 在该方法执行前会先对当前认证的用户进行权限检查。如果检查通过,方法调用得以继续;否则,框架会抛出相应的权限异常(如 AccessDeniedException),阻止方法执行。

若依框架中的权限控制代码,如下:

@PreAuthorize("@ss.hasPermi('manage:order:list')")
public TableDataInfo list(Order order)
{
    startPage();
    List<Order> list = orderService.selectOrderList(order);
    return getDataTable(list);
}
  • @PreAuthorize是Spring Security框架的权限注解,在执行方法前执行

  • @ss.hasPermi('manage:order:list')

    • 其中的ss是指的一个spring管理的bean

      • 位置:ruoyi-framework模块中的com.ruoyi.framework.web.service.PermissionService

    • hasPermi 是PermissionService类中的一个方法,判断是否拥有该权限

    • manage:order:list为方法的参数

注意:在SecurityConfig类中添加@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)@PreAuthorize才能生效

权限控制流程:

权限方法

@PreAuthorize注解用于配置接口要求用户拥有某些权限才可访问,它拥有如下方法

方法 参数 描述
hasPermi String 验证用户是否具备某权限
lacksPermi String 验证用户是否不具备某权限,与 hasPermi逻辑相反
hasAnyPermi String 验证用户是否具有以下任意一个权限
hasRole String 判断用户是否拥有某个角色
lacksRole String 验证用户是否不具备某角色,与 hasRole逻辑相反
hasAnyRoles String 验证用户是否具有以下任意一个角色,多个逗号分隔

使用示例

1)数据权限示例。

// 符合system:user:list权限要求
@PreAuthorize("@ss.hasPermi('system:user:list')")

// 不符合system:user:list权限要求
@PreAuthorize("@ss.lacksPermi('system:user:list')")

// 符合system:user:add或system:user:edit权限要求即可
@PreAuthorize("@ss.hasAnyPermi('system:user:add,system:user:edit')")

编程式判断是否有资源权限 :

if (SecurityUtils.hasPermi("sys:user:edit"))
{
    System.out.println("当前用户有编辑用户权限");
}

2)角色权限示例。

// 属于user角色
@PreAuthorize("@ss.hasRole('user')")

// 不属于user角色
@PreAuthorize("@ss.lacksRole('user')")

// 属于user或者admin之一
@PreAuthorize("@ss.hasAnyRoles('user,admin')")

编程式判断是否有角色权限 :

if (SecurityUtils.hasRole("admin"))
{
    System.out.println("当前用户有admin角色权限");
}

权限提示:超级管理员拥有所有权限,不受权限约束。

公开接口

如果有些接口是不需要验证权限可以公开访问的,这个时候就需要我们给接口放行。

使用注解方式,只需要在Controller的类或方法上加入@Anonymous该注解即可

// @PreAuthorize("@ss.xxxx('....')") 注释或删除掉原有的权限注解
@Anonymous
@GetMapping("/list")
public List<SysXxxx> list(SysXxxx xxxx)
{
    return xxxxList;
}

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐