SpringBoot实现基于shiro安全框架的,配合thymeleaf模板引擎的用户认证和授权

Posted 新来的大狮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot实现基于shiro安全框架的,配合thymeleaf模板引擎的用户认证和授权相关的知识,希望对你有一定的参考价值。

SpringBoot实现基于shiro安全框架的,配合thymeleaf模板引擎的用户认证和授权,及其思路介绍和流程详解

参考视频:黑马程序员 - SpringBoot与Shiro整合-权限管理实战视频

参考博客:Asurplus、 - 【SpringBoot】廿三、SpringBoot中整合Shiro实现权限管理

1 用户、角色和权限的概念及关系

  • 用户:应用系统的具体操作者。
  • 角色:便于复用的权限集合。
  • 权限:规定了的一系列的访问规则,如:系统资源的访问权限,系统功能的操作权限等。

一般用户具体可具有多种角色(或:角色被分配给用户),角色可包含多种权限。借助不同的角色作为桥梁,使不同的用户具有不同的权限。

具体三者关系(通常情况):

  • 一个用户可以拥有不同的角色,一个角色可以被授予给不同的用户。(即:用户和角色的关系:多对多)
  • 一个角色可以拥有不同的权限,一个权限可以被分配给不同的角色。(即:角色和权限的关系:多对多)

2 shiro安全框架部分基本功能

2.1 用户认证

系统对用户身份进行验证合法性的过程。常见形式如:输入用户名密码登录系统。

2.2 用户授权

认证用户合法性后,对用户具有的角色授予相应权限的过程。

3 不同用户系统功能的差异性实现

3.1 准备工作-引入shiro相关依赖

		<!--shiro权限管理框架-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <!--thymeleaf对shiro的扩展-->
        <dependency>
            <groupId>com.github.theborakompanioni</groupId>
            <artifactId>thymeleaf-extras-shiro</artifactId>
            <version>2.0.0</version>
        </dependency>

3.2 基于代码的shiro框架的简单架构与运行机制介绍

  • 创建shrio过滤工厂:ShiroFilterFactoryBean
  • 创建shiro安全管理器:SecurityManager
  • 创建自定义的领域类:自定义Realm类(继承shiro的授权抽象类),其实现了自定义的用户认证和授权逻辑。

shiro整体执行逻辑:
需要进行认证或授权时,shiro框架根据创建的过滤工厂,为其指配好创建的安全管理器,然后为安全管理器设置自定义认证和授权逻辑的领域类,最终实现业务的安全逻辑。

其中:自定义Realm类的

  • doGetAuthorizationInfo(PrincipalCollection principalCollection) 用于实现授权逻辑
  • doGetAuthenticationInfo(AuthenticationToken authenticationToken) 用于实现认证逻辑

3.3 具体shiro环境配置代码

3.3.1 shiro环境配置 - ShiroConfig.java

核心编写3个Bean类:过滤工厂(设置需拦截的URL),安全管理器,自定义的Realm类(实现用户认证和用户授权逻辑)

package cn.castle.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.mgt.SecurityManager;
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 java.util.LinkedHashMap;
import java.util.Map;

/**
 * 核心编写3个Bean:过滤工厂,安全管理器,自定义的Realm类
 */
@Configuration
public class ShiroConfig 
    /**
     * 注入这个是是为了在thymeleaf中使用shiro的自定义tag。
     */
    @Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() 
        return new ShiroDialect();
    

    /**
     * 1. shiro过滤工厂  URL地址过滤器
     * @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");

        /**
         * shiro内置过滤器
         * 常用过滤器:
         *      anon: 无需登录
         *      authc: 必须认证
         *      user: 使用rememberMe 可以直接访问
         *      perms: 必须获得资源权限
         *      role: 必须获得角色权限
         */
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/student", "authc");
        filterChainDefinitionMap.put("/study", "perms[system]");
        filterChainDefinitionMap.put("/books", "anon");
        //         其余url全部拦截,必须放在最后
//        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    

    /**
     * 2. 自定义安全管理器
     */
    @Bean
    public SecurityManager getSecurityManager() 
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //关联自定义的realm类
        securityManager.setRealm(userRealm());
        return securityManager;
    

    /**
     * 3. 自定义的Realm类实现认证和授权
     * @return
     */
    @Bean
    public UserRealm userRealm()
        return new UserRealm();
    


3.3.2 自定义Realm类实现 - UserRealm.java

3.3.2.1 认证逻辑思路

(shiro的认证核心思想:拦截指定URL)
(自定义的认证核心逻辑:查询数据库,若用户名和密码一致则认证通过)
实现认证需要的配合:过滤工厂拦截指定URL,以及controller层对shiro认证后返回结果的页面跳转逻辑处理 [ 具体controller层代码详见测试代码3.4.1 ])

  1. 总流程:借助先前定义的过滤工厂,对指定的URL请求进行拦截,进行用户认证相关逻辑,若未通过用户认证,则无法进行指定URL跳转,而是返回默认进行登录的URL
  2. 在shiro拦截指定URL后,转向用户登录界面。
  3. 借助AuthenticationToken类型的变量token,将其强制转型为UsernamePasswordToken类型,用于将登录相关数据在进入controller层后,借助该变量将用户名和密码在controller业务逻辑层和shiro用户认证逻辑层传递数据
  4. 然后在shiro用户认证逻辑层借助Dao类对数据库根据用户名进行查询,用户不存在返回null,用户存在则进行密码验证逻辑。
  5. 验证若成功后,shiro用户认证逻辑层将返回SimpleAuthenticationInfo类型给controller层,其构造参数第一个为用户类型,该用户类型的变量指定后 便于进行下一步用户授权时获取当前登录的用户,同时返回相应的登录成功页面。
3.3.2.2 授权逻辑思路(示例自定义的需求的授权过程未使用到URL拦截)

(自定义的授权核心逻辑:查询数据库当前用户所拥有的所有角色,该用户所有角色的所有权限都有哪些,并对当前用户进行授予)
(自定义的业务需求:前端根据预设的权限,若当前用户具有该权限,则开放相应的功能模块)
实现授权需要的配合:前端页面中thymeleaf写出的模板页面中已经添加带有权限(shiro:hasPermission属性)的标签元素)

  1. 预先设置前端页面中某些功能模块(某些功能标签)需要一定的权限才可以显示(借助shiro对thymeleaf的扩展,具体代码在测试代码3.4.2中展示)。
  2. 借助SecurityUtils.getSubject()获取的Subject类型变量,对其调用getPrincipal()方法并对结果强制转型,用于获取当前通过认证的用户。
  3. 根据获得的用户,向数据库查询其所有的角色,再查询角色的所有权限的Set集,最终通过SimpleAuthorizationInfo变量将其授予给当前用户。(数据库逻辑设定用户具有角色,而角色具有权限)
  4. 由shiro框架自动完成 根据当前用户是否拥有某些权限而是否开放(显示)某些功能标签。
3.3.2.3 用户认证和授权具体逻辑代码
package cn.castle.shiro;

import cn.castle.dao.PermissionDao;
import cn.castle.dao.RoleDao;
import cn.castle.dao.UserDao;
import cn.castle.pojo.Function;
import cn.castle.pojo.SysRole;
import cn.castle.pojo.SysUser;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.*;

/**
 * 自定义Realm类,继承授权Realm类
 */
public class UserRealm extends AuthorizingRealm 
    @Autowired
    UserDao userDao;
    @Autowired
    RoleDao roleDao;
    @Autowired
    PermissionDao permissionDao;

    /**
     * 执行授权逻辑方法
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) 
//        获取当前登录的用户
        Subject subject = SecurityUtils.getSubject();
        SysUser sysUser = (SysUser) subject.getPrincipal();

//        获取当前用户的所有角色后,根据角色获取所有权限存入set集合中
        List<SysRole> roleList = roleDao.getRolesByUserUuid(sysUser.getUuid());
        List<Function> functionList = new ArrayList<>();
        Set<String> permissionSet = new LinkedHashSet<>();
        for(SysRole role: roleList)
            functionList.addAll(permissionDao.getFunctionsByRoleUuid(role.getUuid()));
        
        for(Function function: functionList)
            permissionSet.add(function.getPression());
        

//        为当前用户添加读取的所有权限
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        for(String permission: permissionSet)
            info.addStringPermission(permission);
        
        return info;
    

    /**
     * 执行认证逻辑方法
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException 
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        String password = String.valueOf(token.getPassword());

        List<SysUser> userList = userDao.getSysUserByName(username);

        //用户名不存在
        if(userList.size() < 1)
//            返回null底层将输出UnknownAccountException
            return null;
        

//        提供数据库的真实密码,由shiro进行用户认证
        SysUser sysUser = userList.get(0);
        String realPassword = sysUser.getPassword();
        return new SimpleAuthenticationInfo(sysUser, realPassword, "");
    

3.4 具体测试代码

3.4.1 用户认证测试代码

实际操作与内部逻辑:

  1. 使用浏览器访问指定被过滤的URL资源。(此时未进行登录)
  2. 由于该URL被shiro框架过滤的原因,且当前用户未认证,第一次访问失败,系统跳转到设定的用户登录页面。
  3. 用户登录时输入用户名和密码,数据从前端页面传递到controller层,controller层封装后传递给shiro框架的自定义Realm认证逻辑进行认证,并返回认证结果。
  4. 用户登录成功后(认证成功),shiro返回正确的类型给controller层,再根据controller层逻辑,认证的用户成功进入认证后的界面。

设置将被拦截的指定URL(过滤工厂中设置,详见3.3.1):

filterChainDefinitionMap.put("/student", "authc");

默认在过滤工厂中设置的返回的登录界面:

shiroFilterFactoryBean.setLoginUrl("/login");

被拦截后的前端登录页面的登录表单login.html(使用了thymeleaf模板引擎):

		<form role="form" action="/login2">
            <h1 class="login" style="text-align: center">授权版登录</h1>
            <h3 class="mes" th:text="$message2"></h3>
            <p id="result2" hidden th:text="$result"></p>
            <div style="text-align: center">
                <input type="text" id="username2" name="username88" placeholder="用户名">
            </div>
            <div style="text-align: center">
                <input type="password" id="password2" name="password88" placeholder="密码">
            </div>
            <div style="text-align: center">
                <button type="submit">登录</button>
            </div>
        </form>

controller层认证及跳转:
需要借助SecurityUtils获取Subject后,为shiro框架执行认证逻辑时 传递 封装着用户名和密码的token对象。

	@RequestMapping("/login2")
    public String login2(@RequestParam("username88") String username, @RequestParam("password88") String password,
                         Model model)
//        1. 获取subject对象
        Subject subject = SecurityUtils.getSubject();
//        2. 封装用户数据至token
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
//        3. 执行登录方法subject.login,借助shiro执行登录的验证逻辑
        try
            subject.login(token);
//            使用redirect向控制器发送请求,非直接访问模板页面
            return "redirect:/index";
        catch (UnknownAccountException e)
            model.addAttribute("message2", "用户名不存在");
            println("用户不存在");
            return "login";
        catch (IncorrectCredentialsException e)
            println("密码错误");
            model.addAttribute("message2", "密码错误");
            return "login";
        
	

3.4.2 用户授权测试代码

实际操作与内部逻辑;

  1. 认证过的用户(成功登录后)访问相应的带有权限的URL资源(业务需求:整个html页面中部分功能标签需要权限)。
  2. 每次加载该带有权限的URL资源时,shiro框架根据自定义Realm类的执行授权逻辑(从数据库取出当前用户的所有权限字符串,添加至SimpleAuthorizationInfo类型变量后返回),对当前用户进行授权。
  3. 页面根据当前所需权限字符串和用户已拥有权限字符串进行比较,对拥有相应权限(字符串内容一致)的功能标签进行渲染。

注意事项 ①:使用带shiro:hasPermission属性标签的html文件引入的命名空间:

<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">

注意事项 ② : 使用带shiro:hasPermission属性标签的html文件同时需要ShiroConfig类中创建 支持shiro属性的thymeleaf模板页面 的方言Bean类(ShiroDialect),详见3.3.1-shiro环境配置:

	@Bean(name = "shiroDialect")
    public ShiroDialect shiroDialect() 
        return new ShiroDialect();
    

前端配合的带有权限(shiro:hasPermission属性)的功能模块标签:

<a shiro:hasPermission="system" href="#">
	<i class="fa fa-home"></i>
	<span class="nav-label">系统管理</span>
	<span class="fa arrow"></span>
</a>
<a shiro:hasPermission="system.userPage" class="J_menuItem" style="background-color:#293846" οnmοusemοve="this.style.backgroundColor='#465e79'" οnmοuseοut="this.style.backgroundColor='#293846'" href="/userManage">用户管理</a>
<a shiro:hasPermission="system.rolePage" class="J_menuItem" style="background-color:#293846" οnmοusemοve="this.style.backgroundColor='#465e79'" οnmοuseοut="this.style.backgroundColor='#293846'"  href="/toRoleManagement">角色管理</a>
<a shiro:hasPermission="system.functionPage" class="J_menuItem" style="background-color:#293846" οnmοusemοve="this.style.backgroundColor='#465e79'" οnmοuseοut="this.style.backgroundColor='#293846'"  href="/toFunctionManagement">功能管理</a>

数据库中角色存储的权限字符串展示(varchar2类型):

3.5 最终测试结果

3.5.1 用户认证结果

未认证时,申请URL: /student, 被拦截进入登录界面。

3.5.2 用户授权结果

如图所示,左侧为未认证用户,没有任何权限,带有权限的功能都没有展示。(系统管理,用户管理,角色管理,功能管理)
右侧则为管理员用户,所有带权限的功能完全开放。

以上是关于SpringBoot实现基于shiro安全框架的,配合thymeleaf模板引擎的用户认证和授权的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot2.0 整合 Shiro 框架,实现用户权限管理

简单易用的Apache shiro框架,以及复杂完整的springboot security安全框架

基于SpringBoot的博客项目

SpringBoot+Shiro+Jwt实现登录认证——最干的干货

Shiro 核心功能案例讲解 基于SpringBoot 有源码

SpringBoot11:集成Shiro