【SpringBoot】廿三、SpringBoot中整合Shiro实现权限管理

之前在 SSM 项目中使用过 shiro,发现 shiro 的权限管理做的真不错,但是在 SSM 项目中的配置太繁杂了,于是这次在 SpringBoot 中使用了 shiro,下面一起看看吧

一、简介

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

三个核心组件:

1、Subject

即“当前操作用户”。但是,在 Shiro 中,Subject 这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject 代表了当前用户的安全操作,SecurityManager 则管理所有用户的安全操作。

2、SecurityManager

它是Shiro 框架的核心,典型的 Facade 模式,Shiro 通过 SecurityManager 来管理内部组件实例,并通过它来提供安全管理的各种服务。

3、Realm

Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro 会从应用配置的 Realm 中查找用户及其权限信息。从这个意义上讲,Realm 实质上是一个安全相关的 DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给 Shiro。当配置 Shiro 时,你必须至少指定一个 Realm,用于认证和(或)授权。配置多个 Realm 是可以的,但是至少需要一个。Shiro 内置了可以连接大量安全数据源(又名目录)的 Realm,如 LDAP、关系数据库(JDBC)、类似 INI 的文本配置资源以及属性文件等。如果缺省的 Realm 不能满足需求,你还可以插入代表自定义数据源的自己的 Realm 实现。

二、整合 shiro

1、引入 maven 依赖

<!-- web支持 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- thymeleaf 模板引擎 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Shiro 权限管理 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.2.4</version>
</dependency>
<!-- 为了能够在 html 中使用 shiro 的标签引入 -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

我使用的 SpringBoot 版本是 2.3.1,其它依赖自己看着引入吧

2、创建 shiro 配置文件

关于 shiro 的配置信息,我们都放在 ShiroConfig.java 文件中

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * shiro配置类
 */
@Configuration
public class ShiroConfig {

    /**
     * 注入这个是是为了在thymeleaf中使用shiro的自定义tag。
     */
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }

    /**
     * 地址过滤器
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 设置登录url
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 设置主页url
        shiroFilterFactoryBean.setSuccessUrl("/");
        // 设置未授权的url
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 开放登录接口
        filterChainDefinitionMap.put("/doLogin", "anon");
        // 开放静态资源文件
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/layui/**", "anon");
        // 其余url全部拦截,必须放在最后
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

	/**
	* 自定义安全管理策略
	*/
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        /**
         设置自定义的relam
         */
        securityManager.setRealm(loginRelam());
        return securityManager;
    }

	/**
	* 登录验证
	*/
    @Bean
    public LoginRelam loginRelam() {
        return new LoginRelam();
    }

    /**
     * 以下是为了能够使用@RequiresPermission()等标签
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }
}


上面开放静态资源文件,其它博客说的是 **filterChainDefinitionMap.put("/static/**", "anon");** ,但我发现,我们在 html 文件中引入静态文件时,请求路径根本没有经过 static,thymeleaf 自动默认配置 **static/** 下面就是静态资源文件,所以,我们开放静态资源文件需要指定响应的目录路径

2、登录验证管理

关于登录验证的一些逻辑,以及赋权等操作,我们都放在 LoginRelam.java 文件中

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zyxx.sbm.entity.UserInfo;
import com.zyxx.sbm.service.RolePermissionService;
import com.zyxx.sbm.service.UserInfoService;
import com.zyxx.sbm.service.UserRoleService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;

/**
 * 登录授权
 */
public class LoginRelam extends AuthorizingRealm {

    @Autowired
    private UserInfoService userInfoService;
    @Autowired
    private UserRoleService userRoleService;
    @Autowired
    private RolePermissionService rolePermissionService;

    /**
     * 身份认证
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取基于用户名和密码的令牌:实际上这个authcToken是从LoginController里面currentUser.login(token)传过来的
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;

        //根据用户名查找到用户信息
        QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("account", token.getUsername());
        UserInfo userInfo = userInfoService.getOne(queryWrapper);

        // 没找到帐号
        if (null == userInfo) {
            throw new UnknownAccountException();
        }

        // 校验用户状态
        if ("1".equals(userInfo.getStatus())) {
            throw new DisabledAccountException();
        }

        // 认证缓存信息
        return new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getAccount()), getName());
    }

    /**
     * 角色授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        UserInfo authorizingUser = (UserInfo) principalCollection.getPrimaryPrincipal();
        if (null != authorizingUser) {
            //权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

            //获得用户角色列表
            Set<String> roleSigns = userRoleService.listUserRoleByUserId(authorizingUser.getId());
            simpleAuthorizationInfo.addRoles(roleSigns);

            //获得权限列表
            Set<String> permissionSigns = rolePermissionService.listRolePermissionByUserId(authorizingUser.getId());
            simpleAuthorizationInfo.addStringPermissions(permissionSigns);
            return simpleAuthorizationInfo;
        }
        return null;
    }

    /**
     * 自定义加密规则
     *
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        // 自定义认证加密方式
        CustomCredentialsMatcher customCredentialsMatcher = new CustomCredentialsMatcher();
        // 设置自定义认证加密方式
        super.setCredentialsMatcher(customCredentialsMatcher);
    }
}

以上就是登录时,需要指明 shiro 对用户的一些验证、授权等操作,还有自定义密码验证规则,在第3步会讲到,获取角色列表,权限列表,需要获取到角色与权限的标识,每一个角色,每一个权限都有唯一的标识,装入 Set 中

3、自定义密码验证规则

密码的验证规则,我们放在了 CustomCredentialsMatcher.java 文件中

import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.crypto.hash.SimpleHash;

/**
 * @ClassName CustomCredentialsMatcher
 * 自定义密码加密规则
 * @Author Lizhou
 * @Date 2020-07-10 16:24:24
 **/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {

    @Override
    public boolean doCredentialsMatch(AuthenticationToken authcToken, AuthenticationInfo info) {
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        //加密类型,密码,盐值,迭代次数
        Object tokenCredentials = new SimpleHash("md5", token.getPassword(), token.getUsername(), 6).toHex();
        // 数据库存储密码
        Object accountCredentials = getCredentials(info);
        // 将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false
        return equals(tokenCredentials, accountCredentials);
    }
}

我们采用的密码加密方式为 MD5 加密,加密 6 次,使用登录账户作为加密密码的盐进行加密

4、密码加密工具

上面我们自定义了密码加密规则,我们创建一个密码加密的工具类 PasswordUtils.java 文件

import org.apache.shiro.crypto.hash.Md5Hash;


/**
 * 密码加密的处理工具类
 */
public class PasswordUtils {

    /**
     * 迭代次数
     */
    private static final int ITERATIONS = 6;

    private PasswordUtils() {
        throw new AssertionError();
    }

    /**
     * 字符串加密函数MD5实现
     *
     * @param password  密码
     * @param loginName 用户名
     * @return
     */
    public static String getPassword(String password, String loginName) {
        return new Md5Hash(password, loginName, ITERATIONS).toString();
    }
}

三、开始登录

上面,我们已经配置了 shiro 的一系列操作,从登录验证、密码验证规则、用户授权等等,下面我们就开始登录,登录的操作,放在了 LoginController.java 文件中

import com.zyxx.common.consts.SystemConst;
import com.zyxx.common.enums.StatusEnums;
import com.zyxx.common.kaptcha.KaptchaUtil;
import com.zyxx.common.shiro.SingletonLoginUtils;
import com.zyxx.common.utils.PasswordUtils;
import com.zyxx.common.utils.ResponseResult;
import com.zyxx.sbm.entity.UserInfo;
import com.zyxx.sbm.service.PermissionInfoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @ClassName LoginController
 * @Description
 * @Author Lizhou
 * @Date 2020-07-02 10:54:54
 **/
@Api(tags = "后台管理端--登录")
@Controller
public class LoginController {

    @Autowired
    private PermissionInfoService permissionInfoService;

    @ApiOperation(value = "请求登录页面", notes = "请求登录页面")
    @GetMapping("login")
    public String init() {
        return "login";
    }

    @ApiOperation(value = "请求主页面", notes = "请求主页面")
    @GetMapping("/")
    public String index() {
        return "index";
    }

    @ApiOperation(value = "登录验证", notes = "登录验证")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "account", value = "账号", required = true),
            @ApiImplicitParam(name = "password", value = "密码", required = true),
            @ApiImplicitParam(name = "resCode", value = "验证码", required = true),
            @ApiImplicitParam(name = "rememberMe", value = "记住登录", required = true)
    })
    @PostMapping("doLogin")
    @ResponseBody
    public ResponseResult doLogin(String account, String password, String resCode, Boolean rememberMe, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 验证码
        if (!KaptchaUtil.validate(resCode, request)) {
            return ResponseResult.getInstance().error(StatusEnums.KAPTCH_ERROR);
        }
        // 验证帐号和密码
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(account, password);
        // 记住登录状态
        token.setRememberMe(rememberMe);
        try {
            // 执行登录
            subject.login(token);
            // 将用户保存到session中
			UserInfo userInfo = (UserInfo) subject.getPrincipal();
            request.getSession().setAttribute(SystemConst.SYSTEM_USER_SESSION, userInfo);
            return ResponseResult.getInstance().success();
        } catch (UnknownAccountException e) {
            return ResponseResult.getInstance().error("账户不存在");
        } catch (DisabledAccountException e) {
            return ResponseResult.getInstance().error("账户已被冻结");
        } catch (IncorrectCredentialsException e) {
            return ResponseResult.getInstance().error("密码不正确");
        } catch (ExcessiveAttemptsException e) {
            return ResponseResult.getInstance().error("密码连续输入错误超过5次,锁定半小时");
        } catch (RuntimeException e) {
            return ResponseResult.getInstance().error("未知错误");
        }
    }

    @ApiOperation(value = "登录成功,跳转主页面", notes = "登录成功,跳转主页面")
    @PostMapping("success")
    public String success() {
        return "redirect:/";
    }

    @ApiOperation(value = "初始化菜单数据", notes = "初始化菜单数据")
    @GetMapping("initMenu")
    @ResponseBody
    public String initMenu() {
        return permissionInfoService.initMenu();
    }

    @ApiOperation(value = "退出登录", notes = "退出登录")
    @GetMapping(value = "loginOut")
    public String logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "login";
    }
}

当执行 subject.login(token); 时,就会进入我们在 第二步中第二条登录验证中,对用户密码、状态进行检查,对用户授权等操作,登录的密码,一定是通过密码加密工具得到的,不然验证不通过

四、页面权限控制

我们本次使用的是 thymeleaf 模板引擎,我们需要在 html 文件中加入以下内容

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

引入了 thymeleaf 的依赖,以及 shiro 的依赖,这样我们就能在 html 文件中使用 thymeleaf、shiro 的标签了

例如:

1、判断当前用户有无此权限,通过权限标识

<button class="layui-btn" shiro:hasPermission="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button>

2、与上面相反,判断当前用户无此权限,通过权限标识,没有时验证通过

<button class="layui-btn" shiro:lacksPermission="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button>

3、判断当前用户有无以下全部权限,通过权限标识

<button class="layui-btn" shiro:hasAllPermissions="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button>

4、判断当前用户有无以下任一权限,通过权限标识

<button class="layui-btn" shiro:hasAnyPermissions="user_info_add"><i class="layui-icon">&#xe654;</i> 新增 </button>

5、判断当前用户有无此角色,通过角色标识

<a shiro:hasRole="admin" href="admin.html">Administer the system</a>

6、与上面相反,判断当前用户无此角色,通过角色标识,没有时验证通过

<a shiro:lacksRole="admin" href="admin.html">Administer the system</a>

7、判断当前用户有无以下全部角色,通过角色标识

<a shiro:hasAllRoles="admin,role1,role2" href="admin.html">Administer the system</a>

8、判断当前用户有无以下任一角色,通过角色标识

<a shiro:hasAnyRoles="admin,role1,role2" href="admin.html">Administer the system</a>

以上,就是 SpringBoot 中整合 Shiro 实现权限管理的全部内容,希望能够帮助到正在阅读此博客的你

如您在阅读中发现不足,欢迎留言!!!

已标记关键词 清除标记
springboot 前后端分离使用shiro进行权限控制并使用cors进行跨域 本来框架已经搭好了并且持续运行了一段时间前端已经能够拿到后台传过去的数据,cors配置 ``` @Configuration //@EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://192.168.1.141:8080","http://127.0.0.1:8080") .allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS") .allowCredentials(true).maxAge(3600); } /*private CorsConfiguration buildConfig(){ CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("http://192.168.1.141:8080"); config.addAllowedOrigin("http://127.0.0.1:8080"); config.addAllowedHeader("*"); config.addAllowedMethod(HttpMethod.GET); config.addAllowedMethod(HttpMethod.POST); config.addAllowedMethod(HttpMethod.PUT); config.addAllowedMethod(HttpMethod.DELETE); config.addAllowedMethod(HttpMethod.OPTIONS); return config; } @Bean public CorsFilter corsFilter(){ UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(configSource); }*/ } ``` 但是今天前端请求菜单接口的时候报302问题(注:今天之前请求菜单接口是可以跨域返回数据的),不明觉厉,弄了很久还是没有弄好。 前端也加了 ``` xhrFields: { withCredentials: true }, crossDomain: true, ``` 可是为什么后台突然之间就获取不到cookies了,重点是突然,因为之前都是可以的
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页