SpringBoot 整合Shiro框架

前言

Shiro 是一个强大、简单易用的 Java 安全框架,可使认证、授权、加密,会话过程更便捷,并可为应用提供安全保障。本节课重点介绍下 Shiro 的认证和授权功能。

一、Shiro 三大核心组件

Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。先来看一下它们之间的关系。
在这里插入图片描述

1 ,Subject 为认证主体

包含 Principals 和 Credentials 两个信息。我们看下两者的具体含义。

Principals:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份。

Credentials:代表凭证。常见的有密码,数字证书等等。

说白了,两者代表了需要认证的内容,最常见的便是用户名、密码了。比如用户登录时,通过 Shiro 进行身份认证,其中就包括主体认证。

2, SecurityManager 为安全管理员

这是 Shiro 架构的核心,是 Shiro 内部所有原件的保护伞。项目中一般都会配置 SecurityManager,开发人员将大部分精力放在了 Subject 认证主体上,与 Subject 交互背后的安全操作,则由 SecurityManager 来完成。

3 Realm 是一个域

它是连接 Shiro 和具体应用的桥梁。当需要与安全数据交互时,比如用户账户、访问控制等,Shiro 将会在一个或多个 Realm 中查找。我们可以把 Realm 看作 DataSource,即安全数据源。一般,我们会自己定制 Realm,下文会详细说明。

4 其他核心组件

① UserMangerPasswordToken

shiro用来封装用户登录信息,使用用户的登录信息来创建令牌token

② Subject

shiro的一个抽象概念,包含了用户信息,调用SecurityUtils的getSubject()方法获取sunject

③ AuthenticationInfo

用户的角色信息集合,认证时使用

④ AuthorzationInfo

角色的权限集合,授权时使用

⑤ DefaultWebSecurityMannager

安全管理器,开发者自定义的Realm需要注入到DefaultWebSecurityMannager进行管理才能生效

⑥ ShiroFilterFactoryBean

过滤器工厂,Shiro的基本运行机制是开发者定制规则,Shiro去执行,具体的操作就是由ShiroFilterFactoryBean创建的一个个Filter对象来完成

Shiro运行机制
在这里插入图片描述

二、Shiro 身份和权限认证

1 Shiro 身份认证

我们分析下 Shiro 身份认证的过程,首先看一下官方给出的认证图。
在这里插入图片描述
从图中可以看到,这个过程包括五步。

Step1:应用程序代码调用 Subject.login(token) 方法后,传入代表最终用户身份的 AuthenticationToken 实例 Token。

Step2:将 Subject 实例委托给应用程序的 SecurityManager(Shiro 的安全管理)并开始实际的认证工作。这里开始了真正的认证工作。

Step3、4、5SecurityManager 根据具体的 Realm 进行安全认证。从图中可以看出,Realm 可进行自定义(Custom Realm)。

2 Shiro 权限认证

权限认证,也就是访问控制,即在应用中控制谁能访问哪些资源。在权限认证中,最核心的三个要素是:权限、角色和用户。

权限(Permission):即操作资源的权利,比如访问某个页面,以及对某个模块的数据进行添加、修改、删除、查看操作的权利。
角色(Role):指的是用户担任的角色,一个角色可以有多个权限。
用户(User):在 Shiro 中,代表访问系统的用户,即上面提到的 Subject 认证主体。

它们之间的的关系可以用下图来表示:
在这里插入图片描述
一个用户可以有多个角色,而不同的角色可以有不同的权限,也可有相同的权限。比如说现在有三个角色,1 是普通角色,2 也是普通角色,3 是管理员,角色 1 只能查看信息,角色 2 只能添加信息,管理员对两者皆有权限,而且还可以删除信息。

3 编写认证和授权的规则如下

认证过滤器
anno:无需认证
authc:必须认证
auyhcBasic:需要通过HTTPBasic认证
user:不一定通过认证,只要曾经被shiro记录即可,比如记住我
授权过滤器
perms:必须拥有某个权限才能访问
role:必须拥有某个角色才能访问
port:请求的端口必须指定值才可以
rest:请求必须基于RESTful,POST、PUT、 GET、DELETE
ssl:请求必须是安全的URL请求,协议https

三、Spring Boot 集成 Shiro

1 依赖导入

Spring Boot 2.0.3 集成 Shiro 需要导入如下 starter 依赖:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

2 数据库表的建立及初始化

这里主要涉及到三张表:用户表、角色表和权限表。其实在 Demo 中,我们完全可以自己来模拟数据库操作,不用建表,但为了更加接近实际情况,我们还是引入了 MyBatis 来操作数据库。下面是数据库各表的创建脚本。

CREATE TABLE `t_role` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `rolename` varchar(20) DEFAULT NULL COMMENT '角色名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE `t_user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户主键',
  `username` varchar(20) NOT NULL COMMENT '用户名',
  `password` varchar(20) NOT NULL COMMENT '密码',
  `role_id` int(11) DEFAULT NULL COMMENT '外键关联role表',
  PRIMARY KEY (`id`),
  KEY `role_id` (`role_id`),
  CONSTRAINT `t_user_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8

CREATE TABLE `t_permission` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `permissionname` varchar(50) NOT NULL COMMENT '权限名',
  `role_id` int(11) DEFAULT NULL COMMENT '外键关联role',
  PRIMARY KEY (`id`),
  KEY `role_id` (`role_id`),
  CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8

其中,t_user、t_role 和 t_permission 分别存储了用户信息、角色信息和权限信息,表建立好后,我们往表里插入一些测试数据,比如下面这些数据。

t_user 表:

id	username	password	role_id
1	csdn1	123456	1
2	csdn2	123456	2
3	csdn3	123456	3
t_role 表:

id	rolename
1	admin
2	teacher
3	student
t_permission 表:

id	permissionname	role_id
1	user:*	1

2	student:*	2

解释一下这里的权限:user:* 表示权限可以是 user:create 或其他,* 表示一个占位符,可以自己定义,下文介绍 Shiro 配置时会对其再做详细说明。

3 自定义 Realm

有了数据库表和数据,我们开始自定义 Realm。自定义 Realm 需要继承 AuthorizingRealm 类,该类封装了很多方法,且继承自 Realm 类。

继承 AuthorizingRealm 类后,我们需要重写以下两个方法。

doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。

具体实现如下,相关注解请见代码注释:

/**
 * 自定义realm
 * @author shengwu ni
 */
public class MyRealm extends AuthorizingRealm {

    @Resource
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 获取用户名
        String username = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 给该用户设置角色,角色信息存在 t_role 表中取
        authorizationInfo.setRoles(userService.getRoles(username));
        // 给该用户设置权限,权限信息存在 t_permission 表中取
        authorizationInfo.setStringPermissions(userService.getPermissions(username));
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 根据 Token 获取用户名,如果您不知道该 Token 怎么来的,先可以不管,下文会解释
        String username = (String) authenticationToken.getPrincipal();
        // 根据用户名从数据库中查询该用户
        User user = userService.getByUsername(username);
        if(user != null) {
            // 把当前用户存到 Session 中
            SecurityUtils.getSubject().getSession().setAttribute("user", user);
            // 传入用户名和密码进行身份认证,并返回认证信息
            AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "myRealm");
            return authcInfo;
        } else {
            return null;
        }
    }
}

4 Shiro 配置

自定义 Realm 写好了,接下来需要配置 Shiro。我们主要配置三个东西:自定义 Realm、安全管理器 SecurityManager 和 Shiro 过滤器。

首先,配置自定义的 Realm,代码如下:

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 注入自定义的 Realm
     * @return MyRealm
     */
    @Bean
    public MyRealm myAuthRealm() {
        MyRealm myRealm = new MyRealm();
        logger.info("====myRealm注册完成=====");
        return myRealm;
    }
}

接着,配置安全管理器 SecurityManager:

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 注入安全管理器
     * @return SecurityManager
     */
    @Bean
    public SecurityManager securityManager() {
        // 将自定义 Realm 加进来
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(myAuthRealm());
        logger.info("====securityManager注册完成====");
        return securityManager;
    }
}

配置 SecurityManager 时,需要将上面自定义 Realm 添加进来,这样 Shiro 才可访问该 Realm。

最后,配置 Shiro 过滤器:

@Configuration
public class ShiroConfig {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfig.class);

    /**
     * 注入 Shiro 过滤器
     * @param securityManager 安全管理器
     * @return ShiroFilterFactoryBean
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        // 定义 shiroFactoryBean
        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();

        // 设置自定义的 securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 设置默认登录的 URL,身份认证失败会访问该 URL
        shiroFilterFactoryBean.setLoginUrl("/login");
        // 设置成功之后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/success");
        // 设置未授权界面,权限认证失败会访问该 URL
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        // LinkedHashMap 是有序的,进行顺序拦截器配置
        Map<String,String> filterChainMap = new LinkedHashMap<>();

        // 配置可以匿名访问的地址,可以根据实际情况自己添加,放行一些静态资源等,anon 表示放行
        filterChainMap.put("/css/**", "anon");
        filterChainMap.put("/imgs/**", "anon");
        filterChainMap.put("/js/**", "anon");
        filterChainMap.put("/swagger-*/**", "anon");
        filterChainMap.put("/swagger-ui.html/**", "anon");
        // 登录 URL 放行
        filterChainMap.put("/login", "anon");

        // 以“/user/admin” 开头的用户需要身份认证,authc 表示要进行身份认证
        filterChainMap.put("/user/admin*", "authc");
        // “/user/student” 开头的用户需要角色认证,是“admin”才允许
        filterChainMap.put("/user/student*/**", "roles[admin]");
        // “/user/teacher” 开头的用户需要权限认证,是“user:create”才允许
        filterChainMap.put("/user/teacher*/**", "perms[\"user:create\"]");

        // 配置 logout 过滤器
        filterChainMap.put("/logout", "logout");

        // 设置 shiroFilterFactoryBean 的 FilterChainDefinitionMap
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        logger.info("====shiroFilterFactoryBean注册完成====");
        return shiroFilterFactoryBean;
    }
}

配置 Shiro 过滤器时,我们引入了安全管理器。

至此,我们可以看出,Shiro 配置一环套一环,遵循从 Reaml 到 SecurityManager 再到 Filter 的过程。在过滤器中,我们需要定义一个 shiroFactoryBean,然后将 SecurityManager 引入其中,需要配置的内容主要有以下几项。

  • 默认登录的 URL:身份认证失败会访问该 URL。
  • 认证成功之后要跳转的 URL。
  • 权限认证失败后要跳转的 URL。
  • 需要拦截或者放行的 URL:这些都放在一个 Map 中。

通过上面的代码,我们也了解到, Map 中针对不同的 URL有不同的权限要求,下表总结了几个常用的权限。
在这里插入图片描述

5 使用 Shiro 进行认证

至此,我们完成了 Shiro 的准备工作。接下来开始使用 Shiro 进行认证。

首先,设计如下几个接口。
接口一:使用 http://localhost:8080/user/admin 进行身份认证。
接口二:使用 http://localhost:8080/user/student 进行角色认证。
接口三:使用 http://localhost:8080/user/teacher 进行权限认证。
接口四:使用 http://localhost:8080/user/login 实现用户登录。
开始编码前,我们先了解下认证的流程。

流程一:直接访问接口一(此时还未登录),认证失败,跳转到 login.html 页面让用户登录。登录时请求接口四,实现用户登录,此时 Shiro 已经保存了用户信息。
流程二:再次访问接口一(此时用户已经登录),认证成功,跳转到 success.html 页面,展示用户信息。
流程三:访问接口二,测试角色认证是否成功。
流程四:访问接口三,测试权限认证是否成功。
接下来,编写身份、角色、权限认证接口,代码如下所示:

@Controller
@RequestMapping("/user")
public class UserController {

    /**
     * 身份认证测试接口
     * @param request
     * @return
     */
    @RequestMapping("/admin")
    public String admin(HttpServletRequest request) {
        Object user = request.getSession().getAttribute("user");
        return "success";
    }

    /**
     * 角色认证测试接口
     * @param request
     * @return
     */
    @RequestMapping("/student")
    public String student(HttpServletRequest request) {
        return "success";
    }

    /**
     * 权限认证测试接口
     * @param request
     * @return
     */
    @RequestMapping("/teacher")
    public String teacher(HttpServletRequest request) {
        return "success";
    }
}

这三个接口很简单,直接返回到指定页面即可。认证成功正常跳转,如若认证失败,就会跳转到上文 ShrioConfig 中配置的页面。

之后,我们开始编写用户登录接口,如下所示:

@Controller
@RequestMapping("/user")
public class UserController {

    /**
     * 用户登录接口
     * @param user user
     * @param request request
     * @return string
     */
    @PostMapping("/login")
    public String login(User user, HttpServletRequest request) {

        // 根据用户名和密码创建 Token
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        // 获取 subject 认证主体
        Subject subject = SecurityUtils.getSubject();
        try{
            // 开始认证,这一步会跳到我们自定义的 Realm 中
            subject.login(token);
            request.getSession().setAttribute("user", user);
            return "success";
        }catch(Exception e){
            e.printStackTrace();
            request.getSession().setAttribute("user", user);
            request.setAttribute("error", "用户名或密码错误!");
            return "login";
        }
    }
}

我们重点分析下用户登录接口。整个处理过程是这样的。

首先,根据前端传来的用户名和密码,创建一个 Token。

然后,使用 SecurityUtils 创建认证主体。

紧接着,调用 subject.login(token) 进行身份认证——注意,这里传入了刚刚创建的 Token,如注释所述,这一步会跳转入自定义的 Realm,访问 doGetAuthenticationInfo 方法,开始身份认证。

最后,启动项目,测试一下。在浏览器中请求:http://localhost:8080/user/admin, 首先进行身份认证,此时未登录,会跳转至 IndexController 中 /login 接口处,呈现出 login.html 页面供我们登录。

接着,使用用户名(csdn1)、密码(123456)登录,在浏览器中请求:http://localhost:8080/user/student 接口,开始角色认证,因为数据库中 csdn1 的用户角色是 admin,和配置中的吻合,认证通过。

我们再请求:http://localhost:8080/user/teacher 接口,进行权限认证,因为数据库中 csdn1 的用户权限为 user:*,满足配置中的 user:create,认证通过。

接下来,我们点击“退出”,系统会将该用户注销,让我们重新登录。我们尝试使用 csdn2 用户登录,重复上述操作,进行角色认证和权限认证时,因为数据库中 csdn2 用户的角色和权限与配置中的不同,所以认证失败。