spring boot + shiro 实现角色权限控制

spring boot + shiro 实现角色权限控制

简介

Apache Shiro 是一个强大并且易于使用的java安全框架,可以用与身份验证、授权、加密和会话管理。同样的框架还有spring security,spring security有很好的平台支持,和活跃的社区氛围,并且对spring完美兼容,但是使用难度上,远远超过shiro。

  • 身份认证:用户身份识别。
  • 授权:用户权限控制。知道来的人有没有资格进来。
  • 加密:加密敏感数据,防止偷窥。比如密码md5两次加密。
  • 会话管理:用户的时间敏感的状态信息。

shiro上手快 ,控制粒度可糙可细 ,自由度高,可以独立运行。

类说明

类名 说明
Subject 可以理解为与shiro打交道的对象,该对象封装了一些对方的信息,shiro可以通过subject拿到这些信息
SecurityManager Shiro的总经理,通过指使Authorizer和Authenticator等对subject进行授权和身份验证等工作
Realm 管理着一些如用户、角色、权限等重要信息,Shiro中所需的这些重要信息都是从Realm这里获取的,Realm本质上就是一个重要信息的数据源
Authenticator 认证器,负责Subject的认证操作,认证过程就是根据Subject提供的信息通过Realm查询到相关信息,然后做对比,支持扩展
Authorizer 授权器,控制着Subject对服务资源的访问权限
SessionManager 用于管理Session,这个Session可以是web的也可以不是web的
SessionDao 把Session的 CRUD和存储介质联系起来的工具,存储介质可以是数据库,也可以是缓存,比如把session放到redis里面
CacheManager 缓存控制器,Realm管理的数据(用户、角色、权限)可以放到缓存里由CacheManager管理,提高认证授权等的速度
Cryptography 加密组件,Shiro提供了很多加解密算法的组件

Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)代表Shiro应用安全的四大基石。

实践开发

开发环境

  • spring boot 2.3.0.RELEASE
  • maven 3.6.0
  • jdk 1.8
  • mysql 5.7
  • intellij idea

设计技术

spring boot + shiro + swagger + mybatis

maven


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.3.0.RELEASEversion>
        <relativePath/> 
    parent>
    <groupId>com.felton.springbootgroupId>
    <artifactId>shrioartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>shrioname>
    <description>Demo project for Spring Bootdescription>

    <properties>
        <java.version>1.8java.version>
        <mysql.version>5.1.46mysql.version>
    properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starterartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-springartifactId>
            <version>1.4.2version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>${mysql.version}version>
        dependency>
        <dependency>
            <groupId>org.mybatis.generatorgroupId>
            <artifactId>mybatis-generator-coreartifactId>
            <version>1.3.5version>
        dependency>

        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger2artifactId>
            <version>2.9.2version>
        dependency>
        <dependency>
            <groupId>io.springfoxgroupId>
            <artifactId>springfox-swagger-uiartifactId>
            <version>2.9.2version>
        dependency>

        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>1.3.2version>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
            <plugin>
                <groupId>org.mybatis.generatorgroupId>
                <artifactId>mybatis-generator-maven-pluginartifactId>
                <version>1.3.5version>
                <configuration>
                    
                    <configurationFile>${basedir}/src/main/resources/mybatis-generator.xmlconfigurationFile>
                    <overwrite>trueoverwrite>
                    <verbose>trueverbose>
                configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysqlgroupId>
                        <artifactId>mysql-connector-javaartifactId>
                        <version>${mysql.version}version>
                    dependency>
                dependencies>
            plugin>
        plugins>
    build>

project>

application.yml配置

主要是数据库配置

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/shrio?useUnicode=true&characterEncoding=utf-8&useSSL=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver


#mybatis
mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

数据库脚本

数据库表结构,标准的RBAC用户角色权限结构。

spring boot + shiro 实现角色权限控制_第1张图片

/*
 Navicat Premium Data Transfer

 Source Server         : 本地
 Source Server Type    : MySQL
 Source Server Version : 50729
 Source Host           : localhost:3306
 Source Schema         : shrio

 Target Server Type    : MySQL
 Target Server Version : 50729
 File Encoding         : 65001

 Date: 16/06/2020 15:33:00
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for permission
-- ----------------------------
DROP TABLE IF EXISTS `permission`;
CREATE TABLE `permission`  (
  `permission_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `permission_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称',
  `resource_type` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源类型,[menu|button]',
  `url` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源路径',
  `permission` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view',
  `parent_id` int(11) NULL DEFAULT NULL COMMENT '父编号',
  `available` int(255) NULL DEFAULT NULL COMMENT '是否可用,如果不可用将不会添加给用户',
  PRIMARY KEY (`permission_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of permission
-- ----------------------------
INSERT INTO `permission` VALUES (1, '用户管理', 'menu', 'user/userList', 'user:view', 0, 1);
INSERT INTO `permission` VALUES (2, '用户添加', 'button', 'user/userAdd', 'user:add', 1, 1);
INSERT INTO `permission` VALUES (3, '用户删除', 'button', 'user/userDel', 'user:del', 1, 1);

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
  `role` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色标识程序中判断使用,如\"admin\",这个是唯一的:',
  `description` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述,UI界面显示使用',
  `available` int(255) NULL DEFAULT NULL COMMENT '是否可用,如果不可用将不会添加给用户',
  PRIMARY KEY (`role_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'admin', '管理员', 1);

-- ----------------------------
-- Table structure for role_permission_relation
-- ----------------------------
DROP TABLE IF EXISTS `role_permission_relation`;
CREATE TABLE `role_permission_relation`  (
  `permission_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`permission_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role_permission_relation
-- ----------------------------
INSERT INTO `role_permission_relation` VALUES (1, 1);
INSERT INTO `role_permission_relation` VALUES (2, 1);

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '登录用户名',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '名称(昵称或者真实姓名,根据实际情况定义)',
  `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
  `state` int(255) NULL DEFAULT NULL COMMENT '用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.',
  `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
  `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `expired_date` datetime(0) NULL DEFAULT NULL COMMENT '过期日期',
  PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', '管理员', '597042ee35c3a0a937c9ef4afff842f2', 1, '2020-06-10 16:17:04', '[email protected]', '2020-06-17 16:17:16');

-- ----------------------------
-- Table structure for user_role_relation
-- ----------------------------
DROP TABLE IF EXISTS `user_role_relation`;
CREATE TABLE `user_role_relation`  (
  `user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  PRIMARY KEY (`user_id`, `role_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role_relation
-- ----------------------------
INSERT INTO `user_role_relation` VALUES (1, 1);

SET FOREIGN_KEY_CHECKS = 1;

目录结构

spring boot + shiro 实现角色权限控制_第2张图片

这里使用了mbg自动生成model、dao、mapper。

配置realm

添加一个MyShiroRealm类,并继承AuthorizingRealm,并且需要实现两个方法。

  • doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。

  • doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。

package com.felton.springboot.shrio.config;

import com.felton.springboot.shrio.entity.Permission;
import com.felton.springboot.shrio.entity.Role;
import com.felton.springboot.shrio.entity.User;
import com.felton.springboot.shrio.service.PermissionService;
import com.felton.springboot.shrio.service.RoleService;
import com.felton.springboot.shrio.service.UserService;
import jdk.nashorn.internal.parser.Token;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import java.util.List;

/**
 * 类  名:com.felton.springboot.shrio.config.MyShrioRealm
 * 类描述:todo
 * 创建人:liurui
 * 创建时间:2020/6/10 11:23
 * 修改人:
 * 修改时间:
 * 修改备注:
 *
 * @author liurui
 * @version 1.0
 */
public class MyShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Autowired
    private RoleService roleservice;

    @Autowired
    private PermissionService permissionService;


    /**
     * 权限信息
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo  authorizationInfo = new SimpleAuthorizationInfo();
        User user = (User) principalCollection.getPrimaryPrincipal();

        List<Role> roleList = roleservice.getRoleListByUserName(user.getUserName());
        List<Permission> permissionList = null;
        if (roleList.size() > 0) {
            //添加角色
            for (Role role : roleList) {
                authorizationInfo.addRole(role.getRole());
                permissionList = permissionService.getPermissionListByRoleId(role.getRoleId());
                for (Permission permission : permissionList) {
                    //添加权限
                    authorizationInfo.addStringPermission(permission.getPermission());
                }
            }
        }

        return authorizationInfo;
    }

    /**
     * 身份认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("【MyShiroRealm】身份认证");
        String userName = (String) authenticationToken.getPrincipal();
        User user = userService.findByUserName(userName);
        if (user == null) {
            return null;
        }
        if (user.getState() == 0) {
            // 用户被管理员锁定抛出异常
            throw new AuthenticationException();
        }

        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                getName()
        );
        return authenticationInfo;
    }


}

配置shiro

这里使用的是spring boot,所以抛弃传统的xml配置,改为编写配置类ShiroConfig。在项目启动的时候,实现方法会给注入到spring容器中。

package com.felton.springboot.shrio.config;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

/**
 * 类  名:com.felton.springboot.shrio.config.ShiroConfig
 * 类描述:todo
 * 创建人:liurui
 * 创建时间:2020/6/10 11:24
 * 修改人:
 * 修改时间:
 * 修改备注:
 *
 * @author liurui
 * @version 1.0
 */
@Configuration
public class ShiroConfig {

    /**
     * 将自己的验证方式加入容器
     * @return
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }

    /**
     * 权限管理,配置主要是realm的管理认证
     * @return
     */
    @Bean
    DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(myShiroRealm());
        return manager;
    }

    /**
     * 凭证匹配器(密码校验交给Shiro的SimpleAuthenticationInfo进行处理)
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//        加密方式
        hashedCredentialsMatcher.setHashAlgorithmName("md5");
        // 加密两次
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager());
        Map<String, String> filterMap = new HashMap<>();
        // 登出
        filterMap.put("/logout", "logout");
        // 对所有用户认证
        filterMap.put("/**", "authc");
        filterMap.put("/swagger**/**", "anon");
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/v2/**", "anon");
        // 登录
        bean.setLoginUrl("/login");
        // 首页
        bean.setSuccessUrl("/index");
        // 未授权页面,认证不通过跳转
        bean.setUnauthorizedUrl("/403");

        bean.setFilterChainDefinitionMap(filterMap);
        return bean;

    }

    /**
     * 开启shiro aop 注解支持
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }


    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
        System.out.println("错误");
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        mappings.setProperty("DatabaseException", "databaseError");
        mappings.setProperty("UnauthorizedException", "/403");
        resolver.setExceptionMappings(mappings);
        resolver.setDefaultErrorView("error");
        resolver.setExceptionAttribute("exception");
        return resolver;
    }

}

service层的简单使用

package com.felton.springboot.shrio.service.impl;

import com.felton.springboot.shrio.model.LoginResult;
import com.felton.springboot.shrio.service.LoginService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Service;


/**
 * 类  名:com.felton.springboot.shrio.service.impl.LoginServiceImpl
 * 类描述:todo
 * 创建人:liurui
 * 创建时间:2020/6/10 11:08
 * 修改人:
 * 修改时间:
 * 修改备注:
 *
 * @author liurui
 * @version 1.0
 */
@Service
public class LoginServiceImpl implements LoginService {

    @Override
    public LoginResult login(String userName, String password) {
        LoginResult result = new LoginResult();
        if (userName == null || userName.isEmpty()) {
            result.setLogin(false);
            result.setResult("用户名为空");
            return result;
        }

        String msg = "";

        UsernamePasswordToken token = new UsernamePasswordToken(userName, password);

        try {
            Subject currentUser = SecurityUtils.getSubject();
            currentUser.login(token);
            Session session = currentUser.getSession();
            // 设置会话session
            session.setAttribute("userName", userName);
            result.setLogin(true);
            return result;

        } catch (Exception e) {
            e.printStackTrace();
        }
        result.setLogin(false);
        result.setResult("");

        return result;
    }

    @Override
    public void logout() {
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
    }
}

大致的代码都在这里,如果需要完整的项目地址可以到这里

代码量也不算很多,但是要真正的理解shiro的执行过程还需要对其源码进行分析。简单的走了一下代码的执行过程。

用户的每一次请求都会执行自定义的AuthorizingRealm类,如果没有session记录的会执行身份认证doGetAuthenticationInfo,如果已经登录并且有session会话记录,则直接执行权限认证doGetAuthorizationInfo

登录过程

spring boot + shiro 实现角色权限控制_第3张图片

执行过程

spring boot + shiro 实现角色权限控制_第4张图片

权限判断

如果在数据库中匹配到指定的权限,返回true值,由于每次访问接口都会触发doGetAuthorizationInfo方法,从而会经常性的查找数据库,如果用户数量大,查找权限多,会导致接口响应慢,这里可以改造使用redis缓存数据库来缓存权限信息。

spring boot + shiro 实现角色权限控制_第5张图片

无权限

如果用户无权限,isPermitted会返回false,ModularRealmAuthorizer中会抛出无权限异常UnauthorizedException,而在ShiroConfig.java中已配置UnauthorizedException的异常捕获,并重定向到/403接口中。

spring boot + shiro 实现角色权限控制_第6张图片

FAQ

shiro的注解使用

推荐一篇文章,里边说了很明白,地址

主要用到的是@RequiresPermissions,注解中的值分为三种形式

  • 普通形式
    value只是一个普通的字符串,例如 @RequiresPermissions(“action”)
  • 多层形式
    value中的字符串,使用冒号:来分割字符串的内容,例如:@RequiresPermissions(“user:add”)
    一般第一个字符串是操作对象的权限领域,第二的是操作的权限类型
  • 多权限多层心事
    value由多个多层形式的字符串组成,例如:@RequiresPermissions(“user:view”,"user:add)

你可能感兴趣的:(java,spring,boot)