• SpringBoot实战(二十五)集成 Shiro


    一、Shiro 简介

    1.1 Shiro 定义

    Apache Shiro:是一款 Java 安全框架,不依赖任何容器,可以运行在 Java 项目中,它的主要作用是做身份认证、授权、会话管理和加密等操作。

    其实不用 Shiro,我们使用原生 Java API 就可以实现安全管理,使用过滤器去拦截用户的各种请求,然后判断是否登录、是否拥有权限即可。但是对于一个大型的系统,分散去管理编写这些过滤器的逻辑会比较麻烦,不成体系,所以需要使用结构化、工程化、系统化的解决方案。

    与 Spring Security 相比,shiro 属于轻量级框架,相对于 Spring Security 简单的多,也没有那么复杂。

    1.2 Shiro 核心组件

    Shiro 的运行机制如下图所示:

    在这里插入图片描述

    1)UsernamePasswordToken:封装用户登录信息,根据用户的登录信息创建令牌 token,用于验证令牌是否具有合法身份以及相关权限。

    2)SecurityManager:核心部分,负责安全认证与授权。

    3)Subject:一个抽象概念,包含了用户信息。

    4)Realm:开发者自定义的模块,根据项目的需求,验证和授权的逻辑在 Realm 中实现。

    5)AuthenticationInfo:用户的角色信息集合,认证时使用。

    6)AuthorizationInfo:角色的权限信息集合,授权时使用。

    7)DefaultWebSecurityManager:安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 中进行管理才能生效。

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

    1.3 Shiro 认证过程

    在这里插入图片描述


    二、SpringBoot集成

    2.1 集成思路

    SpringBoot 集成 Shiro 思路图如下:

    在这里插入图片描述

    项目包结构如下:

    在这里插入图片描述

    2.2 Maven依赖

    
    <dependency>
        <groupId>org.apache.shirogroupId>
        <artifactId>shiro-spring-boot-starterartifactId>
        <version>1.12.0version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.3 自定义 Realm

    自定义 Realm 主要实现了两大模块:

    • 认证:根据用户名,查询密码,然后封装返回 SimpleAuthenticationInfo 认证信息。
    • 授权:根据用户名,查询角色和权限,然后封装返回 SimpleAuthorizationInfo 授权信息。

    CustomRealm.java

    import com.demo.module.entity.User;
    import com.demo.module.service.UserService;
    import com.demo.util.SpringUtils;
    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.apache.shiro.util.ByteSource;
    
    /**
     * 

    @Title CustomRealm *

    @Description 自定义Realm * * @author ACGkaka * @date 2023/10/15 12:42 */ public class CustomRealm extends AuthorizingRealm { /** * 认证 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { // 获取用户名 String principal = (String) authenticationToken.getPrincipal(); // 根据用户名查询数据库 UserService userService = SpringUtils.getBean(UserService.class); User user = userService.findByUsername(principal); if (user != null) { return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), this.getName()); } return null; } /** * 授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 获取用户名 String principal = (String) principalCollection.getPrimaryPrincipal(); if ("admin".equals(principal)) { // 管理员拥有所有权限 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRole("admin"); info.addStringPermission("admin:*"); info.addRole("user"); info.addStringPermission("user:find:*"); return info; } return null; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    2.4 Shiro 配置类

    配置类中指定了如下内容:

    • 需要进行鉴权的资源路径;
    • 指定自定义 Realm;
    • 加密规则。

    ShiroConfig.java

    import com.demo.config.shiro.realm.CustomRealm;
    import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
    import org.apache.shiro.realm.Realm;
    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.HashMap;
    import java.util.Map;
    
    /**
     * 

    @Title ShiroConfig *

    @Description Shiro配置类 * * @author ACGkaka * @date 2023/10/15 12:44 */ @Configuration public class ShiroConfig { /** * ShiroFilter过滤所有请求 */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 给ShiroFilter配置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); // 配置系统公共资源、系统受限资源(公共资源必须在受限资源上面,不然会造成死循环) Map<String, String> map = new HashMap<>(); // 系统公共资源 map.put("/login", "anon"); map.put("/register", "anon"); map.put("/static/**", "anon"); // 受限资源 map.put("/**", "authc"); // 设置认证界面路径 shiroFilterFactoryBean.setLoginUrl("/login"); shiroFilterFactoryBean.setFilterChainDefinitionMap(map); return shiroFilterFactoryBean; } /** * 创建安全管理器 */ @Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); return securityManager; } /** * 创建自定义Realm */ @Bean public Realm realm() { CustomRealm realm = new CustomRealm(); // 设置使用哈希凭证匹配 HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); // 设置使用MD5加密算法 credentialsMatcher.setHashAlgorithmName("MD5"); // 设置散列次数:加密次数 credentialsMatcher.setHashIterations(1024); realm.setCredentialsMatcher(credentialsMatcher); return realm; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72

    2.5 静态资源映射

    静态资源映射主要将 cssjs 等静态文件夹映射到浏览器端,方便页面加载。

    在这里插入图片描述

    WebConfiguration.java

    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Primary;
    import org.springframework.util.ResourceUtils;
    import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * SpringBoot静态路径配置
     *
     * @author ACGkaka
     * @date 2019/11/27 15:38
     */
    @Configuration
    @Primary
    public class WebConfiguration implements WebMvcConfigurer {
    
        /**
         * 访问外部文件配置
         */
        @Override
        public void addResourceHandlers(ResourceHandlerRegistry registry) {
            registry.addResourceHandler("/static/css/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/css/");
            registry.addResourceHandler("/static/js/**").addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX + "/static/js/");
            WebMvcConfigurer.super.addResourceHandlers(registry);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    2.6 AuthController

    主要用于进行 thymeleaf 模板引擎的页面跳转,以及登录、注册、退出登录等功能的实现。

    AuthController.java

    import com.demo.module.entity.User;
    import com.demo.module.service.UserService;
    import lombok.AllArgsConstructor;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.IncorrectCredentialsException;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.subject.Subject;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    
    import javax.annotation.Resource;
    
    /**
     * 

    @Title IndexController *

    @Description 鉴权Controller * * @author ACGkaka * @date 2019/10/23 20:23 */ @Controller @AllArgsConstructor public class AuthController { @Resource private UserService userService; /** * 默认跳转主页 */ @GetMapping("/") public String showIndex() { return "redirect:/index"; } /** * 主页 */ @GetMapping("/index") public String index() { return "index.html"; } /** * 主页 */ @GetMapping("/login") public String login() { return "login.html"; } /** * 注册页 */ @GetMapping("/register") public String register() { return "/register.html"; } /** * 登录 */ @PostMapping("/login") public String login(String username, String password) { // 获取主题对象 Subject subject = SecurityUtils.getSubject(); try { subject.login(new UsernamePasswordToken(username, password)); System.out.println("登录成功!!!"); return "redirect:/index"; } catch (UnknownAccountException e) { System.out.println("用户错误!!!"); } catch (IncorrectCredentialsException e) { System.out.println("密码错误!!!"); } return "redirect:/login"; } /** * 注册 */ @PostMapping("/register") public String register(User user) { userService.register(user); return "redirect:/login.html"; } /** * 退出登录 */ @GetMapping("/logout") public String logout() { Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/login.html"; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98

    2.7 User 实体

    封装了用户的基本属性。

    User.java

    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.time.LocalDateTime;
    
    /**
     * 

    * 用户表 *

    * * @author ACGkaka * @since 2021-04-25 */
    @Data @TableName("t_user") public class User implements Serializable { private static final long serialVersionUID = 1L; /** * 主键 */ @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 随机盐 */ private String salt; /** * 创建时间 */ private LocalDateTime createTime; /** * 更新时间 */ private LocalDateTime updateTime; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    2.8 用户接口类

    封装了注册(用户新增)和根据用户名查找接口。

    UserService.java

    import com.baomidou.mybatisplus.extension.service.IService;
    import com.demo.module.entity.User;
    
    /**
     * 用户表 服务类
     */
    public interface UserService extends IService<User> {
    
        /**
         * 注册
         * @param user 用户
         */
        void register(User user);
    
        /**
         * 根据用户名查询用户
         * @param principal 用户名
         * @return 用户
         */
        User findByUsername(String principal);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    2.9 用户接口实现类

    实现了注册(用户新增)和根据用户名查找功能。

    UserServiceImpl.java

    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.demo.module.entity.User;
    import com.demo.module.mapper.UserMapper;
    import com.demo.module.service.UserService;
    import com.demo.util.SaltUtils;
    import org.apache.shiro.crypto.hash.Md5Hash;
    import org.springframework.stereotype.Service;
    
    /**
     * 

    * 用户表 服务实现类 *

    * * @author ACGkaka * @since 2021-04-25 */
    @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public void register(User user) { // 注册用户 checkRegisterUser(user); // 1.生成随机盐 String salt = SaltUtils.getSalt(8); // 2.将随机盐保存到数据库 user.setSalt(salt); // 3.明文密码进行MD5 + salt + hash散列次数 Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024); user.setPassword(md5Hash.toHex()); // 4.保存用户 this.save(user); } @Override public User findByUsername(String principal) { // 根据用户名查询用户 LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUsername, principal); return this.getOne(queryWrapper); } // ------------------------------------------------------------------------------------------ // 内部方法 // ------------------------------------------------------------------------------------------ /** * 校验注册用户 * @param user 用户 */ private void checkRegisterUser(User user) { if (user == null) { throw new RuntimeException("用户信息不能为空"); } if (user.getUsername() == null || "".equals(user.getUsername())) { throw new RuntimeException("用户名不能为空"); } if (user.getPassword() == null || "".equals(user.getPassword())) { throw new RuntimeException("密码不能为空"); } // 判断用户名是否已存在 User existUser = this.getOne(new UpdateWrapper<User>().eq("username", user.getUsername())); if (existUser != null) { throw new RuntimeException("用户名已存在"); } } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71

    2.10 OrderController(鉴权测试)

    在自定义 Realm 配置好用户权限后,用于测试对用户权限和角色权限的控制。

    OrderController.java

    import com.demo.common.Result;
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authz.annotation.RequiresPermissions;
    import org.apache.shiro.authz.annotation.RequiresRoles;
    import org.apache.shiro.subject.Subject;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 

    @Title OrderController *

    @Description 订单控制器 * * @author ACGkaka * @date 2023/10/15 17:15 */ @RestController @RequestMapping("/order") public class OrderController { /** * 新增订单(代码实现权限判断) */ @GetMapping("/add") public Result<Object> add() { Subject subject = SecurityUtils.getSubject(); if (subject.hasRole("admin")) { return Result.succeed().setData("新增订单成功"); } else { return Result.failed().setData("操作失败,无权访问"); } } /** * 编辑订单(注解实现权限判断) */ @RequiresRoles({"admin", "user"}) // 用来判断角色,同时拥有admin和user角色才能访问 @RequiresPermissions("user:edit:01") // 用来判断权限,拥有user:edit:01权限才能访问 @GetMapping("/edit") public String edit() { System.out.println("编辑订单"); return "redirect:/index"; } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    三、测试

    测试1:跳转登录页面

    访问地址:http://localhost:8081,我们配置了根路径默认访问主页,由于用户没有登录,会默认跳转登录页面。

    在这里插入图片描述

    测试2:注册用户

    访问地址:http://localhost:8081/register,输入用户名和密码后,系统会在数据库中新增用户信息,自动跳转登录页面,输入刚才的用户名和密码即可登录。

    在这里插入图片描述

    测试3:登录主页

    输入正确的用户名密码,即可登录至主页。

    在这里插入图片描述

    测试4:权限校验

    我们在主页中加入了 OrderController 中的接口,用于测试权限校验。

    在这里插入图片描述

    当使用非 admin 用户进行登录的时候,代码实现权限判断的 /add 接口,报错如下:

    在这里插入图片描述

    Shiro 注解实现权限判断的 /edit 接口,报错如下:

    在这里插入图片描述

    从提示信息可以看到,返回了状态码 500,报错信息为:当前用户的主体没有 admin 权限。

    由此可见,两种鉴权结果均成功生效,具体使用哪一种由业务场景来定。

    四、注意

    由于 Shiro 核心是通过 Session 来实现用户登录的,所以有很多场景不支持,比如:

    1)Session 默认存储在内存中,重启应用后所有用户登录状态会失效

    2)默认配置不支持分布式,需要分布式部署的时候,可以通过 Redis数据库方式来同步 Session 会话。

    整理完毕,完结撒花~ 🌻





    参考地址:

    1.SpringBoot之整合Shiro(最详细),https://blog.csdn.net/Yearingforthefuture/article/details/117384035

    2.超详细 Spring Boot 整合 Shiro 教程!https://cloud.tencent.com/developer/article/1643122

    3.springboot整合shiro(完整版),https://blog.csdn.net/w399038956/article/details/120434244

    4.Shiro安全框架【快速入门】就这一篇!https://zhuanlan.zhihu.com/p/54176956

  • 相关阅读:
    Linux-CentOS重要模块
    MySQL日志管理、备份与恢复
    Kafka 认证三:Kerberos 认证中心部署
    python练习5
    httpclient连接泄漏实战-_-
    16-SpringBoot 整合Druid数据源
    大视频文件的缓冲播放原理以及实现
    零基础入门学用Arduino 第三部分(三)
    Java+Swing+mysql高校教材管理系统
    深入解析kubernetes中的选举机制
  • 原文地址:https://blog.csdn.net/qq_33204709/article/details/133848334