若依项目学习笔记04——Security权限

学习前先说明一下,这里默认大家都是了解了相关技术的,如果还没学习过的话,大家先去简单看一下大致的相关教程,这里就不占用篇幅来讲解相关技术概念和原理啦;
本项目需要讲解的Security部分还是挺多的,如配置介绍、密码加密、退出配置、登陆配置、权限讲解和权限注解等,我都汇总给到这一个章节来讲

1. 配置介绍

1.1 com.ruoyi.framework.config.SecurityConfig

    /**
     * EnableGlobalMethodSecurity:开启security功能;prePostEnabled:方法执行前验证;securedEnabled:是否有权限访问
     */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter //要配置security就要继承这个
{
    @Autowired
    private UserDetailsService userDetailsService;//自定义用户认证逻辑
    @Autowired
    private AuthenticationEntryPointImpl unauthorizedHandler;//认证失败处理类
    @Autowired
    private LogoutSuccessHandlerImpl logoutSuccessHandler;//退出处理类
    @Autowired
    private JwtAuthenticationTokenFilter authenticationTokenFilter;//token认证过滤器
    @Autowired
    private CorsFilter corsFilter;//跨域过滤器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception//解决 无法直接注入 AuthenticationManager
    {
        //重新获取一下AuthenticationManager
        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
    {
        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类:exceptionHandling()允许配置异常处理,
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 屏蔽session:基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录login 验证码captchaImage 允许匿名访问
                .antMatchers("/login", "/captchaImage").anonymous()
                // 对指定的资源放行
                .antMatchers(
                        HttpMethod.GET,
                        "/*.html",
                        "/**/*.html",
                        "/**/*.css",
                        "/**/*.js"
                ).permitAll()
                // 放行文件上传(如用户头像)
                .antMatchers("/profile/**").anonymous()
                 // 放行文件下载
                .antMatchers("/common/download**").anonymous()
                .antMatchers("/common/download/resource**").anonymous()
                //放行swagger,关于swagger下面有介绍博文
                .antMatchers("/swagger-ui.html").anonymous()
                .antMatchers("/swagger-resources/**").anonymous()
                .antMatchers("/webjars/**").anonymous()
                .antMatchers("/*/api-docs").anonymous()
                //放行阿里druid监控的控制台
                .antMatchers("/druid/**").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated()
                .and()
                .headers().frameOptions().disable();
        //重写退出后的处理类,从request中拿到token进行判断
        httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
        // 添加JWT filter,在authenticationTokenFilter进行token认证;下面有关于jwt的教程博文链接
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder()//强散列哈希加密实现
    {
        return new BCryptPasswordEncoder();//进到源码PasswordEncoder中,encode加密,matches匹配
    }

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

Swagger:用JSON或YAML元数据来描述API属性,并且提供了Web UI,它可以将元数据转换为一个很好的HTML文档,在该UI中,我们可以浏览有关API端点的信息,还可以将UI用作REST客户端 ,调用任何端点,指定要发送的数据并检查响应;关于swagger的教程大家可以先看看这篇博文
JWT:JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,服务器不保存 session 数据,所有数据都保存在客户端,每次请求都发回服务器;详细见这篇介绍博文


2. 密码加密

接着上面的security配置文件讲,即 configure() 方法

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

我们进入BCryptPasswordEncoder的实现接口PasswordEncoder

可以看到其中有两个方法,一个是对密码进行加密的 encode() 方法,传入明文密码,然后对其进行MD5加密,这个过程是不可逆的,也就是说每次加密生成的密码都是不一样的;另一个是 matches() 通过对明文密码和加密后的密码进行匹配;例如在重置密码中就有调用该方法
个人信息 业务处理SysProfileController
在用户信息SysUserController的新增用户方法也有调用到
add
下面是被调用的安全服务工具类SecurityUtils的加密方法 encryptPassword() 和 匹配方法 matchesPassword()

public static String encryptPassword(String password)
{
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    return passwordEncoder.encode(password);
}
public static boolean matchesPassword(String rawPassword, String encodedPassword)
{
    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    return passwordEncoder.matches(rawPassword, encodedPassword);
}

我们可以通过SysLoginService(登录校验方法)进行查看其进行的流程;下面是SysLoginService中涉及的登录验证方法

      // 用户验证
        Authentication authentication = null;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        }
        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 CustomException(e.getMessage());
            }
        }

该用户验证方法调用 UserDetailsServiceImpl.loadUserByUsername(大家自行查看),对进行判断,不存在/被删除/停用?如果是的话,则匹配是否是 BadCredentialsException ,然后抛出 UserPasswordNotMatchException() 用户密码不匹配异常


3. 退出配置

我们已经在SecurityConfig中定义了退出的配置了

//指定了退出的配置.退出的Url.退出成功的处理方法
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);

LogoutSuccessHandlerImpl 实现了 LogoutSuccessHandler ,重写了其中的 onLogoutSuccess() 方法,删除缓存并记录日志
我们用vscode打开前端代码,来到 src.layout.omponents.Navbar.vue (Navbar.vue是项目顶栏的导航区域)中,可以看到退出登录部分有一个点击事件 logout

<el-dropdown-item divided @click.native="logout">
     <span>退出登录</span>
</el-dropdown-item>

我们往下看,来到 <script> 区,有定义了退出点击事件的方法

methods: {
    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
    },
    async logout() {//对弹出框进行配置
      this.$confirm('确定注销并退出系统吗?', '提示', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      }).then(() => {
        this.$store.dispatch('LogOut').then(() => {//如果点击确定,则调用logout方法,并跳转到首页登录页
          location.href = '/index';
        })
      })
    }
  }

被调用的logout方法为 src.api.login.js

// 退出方法
export function logout() {
  return request({//定义了请求路径和方式
    url: '/logout',
    method: 'post'
  })
}

退出流程


4. 登录配置 *

我们继续回到 SecurityConfig 配置中,其中有登录(身份认证接口)的实现

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception
{
    auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}

UserDetailsService 是一个接口,其中有登录的抽象方法 loadUserByUsername;下面我们通过登录流程来讲解登录是如何配置和进行的,我们来到登录验证 SysLoginController

@PostMapping("/login")
public AjaxResult login(@RequestBody LoginBody loginBody)
{
    AjaxResult ajax = AjaxResult.success();
    // 生成令牌,传入用户名、密码、验证码和uuid进行判断
    String token = loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),
            loginBody.getUuid());
    ajax.put(Constants.TOKEN, token);将判断信息加入AjaxResult
    return ajax;
}

登录方法调用了 loginService ,其中包括对验证码的判断,对用户状态的判断和生成其token
登录


5. 权限讲解

这部分我们配合 SysLoginService 来讲解,依旧是用户验证部分,调用 UserDetailsServiceImpl.loadUserByUsername ,在 loadUserByUsername() 中验证完用户状态后,会创建登录用户,这是会调用 SysPermissionService.getMenuPermission ,即菜单权限的集合,管理员则使用 perms.add("*:*:*") 来赋予其所有权限,这各会在后面判断;普通用户则使用一个普通的数据库查询方法 selectMenuPermsByUserId(user.getUserId()) ,其返回一个集合,我们可以点进去看看,她所对应的mapper文件是 SysMenuMapper ,我们找到 selectMenuPermsByUserId

<select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">
	select distinct m.perms
	from sys_menu m
		 left join sys_role_menu rm on m.menu_id = rm.menu_id
		 left join sys_user_role ur on rm.role_id = ur.role_id
		 left join sys_role r on r.role_id = ur.role_id
	where m.status = '0' and r.status = '0' and ur.user_id = #{userId}
</select>

该功能就是返回perms的字段,去重;我们可以打开数据库查看对应的表格
perms

perms字段都是各种查询、增删查改和导入导出等标识,即对应的业务模块;查询完毕后返回一个 LoginUser(实现了 UserDetails) ,其结果就是所创建新登录用户的权限

public LoginUser(SysUser user, Set<String> permissions)
{
    this.user = user;
    this.permissions = permissions;
}

然后我们再回到 SysLoginService ,我们拿到一个 LoginUser 后,通过 createToken(loginUser) 将她存到token当中;然后使用 refreshToken(loginUser) 刷新,然后在当中去把对应权限保存保存在缓存中和设置他的loginUser,这样下次就可以直接在缓存中校验,其对应实现在 framework.web.service.PermissionService中;

注意区分 菜单权限角色权限


6. 权限注解

这部分讲权限注解部分,我们打开 PermissionService ,这个类是处理权限注解相关部分;@Service("ss") 是自定义的service标识符

一开始定义的分割符是 SysMenuServiceImpl.selectMenuPermsByUserId 所需,因为该方法索要遍历的集合是用 “,” 进行分割的;下面对 PermissionService 中的方法进行讲解

  • hasPermi() : 验证用户是否具备某权限
public boolean hasPermi(String permission)//传进来一个权限标识字符串
{
    //判断是否为空
    if (StringUtils.isEmpty(permission))
    {
        return false;
    }
    //从 request中获取用户信息
    LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
    if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))//如果不为空则有这个用户
    {
        return false;
    }
    //对权限和权限集合进行匹配,匹配成功则说明有对应权限,返回boolean
    return hasPermissions(loginUser.getPermissions(), permission);
}

这个方法我们结合创建用户来理解,打开 ruoyi.web.controller.system.add 方法;我们看预处理标签 @PreAuthorize("@ss.hasPermi('system:user:add')") ,这里会匹配service标识符为 ss 的类,然后调用hasPermi方法传入 “system:user:add” 权限字符串,去比较当前用户是否拥有新增用户的权限

  • lacksPermi() : 验证用户是否不具备某权限,与 hasPermi逻辑相反;比如我们让某个用户没有查询的权限,其他权限都有,就可以调用这个方法

  • hasAnyPermi() : 验证用户是否具有以下任意一个权限;比如上面的新增用户方法中,我们不止要验证是否有add权限,还要验证是否有编辑edit权限,我们可以改成 @PreAuthorize("@ss.hasAnyPermi('system:user:add','system:user:edit')")

  • hasRole() : 判断用户是否拥有某个角色;

  • lacksRole() : 验证用户是否不具备某角色,与 hasRole逻辑相反

  • hasAnyRoles() : 验证用户是否具有以下任意一个角色

综上,权限注解的使用就是根据自己所需的功能要求,在对应的控制类方法中添加 @PreAuthorize 预处理注解,通过 @ss 来找到当前所需被标识为 @Service("ss") 的 的service类,然后通过方法名和传入的权限参数来调用相应的权限判断方法;有些方法可以传入多个权限参数,特别要注意,要根据所需的方法来改对应的调用方法名

posted @ 2020-11-27 10:34  刘条条  阅读(1557)  评论(0编辑  收藏  举报