Shiro安全框架

一、什么是Shiro

Apache Shiro™是一个功能强大且易于使用的Java安全框架,用于执行身份验证,授权,加密和会话管理。使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序-从最小的移动应用程序到最大的Web和企业应用程序。简答你来说shiro就是一个可以处理身份验证、授权、加密和会话管理的开源安全框架

二、Shiro的特点

  • 易于使用——易用性是项目的最终目标。应用程序安全非常令人困惑和沮丧,被认为是“不可避免的灾难”。如果你让它简化到新手都可以使用它,它就将不再是一种痛苦了。
  • 全面——没有其他安全框架的宽度范围可以同Apache Shiro一样,它可以成为你的“一站式”为您的安全需求提供保障。
  • 灵活——Apache Shiro可以在任何应用程序环境中工作。虽然在网络工作、EJB和IoC环境中可能并不需要它。但Shiro的授权也没有任何规范,甚至没有许多依赖关系。
  • Web支持——Apache Shiro拥有令人兴奋的web应用程序支持,允许您基于应用程序的url创建灵活的安全策略和网络协议(例如REST),同时还提供一组JSP库控制页面输出。
  • 低耦合——Shiro干净的API和设计模式使它容易与许多其他框架和应用程序集成。你会看到Shiro无缝地集成Spring这样的框架, 以及Grails, Wicket, Tapestry, Mule, Apache Camel, Vaadin…等。
  • 被广泛支持——Apache Shiro是Apache软件基金会的一部分。项目开发和用户组都有友好的网民愿意帮助。这样的商业公司如果需要Katasoft还提供专业的支持和服务。

三、Shiro具有的功能

image-20210208121130012

Shiro以Shiro开发团队所谓的“应用程序安全性的四个基石”为目标-身份验证(Authentication),授权(Authorization),会话管理(Session Management)和加密(Cryptography)

  • 身份验证(Authentication):有时称为“登录”,用于识别用户的身份
  • 授权(Authorization):访问控制的过程中对用户进行授权
  • 会话管理(Session Management):特定于用户的会话管理,即使在非web 或 EJB 应用程序。
  • 加密(Cryptography):使用密码算法保持数据安全,同时仍易于使用。

在不同的应用程序环境中,还具有其他功能来支持和加强这些问题,尤其是:

  • Web支持:Shiro的Web支持API可帮助轻松保护Web应用程序。
  • 缓存:缓存是Apache Shiro API的第一层公民,可确保安全操作保持快速有效。
  • 并发性:Apache Shiro的并发功能支持多线程应用程序。
  • 测试:测试支持可以帮助您编写单元测试和集成测试,并确保您的代码将按预期进行保护。
  • “运行方式”:一种功能,允许用户采用其他用户的身份(如果允许),有时在管理方案中很有用。
  • “记住我”:在整个会话中记住用户的身份,因此他们仅在必要时登录。

四、Shiro的体系结构

在Shiro最高层次的概念中shiro有3个主要的概念Subject,SecurityManager,Realms

image-20210208123441633

  • Subject(org.apache.shiro.subject.Subject):应用代码的直接交互对象就是Subject,也就是说Shiro对外的核心API就是Subject,Subject代表了当前“用户”,这个用户不是指具体的某一个人,可以说与当前应用交互的任何东西都是Subject,与Subject的所有交互都会委托给SecurityManager来执行,可以理解为Subject只是一个充当门面的,真正的幕后老大是SecurityManager,SecurityManager才是实际的执行者。
  • SecurityManager(org.apache.shiro.mgt.SecurityManager):SecurityManager是Shiro体系结构的核心,可协调其内部安全组件,所有与安全有关的操作都会与SecurityManager进行交互,并且SecurityManager管理所有的Subject。
  • Realm(org.apache.shiro.realm.Realm):用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法,也需要从Realm获取用户的角色\权限来判断用户是否能进行一系列操作。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。

shiro的详细架构图

image-20210208205138014

  • Authenticator:Authenticator是一个对执行及对用户的身份验证(登录)尝试负责的组件。当一个用户尝试登录时,该逻辑被 Authenticator执行。Authenticator知道如何与一个或多个Realm协调来存储相关的用户/帐户信息。从这些Realm中获得的数据被用来验证用户的身份来保证用户确实是他们所说的他们是谁。
  • Authentication Strategy(org.apache.shiro.authc.pam.AuthenticationStrategy):如果不止一个Realm被配置,则AuthenticationStrategy将会协调这些Realm来决定身份认证尝试成功或失败下的条件(例如,如果一个Realm成功,而其他的均失败,是否该尝试成功?是否所有的Realm必须成功?或只有第一个成功即可?)。
  • Authorizer(org.apache.shiro.authz.Authorizer):Authorizer是负责在应用程序中决定用户的访问控制的组件。它是一种最终判定用户是否被允许做某事的机制。与 Authenticator相似,Authorizer还知道如何与多个后端数据源进行协调以访问角色和权限信息。在Authorizer使用该信息来准确确定是否允许用户执行特定的操作。
  • SessionManager:SessionManager知道如何去创建及管理用户Session生命周期来为所有环境下的用户提供一个强健的Session体验。这在安全框架界是一个独有的特色——Shiro拥有能够在任何环境下本地化管理用户Session的能力,即使没有可用的Web/Servlet或EJB容器,它将会使用它内置的企业级会话管理来提供同样的编程体验。SessionDAO的存在允许任何数据源能够在持久会话中使用。
  • SessionDAO(org.apache.shiro.session.mgt.eis.SessionDAO):SesssionDAO代表SessionManager执行Session持久化(CRUD)操作。这允许任何数据存储被插入到会话管理的基础之中。
  • CacheManager(org.apahce.shiro.cache.CacheManager):CacheManager创建并管理其他Shiro组件使用的Cache实例生命周期。因为Shiro可以访问许多后端数据源以进行身份验证,授权和会话管理,所以缓存一直是框架中的一流架构功能,可以在使用这些数据源时提高性能。可以将任何现代的开源和/或企业缓存产品插入Shiro,以提供快速有效的用户体验。
  • Cryptography(org.apache.shiro.crypto.*):Cryptography是对企业安全框架的一个很自然的补充。Shiro的crypto包包含大量易于使用和理解的cryptographic Ciphers,Hasher(又名digests)以及不同的编码器实现的代表。Shiro的加密API简化了复杂的Java机制,并使加密技术易于为普通凡人使用。

五、Shiro简单使用

新建maven项目,导入shiro依赖

		<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.7.1</version>
        </dependency>

自定义加密工具类

public class EncodeUtils {
    /**
     * 加密方法,MD5加密虽然是不可逆的但是相同的密码经过MD5加密后的只是一样的,所以这里我们可以使用加盐的方式	同时多次MD5加密的方式,增加密码破解的难度
     * @param username
     * @param password
     * @return
     */
    public static String encode(String username, String password) {
        //加密方式为MD5
        String algorithmName = "MD5";

        //第三个参数:盐值(这个盐是 username)
        ByteSource salt = ByteSource.Util.bytes(username);

        //加密的次数,可以进行多次的加密操作
        int hashIterations = 1;

        //通过SimpleHash 来进行加密操作
        SimpleHash hash = new SimpleHash(algorithmName, password, salt, hashIterations);;
        return hash.toString();
    }
}

自定义Realm类进行权限信息验证和授权

 public class MyRealm extends AuthorizingRealm {


    /**
     * 模拟数据库数据
     */
    Map<String, String> userMap = new HashMap<>(16);

    {
        String password = EncodeUtils.encode("lisi", "123456");
        userMap.put("lisi", password);
        // 设置自定义Realm的名称
        super.setName("myRealm");
    }
     
    /**
     * 授权
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String userName = (String) principalCollection.getPrimaryPrincipal();
        // 根据当前用户从数据库获取角色和权限数据
        Set<String> roles = getRolesByUserName(userName);
        Set<String> permissions = getPermissionsByUserName(userName);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 模拟从数据库中获取权限数据
     *
     * @param userName
     * @return
     */
    private Set<String> getPermissionsByUserName(String userName) {
        Set<String> permissions = new HashSet<>();
        permissions.add("user:delete");
        permissions.add("user:add");
        return permissions;
    }

    /**
     * 模拟从数据库中获取角色数据
     *
     * @param userName
     * @return
     */
    private Set<String> getRolesByUserName(String userName) {
        Set<String> roles = new HashSet<>();
        roles.add("admin");
        roles.add("user");
        return roles;
    }

    /**
     * 认证
     *
     * @param authenticationToken 主体传过来的认证信息
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1.从主体传过来的认证信息中,获得用户名
        String userName = (String) authenticationToken.getPrincipal();
        // 2.通过用户名到数据库中获取凭证
        String password = getPasswordByUserName(userName);
        if (password == null) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, password,"myRealm");
        return authenticationInfo;
    }

    /**
     * 模拟从数据库取凭证的过程
     *
     * @param userName
     * @return
     */
    private String getPasswordByUserName(String userName) {
        return userMap.get(userName);
    }
}

创建测试类ShiroTest

public class ShiroTest {
    public static void main(String[] args) {
        
        MyRealm myRealm = new MyRealm();
		
        // 创建SecurityManager对象,并将自定义的Realm注入
        DefaultSecurityManager manager = new DefaultSecurityManager();
        manager.setRealm(myRealm);

        // 设置SecurityManager
        SecurityUtils.setSecurityManager(manager);
		
        // 获取Subject主题
        Subject subject = SecurityUtils.getSubject();

        String username = "lisi";
        String password = "123456";
        String encodePassword = EncodeUtils.encode(username, password);
        UsernamePasswordToken token = new UsernamePasswordToken(username, encodePassword);

        // login会调用realm的doGetAuthenticationInfo()方法进行登录认证
        subject.login(token);

        // subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
        System.out.println("isAuthenticated:" + subject.isAuthenticated());
        // 判断subject是否具有admin和user两个角色权限,如没有则会报错
        // checkRoles 会调用realm的doGetAuthorizationInfo()方法,要验证几个角色就会调用几下
        // 这里检查admin和user两个角色就会调用两次realm的doGetAuthorizationInfo()方法
        subject.checkRoles("admin", "user");

        // 判断subject是否具有user:add权限
        // checkRoles同样会调用realm的doGetAuthorizationInfo()方法,要验证几个权限就会调用几下
        // 这里检查一次user:add权限,就会调用一次realm的doGetAuthorizationInfo
        subject.checkPermissions("user:add");
             // 退出登录
        subject.logout();
        System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
    }
}

以上代码利用shiro进行了简单的登录和授权

六、Shiro的认证过程和授权过程


认证过程

image-20210208220921277

  1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
  2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
  3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
  4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
  5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 或者 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

shiro授权过程

image-20210208221300636

1、首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer;
2、Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
3、在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;
4、Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole会返回true,否则返回false表示授权失败。

七、SpringBoot整合Shiro

新建SpringBoot工程pom依赖如下:

     <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

application.yaml配置如下

#缓存设置为false, 这样修改之后马上生效,便于调试
spring:
    thymeleaf:
      cache: false

    datasource:
      url: jdbc:mysql://127.0.0.1:3306/shiro?serverTimezone=UTC
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: root
mybatis:
  mapper-locations: classpath:/mapper/**/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

整个项目目录如下:image-20210209174635968

config包下编写ShiroConfig配置类,配置shiro

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        System.out.println("ShiroConfiguration.shirFilter()");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 过滤器链
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");

        // 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
        filterChainDefinitionMap.put("/logout", "logout");

        // <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
        //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->,/** 一定要放在最后边
        filterChainDefinitionMap.put("/**","authc");


        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");

        // 登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");

        //未授权界面;
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");


        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");
        // 散列的次数,比如散列两次,相当于 md5(md5(""));
        hashedCredentialsMatcher.setHashIterations(2);
        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm myShiroRealm = new MyShiroRealm();
        myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myShiroRealm;
    }


    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }

    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     *
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     *开启shiro aop注解支持
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator app=new DefaultAdvisorAutoProxyCreator();
        app.setProxyTargetClass(true);
        return app;
    }

    @Bean(name = "simpleMappingExceptionResolver")
    public SimpleMappingExceptionResolver createSimpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
        Properties mappings = new Properties();
        // 数据库异常处理
        mappings.setProperty("DatabaseException", "databaseError");
        mappings.setProperty("UnauthorizedException", "403");
        // None by default
        r.setExceptionMappings(mappings);
        // No default
        r.setDefaultErrorView("error");
        // Default is "exception"
        r.setExceptionAttribute("ex");
        //r.setWarnLogCategory("example.MvcLogger");     // No default
        return r;
    }
}

关于Shiro的拦截器,Shiro1.7中DefaultFilter枚举定义了13个过滤器

image-20210209212128513

  • anon---------------org.apache.shiro.web.filter.authc.AnonymousFilter 没有参数,表示可以匿名使用。
  • authc--------------org.apache.shiro.web.filter.authc.FormAuthenticationFilter 表示需要认证(登录)才能使用,没有参数
  • authcBasic---------org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter 没有参数,要求用户必须经过身份认证,如果没有则要求用户通过特定于HTTP Basic协议认证
  • perms--------------org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter 参数可以写多个,并且参数之间用逗号分割,例如filterChainDefinitionMap.put("/ceshi/**","perms[admin,admin2]");,当有多个参数时必须每个参数都通过才通过,相当于isPermitedAll()方法。
  • port---------------org.apache.shiro.web.filter.authz.PortFilter port[8080],过滤请求端口当请求的url的端口不是8080则重定向到8080端口的相同url上*
  • rest---------------org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter:它将HTTP请求的方法转换为相应的动作并使用该动做来构造权限。例如 /user/**=rest[user],如果是post请求则会构造权限user:create,put请求则会构造权限user:update,delete请求则会构造权限user:delete,get请求则会构造权限user:read然后执行 Subject.isPermitted("user:create")或者Subject.isPermitted("user:update")或者Subject.isPermitted("user:delete")或者Subject.isPermitted("user:read")
  • roles--------------org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 参数可以写多个,参数之间用逗号分割,同perms类似一个是判断角色一个是判断权限
  • ssl----------------org.apache.shiro.web.filter.authz.SslFilter 没有参数,表示安全的url请求,协议为https
  • user---------------org.apache.shiro.web.filter.authz.UserFilter 没有参数表示必须存在用户,当登入操作时不做检查user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求
  • InvalidRequestFilter--------------- org.apache.shiro.web.filter.InvalidRequestFilter阻止非法请求,例如请求中带有逗号,反斜杠

entity包

新建数据库表,插入测试数据

/*
 Navicat Premium Data Transfer

 Source Server         : localMysql
 Source Server Type    : MySQL
 Source Server Version : 50731
 Source Host           : localhost:3306
 Source Schema         : shiro

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

 Date: 09/02/2021 17:21:19
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` bigint(20) NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, '查询用户', 'userInfo:view', '/userList');
INSERT INTO `sys_permission` VALUES (2, '增加用户', 'userInfo:add', '/userAdd');
INSERT INTO `sys_permission` VALUES (3, '删除用户', 'userInfo:delete', '/userDelete');

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` bigint(20) NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

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

-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `role_id` bigint(20) NOT NULL,
  `permission_id` bigint(20) NOT NULL,
  INDEX `FKomxrs8a388bknvhjokh440waq`(`permission_id`) USING BTREE,
  INDEX `FK9q28ewrhntqeipl1t04kh1be7`(`role_id`) USING BTREE,
  CONSTRAINT `FK9q28ewrhntqeipl1t04kh1be7` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FKomxrs8a388bknvhjokh440waq` FOREIGN KEY (`permission_id`) REFERENCES `sys_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

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

-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `uid` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  INDEX `FKhh52n8vd4ny9ff4x9fb8v65qx`(`role_id`) USING BTREE,
  INDEX `FKgkmyslkrfeyn9ukmolvek8b8f`(`uid`) USING BTREE,
  CONSTRAINT `FKgkmyslkrfeyn9ukmolvek8b8f` FOREIGN KEY (`uid`) REFERENCES `user_info` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
  CONSTRAINT `FKhh52n8vd4ny9ff4x9fb8v65qx` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

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

-- ----------------------------
-- Table structure for user_info
-- ----------------------------
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info`  (
  `id` bigint(20) NOT NULL,
  `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `UK_f2ksd6h8hsjtd57ipfq9myr64`(`username`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_info
-- ----------------------------
INSERT INTO `user_info` VALUES (1, '管理员', '951cd60dec2104024949d2e0b2af45ae', 'xbNIxrQfn6COSYn1/GdloA==', 'zhangsan');

SET FOREIGN_KEY_CHECKS = 1;

与表对应的5个实体类

@Data
public class SysPermission implements Serializable {

    private static final long serialVersionUID = 1L;


    private Long id;


    private String description;


    private String name;


    private String url;

    List<SysRole> roles;
}


@Data
public class SysRole implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String description;

    private String name;

    List<UserInfo> userInfos;

    List<SysPermission> permissions;
}

@Data
public class SysRolePermission implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long roleId;

    private Long permissionId;


}

@Data
public class SysUserRole implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long uid;

    private Long roleId;


}

@Data
public class UserInfo implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String nickname;

    private String password;

    private String salt;

    private String username;

    List<SysRole> roles;

}
  • 自定义Realm实现验证和授权两个方法

    public class MyShiroRealm extends AuthorizingRealm {
    
        @Resource
        private UserInfoService userInfoService;
    
        /**
         * 用户授权
         * @param principals
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
            // 能进入这里说明用户已经通过验证了
            UserInfo userInfo = (UserInfo) principals.getPrimaryPrincipal();
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
            for (SysRole role : userInfo.getRoles()) {
                simpleAuthorizationInfo.addRole(role.getName());
                for (SysPermission permission : role.getPermissions()) {
                    simpleAuthorizationInfo.addStringPermission(permission.getName());
                }
            }
            return simpleAuthorizationInfo;
        }
    
        /**
         * 用户验证
         * @param token
         * @return
         * @throws AuthenticationException
         */
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            // 获取用户输入的账户
            String username = (String) token.getPrincipal();
            System.out.println(token.getPrincipal());
            // 通过username从数据库中查找 UserInfo 对象
            // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
            UserInfo userInfo = userInfoService.findByUsername(username);
            if (null == userInfo) {
                return null;
            }
    
            SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
                    userInfo,
                    userInfo.getPassword(),
                    ByteSource.Util.bytes(userInfo.getSalt()),
                    getName()
            );
            return simpleAuthenticationInfo;
        }
    }
    

dao层

@Mapper
public interface UserInfoMapper {

    /**
     * 查找用户信息
     * @param username
     * @return
     */
    UserInfo findByUsername(@Param("username") String username);
}

UserInfoMapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.shiro.dao.UserInfoMapper">
    <resultMap id = "userInfoMap" type = "com.example.shiro.entity.UserInfo">
        <id column="uid"  property="id" />
        <result column="nickname" property="nickname"/>
        <result column="password" property="password"/>
        <result column="salt" property="salt"/>
        <result column="username" property="username"/>
        <collection property = "roles" resultMap="roleMap" column="uid"/>
    </resultMap>

    <resultMap id = "roleMap" type = "com.example.shiro.entity.SysRole">
        <id column="role_id" property="id"/>
        <result column="role_description" property="description"/>
        <result column="role_name" property="name"/>
        <collection property = "permissions" resultMap="permissionMap" column="role_id"/>
    </resultMap>

    <resultMap id = "permissionMap" type = "com.example.shiro.entity.SysPermission">
        <id column="permission_id" property="id"/>
        <result column="permission_description" property="description"/>
        <result column="permission_name" property="name"/>
        <result column="url" property="url"/>
    </resultMap>

    <select id="findByUsername" resultMap="userInfoMap">
        SELECT
            ui.id AS uid, ui.nickname, ui.password, ui.salt, ui.username,
            sr.id AS role_id, sr.description AS role_description, sr.name AS role_name ,
            sp.id As permission_id, sp.description AS permission_description, sp.name AS permission_name, sp.url
        FROM 
        	user_info ui
        LEFT JOIN 
        	sys_user_role sur ON ui.id = sur.uid
        LEFT JOIN 
        	sys_role sr ON sur.role_id = sr.id
        LEFT JOIN 
        	sys_role_permission srp ON sr.id = srp.role_id
        LEFT JOIN 
        	sys_permission sp ON srp.permission_id = sp.id
        WHERE 
        ui.username = #{username}
    </select>
</mapper>

service层

public interface UserInfoService{
    /**
     * 根据用户名查找用户信息
     * @param username
     * @return
     */
  UserInfo  findByUsername(String username);
}

@Service
public class UserInfoServiceImpl implements UserInfoService{

    @Resource
    UserInfoMapper userInfoMapper;

    @Override
    public UserInfo findByUsername(String username) {
        return userInfoMapper.findByUsername(username);
    }
}

controller层两个controller,用shiro注解控制访问

@Controller
public class HomeController {


    @RequestMapping({"/","/index"})
    public String index(){
        return"/index";
    }

    @RequestMapping("/login")
    public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
        System.out.println("HomeController.login()");
        // 登录失败从request中获取shiro处理的异常信息。
        // shiroLoginFailure:就是shiro异常类的全类名.
        String exception = (String) request.getAttribute("shiroLoginFailure");
        System.out.println("exception=" + exception);
        String msg = "";
        if (exception != null) {
            if (UnknownAccountException.class.getName().equals(exception)) {
                System.out.println("UnknownAccountException -- > 账号不存在:");
                msg = "UnknownAccountException -- > 账号不存在:";
            } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
                System.out.println("IncorrectCredentialsException -- > 密码不正确:");
                msg = "IncorrectCredentialsException -- > 密码不正确:";
            } else if ("kaptchaValidateFailed".equals(exception)) {
                System.out.println("kaptchaValidateFailed -- > 验证码错误");
                msg = "kaptchaValidateFailed -- > 验证码错误";
            } else {
                msg = "else >> "+exception;
                System.out.println("else -- >" + exception);
            }
        }
        map.put("msg", msg);
        // 此方法不处理登录成功,由shiro进行处理
        return "/login";
    }

    @RequestMapping("/403")
    public String unauthorizedRole(){
        System.out.println("------没有权限-------");
        return "403";
    }

}

@Controller
public class UserInfoController {

    @Resource
    UserInfoService userInfoService;

    /**
     * 按username账户从数据库中取出用户信息
     *
     * @param username 账户
     * @return
     */

    @ResponseBody
    @GetMapping("/userList")
    @RequiresPermissions("userInfo:view")
    public UserInfo findUserInfoByUsername(@RequestParam String username) {
        return userInfoService.findByUsername(username);
    }

    /**
     * 简单模拟从数据库添加用户信息成功
     *
     * @return
     */
    @PostMapping("/userAdd")
    @RequiresPermissions("userInfo:add")
    public String addUserInfo() {
        return "userAdd";
    }

    /**
     * 简单模拟从数据库删除用户成功,没有权限返回403
     *
     * @return
     */
    @DeleteMapping("/userDelete")
    @RequiresPermissions("userInfo:delete")
    public String deleteUserInfo() {
        return "userDelete";
    }

    /**
     *
     * 没有权限返回403
     * @return
     */
    @ResponseBody
    @GetMapping("/userInfo")
    @RequiresPermissions("userInfo:Info")
    public String UserInfo() {
        return "userInfo";
    }
}


Shiro中有五个注解用与控制权限

  • @RequiresAuthentication:

    当前Subject必须在当前session中已经过认证。等同于方法subject.isAuthenticated() 结果为true时。

  • @RequiresUser:

    当前Subject必须是应用的用户,subject.isAuthenticated() 结果为true或者subject.isRemembered()结果为true

  • @RequiresGuest:

    验证是否是一个guest的请求,与@RequiresUser完全相反。subject.getPrincipal() 结果为null.

  • @RequiresRoles

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequiresRoles {
    
        /**
         * A single String role name or multiple comma-delimited role names required in order for the method
         * invocation to be allowed.
         */
        String[] value();
        
        /**
         * The logical operation for the permission check in case multiple roles are specified. AND is the default
         * @since 1.1.0
         */
        Logical logical() default Logical.AND; 
    }
    

    当前Subject必须拥有所有指定的角色时(logical = Logical.AND)或者指定的任一角色(logical = Logical.OR)

  • @RequiresPermissions:

    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface RequiresPermissions {
    
        /**
         * The permission string which will be passed to {@link org.apache.shiro.subject.Subject#isPermitted(String)}
         * to determine if the user is allowed to invoke the code protected by this annotation.
         */
        String[] value();
        
        /**
         * The logical operation for the permission checks in case multiple roles are specified. AND is the default
         * @since 1.1.0
         */
        Logical logical() default Logical.AND; 
    
    }
    

    当前Subject必须拥有所有指定的权限时(logical = Logical.AND)或者指定的任一权限(logical = Logical.OR)

templates目录下的静态资源

403页面

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>403错误页</title>
</head>
<body>
权限不足
</body>
</html>

index页面

<!DOCTYPE html>
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
index - 首页
</body>
</html>

login页面

<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="/login" method="post">
    <p>账号:<input type="text" name="username" value="zhangsan"/></p>
    <p>密码:<input type="text" name="password" value="123456"/></p>
    <p><input type="submit" value="登录"/></p>
</form>
</body>
</html>

userAdd页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>用户添加页面</h1>
</body>
</html>

userDelete页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <hi>用户删除页面</hi>
</body>
</html>

userInfo页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>用户信息</title>
</head>
<body>
<div>


    <li>
        彭于晏
    </li>
    <li>
        胡歌
    </li>
    <li>
        霍建华
    </li>
</div>

</body>
</html>

以上为个人笔记,有误请各位大佬指出!

参考文章:https://blog.csdn.net/qi923701/article/details/75224554

参考文章:https://blog.csdn.net/youanyyou/article/details/106812041

参考文章:https://zhuanlan.zhihu.com/p/54176956

参考文章:https://www.jianshu.com/p/ef0a82d471d2

参考文章:https://blog.csdn.net/Emma_Joans/article/details/78212542

posted @ 2021-02-09 23:29  下海搬砖  阅读(249)  评论(0编辑  收藏  举报