Spring Boot项目搭建流程(二)—— 整合Apache Shiro框架

整合Apache Shiro框架

  • Shiro三大核心元素

    • Subject:主体(即"the current ‘user’");在Shiro中通过SecurityUtils类来获取;
    • SecurityManager:安全管理器;执行所有与安全相关的操作,需从Realm中获取用户的角色或权限进行验证;
    • Realm:域;管理用户、角色、权限等安全数据,供SecurityManager使用。
    • 注意: 需要手动完成Realm的实现;继承AuthorizingRealm抽象类并实现doGetAuthorizationInfo()和doGetAuthenticationInfo()方法。
  • Shiro功能及特性

    • 身份验证(Authentication)
    • 授权(Authorization)
    • 会话管理(Session Management)
    • 密码学(Cryptography)
    • Web支持(Web Support)
    • 缓存(Caching)
    • 并发性(Concurrency)
    • 测试(Testing)
    • 运行方式(Run As)
    • 记住我(Remember Me)
  • Spring Boot Shiro用户认证

    • 集成步骤:

      • 自定义ShiroConfig类,配置SecurityManager Bean;
      • 在ShiroConfig中配置过滤工厂类(ShiroFilterFactoryBean,依赖于SecurityManager);
      • 自定义Reaml实现,实现doGetAuthenticationInfo()和doGetAuthorizationInfo()方法。
    • 引入shiro-spring依赖
    • 自定义Shiro配置类
    	@Configuration
    	public class ShiroConfig {
    	
    	    @Bean
    	    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
    	        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    	        // 设置SecurityManager
    	        shiroFilterFactoryBean.setSecurityManager(securityManager);
    	        // 配置登录URL
    	        shiroFilterFactoryBean.setLoginUrl("/login");
    	        // 配置登录成功跳转URL
    	        shiroFilterFactoryBean.setSuccessUrl("/index");
    	        // 配置未授权URL
    	        shiroFilterFactoryBean.setUnauthorizedUrl("/403");
    	
    	        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    	        // 定义filterChain,静态资源不拦截
    	        filterChainDefinitionMap.put("/css/**", "anon");
    	        filterChainDefinitionMap.put("/js/**", "anon");
    	        filterChainDefinitionMap.put("/fonts/**", "anon");
    	        filterChainDefinitionMap.put("/img/**", "anon");
    	        // Druid数据源监控页面不拦截
    	        filterChainDefinitionMap.put("/druid/**", "anon");
    	        // 配置退出过滤器,其中具体的退出代码Shiro已经替我们实现了
    	        filterChainDefinitionMap.put("/logout", "logout");
    	        filterChainDefinitionMap.put("/", "anon");
    	        // 除上以外所有URL都必须认证通过后才可以访问,未通过认证自动访问LoginUrl
    	        filterChainDefinitionMap.put("/**", "authc");
    	
    	        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    	        return shiroFilterFactoryBean;
    	    }
    	
    	    @Bean
    	    public SecurityManager securityManager() {
    	        // 配置SecurityManager,并注入ShiroRealm
    	        DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
    	        securityManager.setRealm(shiroRealm());
    	        return securityManager;
    	    }
    	
    	    @Bean
    	    public ShiroRealm shiroRealm() {
    	        // 配置ShiroRealm,需要自己实现
    	        return new ShiroRealm();
    	    }
    	
    	}
    
    • 注意: filterChain基于短路机制,即最先匹配规则;anon、authc等为Shiro实现的过滤器。
    • 实现Realm(继承AuthorizingRealm类)
    	public class ShiroRealm extends AuthorizingRealm {
    	
    	    @Resource
    	    private UserService userService;
    	
    	    @Override
    	    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    	        return null;
    	    }
    	
    	    @Override
    	    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    	        // 获取用户输入的用户名和密码
    	        String username = (String) authenticationToken.getPrincipal();
    	        String password = new String((char[]) authenticationToken.getCredentials());
    	
    	        // 通过用户名到数据库查询用户信息
    	        UserPO user = userService.findByUsername(username);
    	        if (user == null) {
    	            throw new UnknownAccountException("用户名或密码错误!");
    	        }
    	        if (!password.equals(user.getPassword())) {
    	            throw new IncorrectCredentialsException("用户名或密码错误!");
    	        }
    	        if (!user.getStatus()) {
    	            throw new LockedAccountException("账号已被锁定,请联系管理员!");
    	        }
    	
    	        return new SimpleAuthenticationInfo(user, password, getName());
    	    }
    	}
    
    • 注意: Shiro具有丰富的运行时AuthenticationException层次结构,可以根据捕获的异常准确指出尝试失败的原因。
    • 数据层:与用户、角色、权限相关的表、Mapper映射文件、Dao层接口、Service层接口及实现类
    • 登录页面
    • 登录接口
    	@Controller
    	public class LoginController {
    	
    	    @GetMapping("/login")
    	    public String login() {
    	        return "login";
    	    }
    	
    	    @PostMapping("/login")
    	    @ResponseBody
    	    public BaseResponse login(String username, String password){
    	        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    	        // 获取Subject对象
    	        Subject subject = SecurityUtils.getSubject();
    	        try {
    	            subject.login(token);
    	            return BaseResponse.ok();
    	        } catch (UnknownAccountException | IncorrectCredentialsException | LockedAccountException e) {
    	            return BaseResponse.error(e.getMessage());
    	        } catch (AuthenticationException e) {
    	            return BaseResponse.error("认证失败!");
    	        }
    	    }
    	
    	    @RequestMapping("/")
    	    public String redirectIndex() {
    	        return "redirect:/index";
    	    }
    	
    	    @RequestMapping("/index")
    	    public String index(Model model) {
    	        UserPO user = (UserPO) SecurityUtils.getSubject().getPrincipal();
    	        model.addAttribute("user", user);
    	        return "index";
    	    }
    	
    	}
    
    • 注意: 根据需要在application.yml中加入静态页面的访问路径配置
    	spring:
    	  thymeleaf:
    	    prefix: classpath:/templates/febs/views/
    
  • Spring Boot Shiro Remember Me

    • 记住我:用户的登录状态不会因为浏览器的关闭而失效,直到Cookie过期。
    • 修改Shiro配置类,加入Cookie对象及Cookie管理对象
    	/**
         * Cookie对象:用于实现RememberMe功能
         * @return
         */
        private SimpleCookie rememberMeCookie() {
            // 设置Cookie名称,对应login.html页面的<input type="checkbox" name="rememberMe"/>
            SimpleCookie cookie = new SimpleCookie("rememberMe");
            // 设置Cookie的过期时间,单位为秒,此处设置30分钟
            cookie.setMaxAge(1800);
            return cookie;
        }
    
        /**
         * Cookie管理对象:用于实现RememberMe功能
         * @return
         */
        private CookieRememberMeManager rememberMeManager() {
            CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
            cookieRememberMeManager.setCookie(rememberMeCookie());
            // 设置rememberMeCookie加密的密钥
            // cookieRememberMeManager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag=="));
            return cookieRememberMeManager;
        }
    
    • 将Cookie管理对象设置到SecurityManager中
    	@Bean
        public SecurityManager securityManager() {
            // 配置SecurityManager,并注入ShiroRealm
            DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(shiroRealm());
            securityManager.setRememberMeManager(rememberMeManager());
            return securityManager;
        }
    
    • 修改权限配置,将(/**)路径的访问权限由"authc"改为"user";user指的是用户认证通过或者配置了Remember Me记住用户登录状态后可访问
    	filterChainDefinitionMap.put("/**", "user");
    
    • 修改登录页面:加入Remember Me复选框
    • 修改登录接口
    	@PostMapping("/login")
        @ResponseBody
        public BaseResponse login(String username, String password, Boolean rememberMe){
            UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
            // 获取Subject对象
            Subject subject = SecurityUtils.getSubject();
            try {
                subject.login(token);
                return BaseResponse.ok();
            } catch (UnknownAccountException | IncorrectCredentialsException | LockedAccountException e) {
                return BaseResponse.error(e.getMessage());
            } catch (AuthenticationException e) {
                return BaseResponse.error("认证失败!");
            }
        }
    
  • Spring Boot Shiro权限控制

    • Shiro权限控制的三大核心元素:用户、角色、权限。
    • 数据库设计模型:RBAC(Role-Based Access Control,基于角色的访问控制)模型,构成“用户-角色-权限”的授权模型;其中,用户与角色之间,角色与权限之间,是多对多的关系。
    • 根据数据库设计模型创建表、Mapper映射文件、Dao层接口、Service层接口及实现类。
    • 实现Realm的doGetAuthorizationInfo()方法
    	/**
         * 获取用户角色和权限
         * @param principalCollection
         * @return
         */
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            UserPO user = (UserPO) SecurityUtils.getSubject().getPrincipal();
            Long userId = user.getId();
    
            SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    
            // 获取用户角色集
            List<RolePO> roleList = userRoleService.findByUserId(userId);
            Set<String> roleSet = new HashSet<>();
            Set<String> permissionSet = new HashSet<>();
            for (RolePO role : roleList) {
                roleSet.add(role.getRoleName());
    
                // 获取用户权限集
                List<PermissionPO> permissionList = rolePermissionService.findByRoleId(role.getId());
                for (PermissionPO permission : permissionList) {
                    permissionSet.add(permission.getPermissionUrl());
                }
            }
            simpleAuthorizationInfo.setRoles(roleSet);
            simpleAuthorizationInfo.setStringPermissions(permissionSet);
            return simpleAuthorizationInfo;
        }
    
    • 修改Shiro配置类,开启权限相关的注解
    	/**
         * 配置开启权限相关的注解
         * @param securityManager
         * @return
         */
        @Bean
        public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
            AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
            authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
            return authorizationAttributeSourceAdvisor;
        }
    
    • Shiro提供的和权限相关的注解:
      • @RequiresAuthentication:需要当前Subject已通过login进行了登录认证
      • @RequiresUser:需要当前Subject已身份认证或已通过记住我进行了登录
      • @RequiresGuest:需要当前Subject未身份认证或未通过记住我登录过(游客身份)
      • @RequiresRoles(value={“admin”, “user”}, logical = Logical.AND):需要当前Subject拥有角色"admin"和"user"
      • @RequiresPermissions (value={“user:a”, “user:b”}, logical = Logical.OR):需要当前Subject拥有权限"user:a"或"user:b"
    • 在需要进行权限控制的接口方法上,添加Shiro权限注解控制接口的访问权限
    	@Controller
    	@RequestMapping("/user")
    	public class UserController {
    	
    	    @RequiresPermissions(value = {"user:user"})
    	    @GetMapping("/list")
    	    @ResponseBody
    	    public String userList() {
    	        return "userList";
    	    }
    	
    	    @RequiresPermissions(value = {"user:add"})
    	    @GetMapping("/add")
    	    @ResponseBody
    	    public String add() {
    	        return "add:success";
    	    }
    	
    	    @RequiresPermissions(value = {"user:delete"})
    	    @GetMapping("/delete")
    	    @ResponseBody
    	    public String delete() {
    	        return "delete:success";
    	    }
    	
    	}
    
    • 在LoginController中添加一个"/403"未授权跳转
    	@GetMapping("/403")
        public String forbid() {
            return "403";
        }
    
    • 注意点
      • 异常:org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method:
      • 解决:在ShiroConfig中配置的shiroFilterFactoryBean.setUnauthorizedUrl("/403"),只对filterChain中设置的路径的访问权限起到拦截跳转(重定向)的作用;需要自定义全局异常捕获类
      	@ControllerAdvice
      	@Order(value = Ordered.HIGHEST_PRECEDENCE)
      	public class GlobalExceptionHandler {
      	
      	    @ExceptionHandler(value = AuthorizationException.class)
      	    public String handleAuthorizationException() {
      	        return "403";
      	    }
      	
      	}
      
  • Spring Boot Shiro使用缓存(Redis)

    • 引入Redis依赖(shiro-redis)
    • 在application.yml中配置Redis
    	spring:
    		redis:
    		    # Redis数据库索引(默认为 0)
    		    database: 1
    		    # Redis服务器地址
    		    host: localhost
    		    # Redis服务器连接端口6379
    		    port: 6379
    		    # Redis密码
    		    password:
    		    # Redis连接池
    		    jedis:
    		      pool:
    		        # 连接池最大连接数(使用负值表示没有限制)
    		        max-active: 8
    		        # 连接池最大阻塞等待时间(使用负值表示没有限制)
    		        max-wait: -1
    		        # 连接池中的最大空闲连接
    		        max-idle: 8
    		        # 连接池中的最小空闲连接
    		        min-idle: 0
    		    # 连接超时时间(毫秒)
    		    timeout: 0
    
    • 修改Shiro配置类,配置RedisManager并注入到RedisCacheManager
    	@Value("${spring.redis.host}")
        private String host;
        @Value("${spring.redis.port}")
        private int port;
        @Value("${spring.redis.password:}")
        private String password;
        @Value("${spring.redis.timeout}")
        private int timeout;
        @Value("${spring.redis.database:0}")
        private int database;
    	
    	private RedisManager redisManager() {
            RedisManager redisManager = new RedisManager();
            redisManager.setHost(host + ":" + port);
            if (StringUtils.isNotBlank(password))
                redisManager.setPassword(password);
            redisManager.setTimeout(timeout);
            redisManager.setDatabase(database);
            return redisManager;
    	}
    
        public RedisCacheManager redisCacheManager() {
            RedisCacheManager redisCacheManager = new RedisCacheManager();
            redisCacheManager.setRedisManager(redisManager());
            return redisCacheManager;
        }
    
    • 将RedisCacheManager设置到SecurityManager中
    	@Bean
        public SecurityManager securityManager() {
            // 配置SecurityManager,并注入ShiroRealm
            DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(shiroRealm());
            securityManager.setRememberMeManager(rememberMeManager());
            securityManager.setCacheManager(redisCacheManager());
            return securityManager;
        }
    
  • Spring Boot Thymeleaf中使用Shiro标签

    • 引入thymeleaf-extras-shiro依赖
    • 修改Shiro配置类,配置方言标签
    	@Bean
        public ShiroDialect shiroDialect() {
            return new ShiroDialect();
        }
    
    • 修改页面:在HTML页面中使用Shiro标签时,需要给html标签添加 xmlns:shiro=“http://www.pollix.at/thymeleaf/shiro”
    	<!DOCTYPE html>
    	<html xmlns:th="http://www.thymeleaf.org"
    	      xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" lang="en">
    	<head>
    	    <meta charset="UTF-8">
    	    <title>首页</title>
    	</head>
    	<body>
    		<p>你好![[${user.username}]]</p>
    		<p shiro:hasRole="admin">你的角色是超级管理员</p>
    		<p shiro:hasRole="test">你的角色是测试账户</p>
    		<div>
    		    <a shiro:hasPermission="user:user" th:href="@{/user/list}">获取用户信息</a>
    		    <a shiro:hasPermission="user:add" th:href="@{/user/add}">添加用户</a>
    		    <a shiro:hasPermission="user:delete" th:href="@{/user/delete}">删除用户</a>
    		</div>
    	<a th:href="@{/logout}">注销</a>
    	</body>
    	</html>
    
    • 其他标签:略
  • Spring Boot Shiro在线会话管理

    • 通过org.apache.shiro.session.mgt.eis.SessionDAO对象的getActiveSessions()方法获取当前所有有效的Session对象。
    • 可实现的功能
      • 查看当前系统的在线人数
      • 查看在线用户的基本信息
      • 强制让某个用户下线
    • 修改Shiro配置类,配置SessionDAO对象(若使用Redist缓存,则使用RedisSessionDAO
    	@Bean
    	public SessionDAO sessionDAO() {
    	    MemorySessionDAO sessionDAO = new MemorySessionDAO();
    	    return sessionDAO;
        }
        或
        @Bean
        public RedisSessionDAO redisSessionDAO() {
            RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
            redisSessionDAO.setRedisManager(redisManager());
            return redisSessionDAO;
        }
    
    • 修改Shiro配置类,配置SessionManager对象,用于管理SessionDAO
    	@Bean
        public SessionManager sessionManager() {
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
            Collection<SessionListener> listeners = new ArrayList<>();
            listeners.add(new ShiroSessionListener());
            sessionManager.setSessionListeners(listeners);
            sessionManager.setSessionDAO(redisSessionDAO());
            return sessionManager;
        }
    
    • 上述ShiroSessionListener类为SessionListener接口(Shiro包)的实现类,需手动创建;AtomicInteger类用于统计在线Session的数量
    	public class ShiroSessionListener implements SessionListener {
    
    	    private final AtomicInteger sessionCount = new AtomicInteger(0);
    	
    	    @Override
    	    public void onStart(Session session) {
    	        sessionCount.incrementAndGet();
    	    }
    	
    	    @Override
    	    public void onStop(Session session) {
    	        sessionCount.decrementAndGet();
    	    }
    	
    	    @Override
    	    public void onExpiration(Session session) {
    	        sessionCount.decrementAndGet();
    	    }
    	}
    
    • 修改Shiro配置类,将SessionManager对象注入SecurityManager
    	@Bean
        public SecurityManager securityManager() {
            // 配置SecurityManager,并注入ShiroRealm
            DefaultSecurityManager securityManager = new DefaultWebSecurityManager();
            securityManager.setRealm(shiroRealm());
            securityManager.setRememberMeManager(rememberMeManager());
            securityManager.setCacheManager(cacheManager());
            securityManager.setSessionManager(sessionManager());
            return securityManager;
        }
    
    • 接下来就是具体的功能实现
      • 创建UserOnline实体类
      • 创建Service接口,定义查看所有在线用户和根据SessionId踢出用户的方法
      • 实现Service接口
      • 创建SessionController
      • 编写页面
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值