第16课:Spring

第16课:Spring Boot 中集成 Shiro

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

Shiro 三大核心组件

Shiro 有三大核心组件,即 Subject、SecurityManager 和 Realm。先来看一下它们之间的关系。

三大核心组件的关系

Subject 为认证主体,包含 Principals 和 Credentials 两个信息。我们看下两者的具体含义。

  • Principals:代表身份。可以是用户名、邮件、手机号码等等,用来标识一个登录主体的身份;
  • Credentials:代表凭证。常见的有密码,数字证书等等。

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

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

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

Shiro 身份和权限认证

Shiro 身份认证

我们分析下 Shiro 身份认证的过程,首先看一下官方给出的认证图:

认证过程

从图中可以看到,这个过程包括五步:

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

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

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

Shiro 权限认证

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

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

它们之间的的关系可以用下图来表示:

用户、角色和权限的关系

一个用户可以有多个角色,而不同的角色可以有不同的权限,也可有相同的权限。比如说现在有三个角色,1是普通角色,2也是普通角色,3是管理员,角色1只能查看信息,角色2只能添加信息,管理员对两者皆有权限,而且还可以删除信息。

Spring Boot 集成 Shiro

依赖导入

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

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

数据库表的建立及初始化

这里主要涉及到三张表:用户表、角色表和权限表。其实在 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_usert_rolet_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 配置时会对其再做详细说明。

自定义 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;
        }
    }
}

从上面两个方法中可以看出,验证身份时需先根据用户输入的用户名从数据库中查出对应的用户,这时还未涉及到密码,也就是说即使用户输入的密码不正确,照样可以查询出该用户。

然后,将该用户的相关信息封装到 authcInfo 中并返回给 Shiro。接下来就该 Shiro 上场了,将封装的用户信息与用户的输入信息(用户名、密码)进行对比、校验(注意,这里对密码也要进行校验)。校验通过则允许用户登录,否则跳转到指定页面。

同理,权限验证时,也需先根据用户名从数据库中获取其对应的角色和权限,将其封装到 authorizationInfo 并返回给 Shiro。

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有不同的权限要求,下表总结了几个常用的权限。

Filter 说明
anon 开放权限,可以理解为匿名用户或游客,可以直接访问。
authc 需要身份认证。
logout 注销,执行后会直接跳转到 shiroFilterFactoryBean.setLoginUrl() 设置的 URL,即登录页面。
roles[admin] 参数可写多个,表示某个或某些角色才能通过。多个参数时,可写作:roles["admin,user"],多个参数时必须每个参数都通过才算通过。
perms[user] 参数可写多个,表示拥有某个或某些权限才能通过,多个参数时写作:perms[“user, admin”],多个参数时必须每个参数都通过才算通过。

使用 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 用户的角色和权限与配置中的不同,所以认证失败。

总结

本文主要介绍了 Shiro 安全框架与 Spring Boot 的整合。

文章首先引出了 Shiro 的三大核心组件,并对各自的作用进行了分析;紧接着,介绍了 Shiro 的身份认证、角色认证和权限认证的过程;最后结合代码,深入讲解了 Spring Boot 整合 Shiro 的完整过程,同时设计了一套测试流程,逐步分析 Shiro 的工作流程和原理,帮助读者更直观地体会 Shiro 整套工作流程。

Shiro 的应用比较广泛,希望读者将其掌握,并能运用到实际项目中。

课程源代码下载地址:

戳我下载

上一篇
下一篇
目录