Apache Shiro 安全框架

前言

Apache Shiro 是一个强大且容易使用的Java安全矿建,执行身份验证,授权,密码和会话管理。使用Shiro的易于理解的API您可以快速轻松的获得任何应用程序直到大的项目。

一丶什么是Shiro

1.Shiro是什么

Apache Shiro是一个强大且易于使用的Java安全框架,执行身份验证,授权,密码和会话管理。主要的好处是易于理解。

  • 验证用户来核实他们的身份
  • 对用户执行访问控制
  • 判断用户是否被分配了一个确定的安全角色
  • 判断用户是否被允许做某事
  • 在任何环境下使用SessionAPI,即使没有Web容器
  • 在身份验证,访问控制期间或在会话的生命周期,对事件做出反应
  • 聚集一个或多个用户安全数据的数据源,并且作为一个单一的复合用户视图
  • 启动单点登录(SSO)功能
  • 为没有关联到登录的用户做Remember Me服务

2.Shiro的功能模块

shiro可以非常容易开发出足够好用的应用,其不仅可以再JaveSE环境,也可以在JavaEE环境。Shiro可以帮助我们完成:认证授权加密会话管理,与WEB集成,缓存等。也可以与springboot集成快速开发

 

  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情。
  • Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的。
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
  • Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率。
  • Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
  • Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
  • "Run As":一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
  • "Remember Me":记住我。

二丶Shiro的内部结构

  •  Subject:主体,是任何可以与应用交互的   “技术用户”
  • Security Manager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;他管理着所有的Subject,负责进行认证,授权,会话,缓存和管理
  • Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用具觉得默认不好,可以自定义实现。需要认证策略,即认证通过的情况
  • Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作
  • Realm:可以有一个或多个Realm,可以认为是安全实体的数据源,即用于获取安全实体的,可以是JDBC实现,也可以是L:DAP实现,或者内存实现等等。由用户提供,注意:Shiro不知道你的用户/权限存储在哪,以及以何种形式存储;所有我们一般在应用中都需要实现自己的Realm;
  • sessionManager:SessionManager是管理Session会话域生命周期的组件。而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;
  • SessionDAO:数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
  • CacheManager:缓存控制器,来管理如用户,角色,权限等的缓存;因为这些数据基本上很少去改变,放到缓存中提高访问性能
  • Crayptography:密码模块,Shiro提高了一些常见的加密组件用于如密码的加密和解密

三丶应用程序使用Shiro

一个简单的Shiro应用最少应该如下: 

1.应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;

2.我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限判断

总而言之:Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入。 

四丶Shiro的入门

1.搭建基于ini的运行环境

创建工程导入shiro坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>cn.xidida</groupId>
    <artifactId>shiro_demo</artifactId>
    <version>1.0-SNAPSHOT</version>
 
    <dependencies>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
<!--        测试 -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.3</version>
        </dependency>
    </dependencies>
 
 
</project>

2.用户认证

认证:身份认证/登录,验证用户是否拥有相应的身份,基于Shiro的认证,是通过subject的login方法完成用户认证的工作

2.1在resource目录下创建爱你shiro的ini配置文件构造模拟数据(shiro_config,ini)

2.2用户授权:

 /**
     * 设置角色权限
     *
     * @param user 用户信息
     */
    public void setRolePermission(SysUser user)
    {
        List<SysRole> roles = user.getRoles();
        if (!roles.isEmpty() && roles.size() > 1)
        {
            // 多角色设置permissions属性,以便数据权限匹配权限
            for (SysRole role : roles)
            {
                Set<String> rolePerms = menuService.selectPermsByRoleId(role.getRoleId());
                role.setPermissions(rolePerms);
            }
        }
    }

2.3登录权限校验

@Component
public class SysLoginService
{
    @Autowired
    //密码
    private SysPasswordService passwordService;
    @Autowired
    //用户
    private ISysUserService userService;
    @Autowired
    //访问列表
    private ISysMenuService menuService;
    @Autowired
    //配置
    private ISysConfigService configService;

    /**
     * 登录
     */
    public SysUser login(String username, String password)
    {
        // 验证码校验
        if (ShiroConstants.CAPTCHA_ERROR.equals(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA)))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
        // 用户名或密码为空 错误
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
            throw new UserNotExistsException();
        }
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }

        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }

        // IP黑名单校验
        String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
        if (IpUtils.isMatchedIp(blackStr, ShiroUtils.getIp()))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
            throw new BlackListException();
        }

        // 查询用户信息
        SysUser user = userService.selectUserByLoginName(username);

        if (user == null)
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists")));
            throw new UserNotExistsException();
        }
        
        if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.delete")));
            throw new UserDeleteException();
        }
        
        if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
        {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked")));
            throw new UserBlockedException();
        }

        passwordService.validate(user, password);

        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        setRolePermission(user);
        recordLoginInfo(user.getUserId());
        return user;
    }

2.4注册校验

@Component
public class SysRegisterService
{
    @Autowired
    private ISysUserService userService;
    @Autowired
    private SysPasswordService passwordService;
    /**
     * 注册
     */
    public String register(SysUser user)
    {
        String msg = "", loginName = user.getLoginName(), password = user.getPassword();

        if (ShiroConstants.CAPTCHA_ERROR.equals(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA)))
        {
            msg = "验证码错误";
        }
        else if (StringUtils.isEmpty(loginName))
        {
            msg = "用户名不能为空";
        }
        else if (StringUtils.isEmpty(password))
        {
            msg = "用户密码不能为空";
        }
        else if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH)
        {
            msg = "密码长度必须在5到20个字符之间";
        }
        else if (loginName.length() < UserConstants.USERNAME_MIN_LENGTH
                || loginName.length() > UserConstants.USERNAME_MAX_LENGTH)
        {
            msg = "账户长度必须在2到20个字符之间";
        }
        else if (!userService.checkLoginNameUnique(user))
        {
            msg = "保存用户'" + loginName + "'失败,注册账号已存在";
        }
        else
        {
            user.setPwdUpdateDate(DateUtils.getNowDate());
            user.setUserName(loginName);
            user.setSalt(ShiroUtils.randomSalt());
            user.setPassword(passwordService.encryptPassword(loginName, password, user.getSalt()));
            boolean regFlag = userService.registerUser(user);
            if (!regFlag)
            {
                msg = "注册失败,请联系系统管理人员";
            }
            else
            {
                AsyncManager.me().execute(AsyncFactory.recordLogininfor(loginName, Constants.REGISTER, MessageUtils.message("user.register.success")));
            }
        }
        return msg;
    }
}

2.5自定义Realm

Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源

/**
 * 自定义Realm 处理登录 权限
 * 
 * @author ruoyi
 */
public class UserRealm extends AuthorizingRealm
{
    private static final Logger log = LoggerFactory.getLogger(UserRealm.class);
    @Autowired
    private ISysMenuService menuService;
    @Autowired
    private ISysRoleService roleService;
    @Autowired
    private SysLoginService loginService;
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0)
    {
        SysUser user = ShiroUtils.getSysUser();
        // 角色列表
        Set<String> roles = new HashSet<String>();
        // 功能列表
        Set<String> menus = new HashSet<String>();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 管理员拥有所有权限
        if (user.isAdmin())
        {
            info.addRole("admin");
            info.addStringPermission("*:*:*");
        }
        else
        {
            roles = roleService.selectRoleKeys(user.getUserId());
            menus = menuService.selectPermsByUserId(user.getUserId());
            // 角色加入AuthorizationInfo认证对象
            info.setRoles(roles);
            // 权限加入AuthorizationInfo认证对象
            info.setStringPermissions(menus);
        }
        return info;
    }

    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
    {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        String password = "";
        if (upToken.getPassword() != null)
        {
            password = new String(upToken.getPassword());
        }

        SysUser user = null;
        try
        {
            user = loginService.login(username, password);
        }
        catch (CaptchaException e)
        {
            throw new AuthenticationException(e.getMessage(), e);
        }
        catch (UserNotExistsException e)
        {
            throw new UnknownAccountException(e.getMessage(), e);
        }
        catch (UserPasswordNotMatchException e)
        {
            throw new IncorrectCredentialsException(e.getMessage(), e);
        }
        catch (UserPasswordRetryLimitExceedException e)
        {
            throw new ExcessiveAttemptsException(e.getMessage(), e);
        }
        catch (UserBlockedException e)
        {
            throw new LockedAccountException(e.getMessage(), e);
        }
        catch (RoleBlockedException e)
        {
            throw new LockedAccountException(e.getMessage(), e);
        }
        catch (Exception e)
        {
            log.info("对用户[" + username + "]进行登录验证..验证未通过{}", e.getMessage());
            throw new AuthenticationException(e.getMessage(), e);
        }
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }

    /**
     * 清理指定用户授权信息缓存
     */
    public void clearCachedAuthorizationInfo(Object principal)
    {
        SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, getName());
        this.clearCachedAuthorizationInfo(principals);
    }

    /**
     * 清理所有用户授权信息缓存
     */
    public void clearAllCachedAuthorizationInfo()
    {
        Cache<Object, AuthorizationInfo> cache = getAuthorizationCache();
        if (cache != null)
        {
            for (Object key : cache.keys())
            {
                cache.remove(key);
            }
        }
    }
}

2.6认证与授权的执行流程分析

(1)认证流程

  • 首先调用Subject .login(token)进行登录,其会自动委托给SuecrityManager,调用之前必须通过SecurityUtils.setSecurityManager()设置。
  • SecurityManager负责真正的身份验证逻辑;他会委托给Authenticator进行身份验证
  • Authenticator才是真正的身份验证者,ShiroAPI中核心的身份认证入口,此处插入自己的实现
  •  Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证,

  • Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处可以配置多个Realm,将按照相应的顺序及策略进行访问。

(2)授权流程

  1. 首先调用Subject.jsPermitted/hasRole接口,其会委托给SecurityManager,而SecurityManager接着委托给Authorize
  2. Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例;
  3. 在真正授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限;Authorize会判断Realm的角色/权限是否和传入的相匹配,如果有多个Realm,则会委托给MoudlarRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole会返回true,否则返回授权失败

五丶Shiro在SpringBoot中的使用

 

 1.案例说明

使用Springboot构建应用程序,整合shiro矿建完成用户认证与授权

1.1整合依赖

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

1.2自定义登录方法

	@RequestMapping(value="/login")
    public String login(String username,String password) {
        /**
         * 密码加密:
         *      shiro 提供了默认的密码加密  =》 Md5Hash
         *      Md5Hash():
         *          参数1:加密内容
         *          参数2:盐(加密混淆字符串)(密码+混淆字符串)==>一般是随机字符串,到时候会存入数据库中
         *          参数3:加密次数
         *
         */
        try {
            // 加密
            password = new Md5Hash(password, username, 3).toString();
            // 构造登录令牌
            UsernamePasswordToken token =  new UsernamePasswordToken(username,password);
            //1、获取subject
            Subject subject = SecurityUtils.getSubject();
            // 2、调用 subject 进行登陆
            subject.login(token);
            return "登录成功";
        }catch (Exception e){
            return "用户名或密码错误";
        }
    }

1.3自定义realm

 
 
import cn.itcast.shiro.domain.Permission;
import cn.itcast.shiro.domain.Role;
import cn.itcast.shiro.domain.User;
import cn.itcast.shiro.service.UserService;
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.springframework.beans.factory.annotation.Autowired;
 
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
 
/**
 * @author YJC
 * @e-mail xiaochun235@qq.com
 * @Date 2022/12/8 20:05
 * @notes shiro 中自定义 realm 域
 */
public class CustomReaml extends AuthorizingRealm {
    @Override
    public void setName(String name) {
        super.setName("customReaml");// 名称一般用类名,也可以自定义
    }
 
    // 进行数据库查询
    @Autowired
    private UserService userService;
    /**
     * 授权:
     *      操作时,判断用户是否具有相应的权限
     *          先认证 -- 安全数据
     *          在授权 -- 根据安全数据获取用户具有的所有操作权限
     *
     *
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 1、获取已认证的用户数据
        User user = (User) principalCollection.getPrimaryPrincipal();// 获取安全数据
        // 2、根据用户数据获取用户的权限信息,(所有角色,所有权限)
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        Set<String> roles = new HashSet<>();
        Set<String> perms = new HashSet<>();
        for(Role role:user.getRoles()){
            roles.add(role.getName());// 暂时存储 role 的名称
            for (Permission permission : role.getPermissions()){
                perms.add(permission.getCode());
            }
        }
        info.setRoles(roles);
        info.setStringPermissions(perms);
        // 返回授权
        return info;
    }
 
    /**
     * 认证:
     *      参数:用户名密码
     *
     *
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 1、获取用户名密码(token)
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        String username = token.getUsername();
        String password = new String( token.getPassword() );// token.getPassword() 返回的是一个 char数组
 
        // 2、根据用户名查询数据库
        User user = userService.findByName(username);
 
        // 3、判断用户是否存在或者密码是否一致
        if (user!=null && user.getPassword().equals(password)){
            // 4、如果一致返回安全数据
            // 构造方法:安全数据;密码,realm 域
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
            return info;
        }
        // 5、不一致,返回 null(抛出异常)
        return null;
    }
}

1.4Shiro的配置

SecurityManager 是 Shiro 架构的心脏,用于协调内部的多个组件完成全部认证授权的过程。例如通过调用realm完成认证与登录。使用基于springboot的配置方式完成SecurityManager,Realm的装配

无redis 配置:

package cn.itcast.shiro;
 
import cn.itcast.shiro.reaml.CustomReaml;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
 
/**
 * @author YJC
 * @e-mail xiaochun235@qq.com
 * @Date 2022/12/8 20:34
 * @notes shiro 配置
 */
 
@Configuration
public class ShiroConfig {
    // 1、创建 realm
    @Bean
    public CustomReaml getRealm(){
        return new CustomReaml(); //CustomReaml ==》自定义 customReaml
    }
    // 2、创建安全管理器
    @Bean
    public SecurityManager securityManager(CustomReaml reaml){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(reaml);
        return securityManager;
    }
    // 3、配置 shiro 的过滤器工厂
 
    /**
     *  在 web 程序中,shiro 进行权限控制全部是通过一组过滤器集合进行控制
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        // 1、创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        // 2、设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        // 3、通过配置(跳转登陆页面,为授权跳转的页面)
        filterFactory.setLoginUrl("/longinerror");// 跳转到url  路径 (没有登陆跳转)
        filterFactory.setUnauthorizedUrl("/autherror");//未授权的页面 (没有权限跳转)
        // 4、设置过滤器集合
        /**
         * 设置所有的过滤器:map
         *      key=拦截的 url 地址
         *      value=过滤器类型
         *      可以选择有序的 map 集合==》LinkedHashMap
         */
        Map<String,String> filterMap = new LinkedHashMap<>();
        //当前请求地址可以匿名访问
        filterMap.put("/user/home","anon");
        // swagger 访问地址开放地址
        filterMap.put("/doc.htm**","anon");
        filterMap.put("/webjars/**","anon");
        filterMap.put("/swagger**","anon");
        filterMap.put("/v2/**","anon");
        // 登陆接口
        filterMap.put("/login","anon");
 
        // 当前请求地址必须认证之后才可以访问
        filterMap.put("/**","authc");
        filterFactory.setFilterChainDefinitionMap(filterMap);
        return filterFactory;
    }
 
    // 4、开启shiro 注解的支持
    //配置shiro注解支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
 
}

redis配置,注:以下配置并不是本人项目配置,考虑到功能的完整性,以下配置是开源项目若依的shiro相关跑配置

package com.ruoyi.framework.config;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;
import org.apache.commons.io.IOUtils;
import org.apache.shiro.cache.ehcache.EhCacheManager;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.io.ResourceUtils;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.security.CipherUtils;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.shiro.realm.UserRealm;
import com.ruoyi.framework.shiro.session.OnlineSessionDAO;
import com.ruoyi.framework.shiro.session.OnlineSessionFactory;
import com.ruoyi.framework.shiro.web.CustomShiroFilterFactoryBean;
import com.ruoyi.framework.shiro.web.filter.LogoutFilter;
import com.ruoyi.framework.shiro.web.filter.captcha.CaptchaValidateFilter;
import com.ruoyi.framework.shiro.web.filter.kickout.KickoutSessionFilter;
import com.ruoyi.framework.shiro.web.filter.online.OnlineSessionFilter;
import com.ruoyi.framework.shiro.web.filter.sync.SyncOnlineSessionFilter;
import com.ruoyi.framework.shiro.web.session.OnlineWebSessionManager;
import com.ruoyi.framework.shiro.web.session.SpringSessionValidationScheduler;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

/**
 * 权限配置加载
 * 
 * @author ruoyi
 */
@Configuration
public class ShiroConfig
{
    /**
     * Session超时时间,单位为毫秒(默认30分钟)
     */
    @Value("${shiro.session.expireTime}")
    private int expireTime;

    /**
     * 相隔多久检查一次session的有效性,单位毫秒,默认就是10分钟
     */
    @Value("${shiro.session.validationInterval}")
    private int validationInterval;

    /**
     * 同一个用户最大会话数
     */
    @Value("${shiro.session.maxSession}")
    private int maxSession;

    /**
     * 踢出之前登录的/之后登录的用户,默认踢出之前登录的用户
     */
    @Value("${shiro.session.kickoutAfter}")
    private boolean kickoutAfter;

    /**
     * 验证码开关
     */
    @Value("${shiro.user.captchaEnabled}")
    private boolean captchaEnabled;

    /**
     * 验证码类型
     */
    @Value("${shiro.user.captchaType}")
    private String captchaType;

    /**
     * 设置Cookie的域名
     */
    @Value("${shiro.cookie.domain}")
    private String domain;

    /**
     * 设置cookie的有效访问路径
     */
    @Value("${shiro.cookie.path}")
    private String path;

    /**
     * 设置HttpOnly属性
     */
    @Value("${shiro.cookie.httpOnly}")
    private boolean httpOnly;

    /**
     * 设置Cookie的过期时间,秒为单位
     */
    @Value("${shiro.cookie.maxAge}")
    private int maxAge;

    /**
     * 设置cipherKey密钥
     */
    @Value("${shiro.cookie.cipherKey}")
    private String cipherKey;

    /**
     * 登录地址
     */
    @Value("${shiro.user.loginUrl}")
    private String loginUrl;

    /**
     * 权限认证失败地址
     */
    @Value("${shiro.user.unauthorizedUrl}")
    private String unauthorizedUrl;

    /**
     * 是否开启记住我功能
     */
    @Value("${shiro.rememberMe.enabled: false}")
    private boolean rememberMe;

    /**
     * 缓存管理器 使用Ehcache实现
     */
    @Bean
    public EhCacheManager getEhCacheManager()
    {
        net.sf.ehcache.CacheManager cacheManager = net.sf.ehcache.CacheManager.getCacheManager("ruoyi");
        EhCacheManager em = new EhCacheManager();
        if (StringUtils.isNull(cacheManager))
        {
            em.setCacheManager(new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream()));
            return em;
        }
        else
        {
            em.setCacheManager(cacheManager);
            return em;
        }
    }

    /**
     * 返回配置文件流 避免ehcache配置文件一直被占用,无法完全销毁项目重新部署
     */
    protected InputStream getCacheManagerConfigFileInputStream()
    {
        String configFile = "classpath:ehcache/ehcache-shiro.xml";
        InputStream inputStream = null;
        try
        {
            inputStream = ResourceUtils.getInputStreamForPath(configFile);
            byte[] b = IOUtils.toByteArray(inputStream);
            InputStream in = new ByteArrayInputStream(b);
            return in;
        }
        catch (IOException e)
        {
            throw new ConfigurationException(
                    "Unable to obtain input stream for cacheManagerConfigFile [" + configFile + "]", e);
        }
        finally
        {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * 自定义Realm
     */
    @Bean
    public UserRealm userRealm(EhCacheManager cacheManager)
    {
        UserRealm userRealm = new UserRealm();
        userRealm.setAuthorizationCacheName(Constants.SYS_AUTH_CACHE);
        userRealm.setCacheManager(cacheManager);
        return userRealm;
    }

    /**
     * 自定义sessionDAO会话
     */
    @Bean
    public OnlineSessionDAO sessionDAO()
    {
        OnlineSessionDAO sessionDAO = new OnlineSessionDAO();
        return sessionDAO;
    }

    /**
     * 自定义sessionFactory会话
     */
    @Bean
    public OnlineSessionFactory sessionFactory()
    {
        OnlineSessionFactory sessionFactory = new OnlineSessionFactory();
        return sessionFactory;
    }

    /**
     * 会话管理器
     */
    @Bean
    public OnlineWebSessionManager sessionManager()
    {
        OnlineWebSessionManager manager = new OnlineWebSessionManager();
        // 加入缓存管理器
        manager.setCacheManager(getEhCacheManager());
        // 删除过期的session
        manager.setDeleteInvalidSessions(true);
        // 设置全局session超时时间
        manager.setGlobalSessionTimeout(expireTime * 60 * 1000);
        // 去掉 JSESSIONID
        manager.setSessionIdUrlRewritingEnabled(false);
        // 定义要使用的无效的Session定时调度器
        manager.setSessionValidationScheduler(SpringUtils.getBean(SpringSessionValidationScheduler.class));
        // 是否定时检查session
        manager.setSessionValidationSchedulerEnabled(true);
        // 自定义SessionDao
        manager.setSessionDAO(sessionDAO());
        // 自定义sessionFactory
        manager.setSessionFactory(sessionFactory());
        return manager;
    }

    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm)
    {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(userRealm);
        // 记住我
        securityManager.setRememberMeManager(rememberMe ? rememberMeManager() : null);
        // 注入缓存管理器;
        securityManager.setCacheManager(getEhCacheManager());
        // session管理器
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * 退出过滤器
     */
    public LogoutFilter logoutFilter()
    {
        LogoutFilter logoutFilter = new LogoutFilter();
        logoutFilter.setLoginUrl(loginUrl);
        return logoutFilter;
    }

    /**
     * Shiro过滤器配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager)
    {
        CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
        // Shiro的核心安全接口,这个属性是必须的
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 身份认证失败,则跳转到登录页面的配置
        shiroFilterFactoryBean.setLoginUrl(loginUrl);
        // 权限认证失败,则跳转到指定页面
        shiroFilterFactoryBean.setUnauthorizedUrl(unauthorizedUrl);
        // Shiro连接约束配置,即过滤链的定义
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 对静态资源设置匿名访问
        filterChainDefinitionMap.put("/favicon.ico**", "anon");
        filterChainDefinitionMap.put("/ruoyi.png**", "anon");
        filterChainDefinitionMap.put("/html/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/docs/**", "anon");
        filterChainDefinitionMap.put("/fonts/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/ajax/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/ruoyi/**", "anon");
        filterChainDefinitionMap.put("/captcha/captchaImage**", "anon");
        // 退出 logout地址,shiro去清除session
        filterChainDefinitionMap.put("/logout", "logout");
        // 不需要拦截的访问
        filterChainDefinitionMap.put("/login", "anon,captchaValidate");
        // 注册相关
        filterChainDefinitionMap.put("/register", "anon,captchaValidate");
        // 系统权限列表
        // filterChainDefinitionMap.putAll(SpringUtils.getBean(IMenuService.class).selectPermsAll());

        Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
        filters.put("onlineSession", onlineSessionFilter());
        filters.put("syncOnlineSession", syncOnlineSessionFilter());
        filters.put("captchaValidate", captchaValidateFilter());
        filters.put("kickout", kickoutSessionFilter());
        // 注销成功,则跳转到指定页面
        filters.put("logout", logoutFilter());
        shiroFilterFactoryBean.setFilters(filters);

        // 所有请求需要认证
        filterChainDefinitionMap.put("/**", "user,kickout,onlineSession,syncOnlineSession");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * 自定义在线用户处理过滤器
     */
    public OnlineSessionFilter onlineSessionFilter()
    {
        OnlineSessionFilter onlineSessionFilter = new OnlineSessionFilter();
        onlineSessionFilter.setLoginUrl(loginUrl);
        onlineSessionFilter.setOnlineSessionDAO(sessionDAO());
        return onlineSessionFilter;
    }

    /**
     * 自定义在线用户同步过滤器
     */
    public SyncOnlineSessionFilter syncOnlineSessionFilter()
    {
        SyncOnlineSessionFilter syncOnlineSessionFilter = new SyncOnlineSessionFilter();
        syncOnlineSessionFilter.setOnlineSessionDAO(sessionDAO());
        return syncOnlineSessionFilter;
    }

    /**
     * 自定义验证码过滤器
     */
    public CaptchaValidateFilter captchaValidateFilter()
    {
        CaptchaValidateFilter captchaValidateFilter = new CaptchaValidateFilter();
        captchaValidateFilter.setCaptchaEnabled(captchaEnabled);
        captchaValidateFilter.setCaptchaType(captchaType);
        return captchaValidateFilter;
    }

    /**
     * cookie 属性设置
     */
    public SimpleCookie rememberMeCookie()
    {
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        cookie.setDomain(domain);
        cookie.setPath(path);
        cookie.setHttpOnly(httpOnly);
        cookie.setMaxAge(maxAge * 24 * 60 * 60);
        return cookie;
    }

    /**
     * 记住我
     */
    public CookieRememberMeManager rememberMeManager()
    {
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        cookieRememberMeManager.setCookie(rememberMeCookie());
        if (StringUtils.isNotEmpty(cipherKey))
        {
            cookieRememberMeManager.setCipherKey(Base64.decode(cipherKey));
        }
        else
        {
            cookieRememberMeManager.setCipherKey(CipherUtils.generateNewKey(128, "AES").getEncoded());
        }
        return cookieRememberMeManager;
    }

    /**
     * 同一个用户多设备登录限制
     */
    public KickoutSessionFilter kickoutSessionFilter()
    {
        KickoutSessionFilter kickoutSessionFilter = new KickoutSessionFilter();
        kickoutSessionFilter.setCacheManager(getEhCacheManager());
        kickoutSessionFilter.setSessionManager(sessionManager());
        // 同一个用户最大的会话数,默认-1无限制;比如2的意思是同一个用户允许最多同时两个人登录
        kickoutSessionFilter.setMaxSession(maxSession);
        // 是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序
        kickoutSessionFilter.setKickoutAfter(kickoutAfter);
        // 被踢出后重定向到的地址;
        kickoutSessionFilter.setKickoutUrl("/login?kickout=1");
        return kickoutSessionFilter;
    }

    /**
     * thymeleaf模板引擎和shiro框架的整合
     */
    @Bean
    public ShiroDialect shiroDialect()
    {
        return new ShiroDialect();
    }

    /**
     * 开启Shiro注解通知器
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            @Qualifier("securityManager") SecurityManager securityManager)
    {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

1.5基于注解的授权

package cn.itcast.shiro;
 
import cn.itcast.shiro.reaml.CustomReaml;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
 
/**
 * @author YJC
 * @e-mail xiaochun235@qq.com
 * @Date 2022/12/8 20:34
 * @notes shiro 配置
 */
 
@Configuration
public class ShiroConfig {
    // 1、创建 realm
    @Bean
    public CustomReaml getRealm(){
        return new CustomReaml(); //CustomReaml ==》自定义 customReaml
    }
    // 2、创建安全管理器
    @Bean
    public SecurityManager securityManager(CustomReaml reaml){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(reaml);
        return securityManager;
    }
    // 3、配置 shiro 的过滤器工厂
 
    /**
     *  在 web 程序中,shiro 进行权限控制全部是通过一组过滤器集合进行控制
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        // 1、创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        // 2、设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        // 3、通过配置(跳转登陆页面,为授权跳转的页面)
        filterFactory.setLoginUrl("/longinerror");// 跳转到url  路径 (没有登陆跳转)
        filterFactory.setUnauthorizedUrl("/autherror");//未授权的页面 (没有权限跳转)
        // 4、设置过滤器集合
        /**
         * 设置所有的过滤器:map
         *      key=拦截的 url 地址
         *      value=过滤器类型
         *      可以选择有序的 map 集合==》LinkedHashMap
         */
        Map<String,String> filterMap = new LinkedHashMap<>();
 
 
        // 不需要登录 即可访问
        //当前请求地址可以匿名访问
        filterMap.put("/user/home","anon");
        // 当前请求地址可以匿名访问  swagger 访问地址开放地址
        filterMap.put("/doc.htm**","anon");
        filterMap.put("/webjars/**","anon");
        filterMap.put("/swagger**","anon");
        filterMap.put("/v2/**","anon");
        // 登陆接口
        filterMap.put("/login","anon");
 
 
        // 当前请求地址必须认证之后才可以访问  需要登录才能访问
        filterMap.put("/user**","authc");
        filterMap.put("/test","authc");
 
        // 使用 过滤器 的形式来配置 权限
        // 需要当前用户 拥有 该角色  才能访问  role.getName()  在 realm 中的授权存入内容的
//        filterMap.put("/user/list","roles[系统管理员]");
 
        // 需要有权限才能访问
        // 具有 某种权限才能访问 查询用户 当有 user-find 该权限才能访问该接口
        // 也可以使用 注解的形式来配置 权限  user-find ==》为 permission.getCode() 在 realm 中的授权存入内容的
//        filterMap.put("/user/list","perms[user-list]");// 如果不具有这个权限则就跳转到 setUnauthorizedUrl 改接口
 
 
 
        filterFactory.setFilterChainDefinitionMap(filterMap);
        return filterFactory;
    }
 
    // 4、开启shiro 注解的支持
    //配置shiro注解支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
 
}

1.6controller

  /**
     * import org.apache.shiro.authz.annotation.RequiresRoles;
     * import org.apache.shiro.authz.annotation.RequiresPermissions;
     * 使用 shiro 注解的形式实现  角色  及其 权限 授权
     *  @RequiresPermissions() :访问当前方法必须具备的权限
     *  @RequiresRoles()   : 访问当前方法所具备的角色
     * @return
     * 1、过滤器:如果权限信息不匹配 则会运行改方法 setUnauthorizedUrl("/autherror");
     * 2、注解:如果没有权限则会 抛出异常 :AuthorizationException
     * @RequiresPermissions("user-list")
     * @RequiresRoles("系统管理员")
     */
    @RequiresPermissions("user-list")
//    @RequiresRoles()
    @ApiOperation("查询用户")
    @RequestMapping(value = "/user/list",method = RequestMethod.GET)
    public String find() {
        return "查询用户成功";
    }

1.7异常处理

package com.ruoyi.framework.web.exception;

import javax.servlet.http.HttpServletRequest;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.ModelAndView;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.DemoModeException;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.security.PermissionUtils;

/**
 * 全局异常处理器
 * 
 * @author ruoyi
 */
@RestControllerAdvice
public class GlobalExceptionHandler
{
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 权限校验异常(ajax请求返回json,redirect请求跳转页面)
     */
    @ExceptionHandler(AuthorizationException.class)
    public Object handleAuthorizationException(AuthorizationException e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',权限校验失败'{}'", requestURI, e.getMessage());
        if (ServletUtils.isAjaxRequest(request))
        {
            return AjaxResult.error(PermissionUtils.getMsg(e.getMessage()));
        }
        else
        {
            return new ModelAndView("error/unauth");
        }
    }

    /**
     * 请求方式不支持
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public AjaxResult handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
            HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 拦截未知的运行时异常
     */
    @ExceptionHandler(RuntimeException.class)
    public AjaxResult handleRuntimeException(RuntimeException e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生未知异常.", requestURI, e);
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 系统异常
     */
    @ExceptionHandler(Exception.class)
    public AjaxResult handleException(Exception e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求地址'{}',发生系统异常.", requestURI, e);
        return AjaxResult.error(e.getMessage());
    }

    /**
     * 业务异常
     */
    @ExceptionHandler(ServiceException.class)
    public Object handleServiceException(ServiceException e, HttpServletRequest request)
    {
        log.error(e.getMessage(), e);
        if (ServletUtils.isAjaxRequest(request))
        {
            return AjaxResult.error(e.getMessage());
        }
        else
        {
            return new ModelAndView("error/service", "errorMessage", e.getMessage());
        }
    }

    /**
     * 请求路径中缺少必需的路径变量
     */
    @ExceptionHandler(MissingPathVariableException.class)
    public AjaxResult handleMissingPathVariableException(MissingPathVariableException e, HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求路径中缺少必需的路径变量'{}',发生系统异常.", requestURI, e);
        return AjaxResult.error(String.format("请求路径中缺少必需的路径变量[%s]", e.getVariableName()));
    }

    /**
     * 请求参数类型不匹配
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public AjaxResult handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e,
            HttpServletRequest request)
    {
        String requestURI = request.getRequestURI();
        log.error("请求参数类型不匹配'{}',发生系统异常.", requestURI, e);
        return AjaxResult.error(String.format("请求参数类型不匹配,参数[%s]要求类型为:'%s',但输入值为:'%s'", e.getName(), e.getRequiredType().getName(), e.getValue()));
    }

    /**
     * 自定义验证异常
     */
    @ExceptionHandler(BindException.class)
    public AjaxResult handleBindException(BindException e)
    {
        log.error(e.getMessage(), e);
        String message = e.getAllErrors().get(0).getDefaultMessage();
        return AjaxResult.error(message);
    }

    /**
     * 演示模式异常
     */
    @ExceptionHandler(DemoModeException.class)
    public AjaxResult handleDemoModeException(DemoModeException e)
    {
        return AjaxResult.error("演示模式,不允许操作");
    }
}

六丶Shiro中会话管理

在shiro中所有的用户的会话信息都回由Shiro来进行控制,Shiro提供的会话可以用JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。通过Shiro的会话管理器(SessionManager)进行统一的会话管理

1.什么是shiro会话管理

SessionManager(会话管理器):管理所有Subject的session包括创建,维护,删除,失效,验证等工作。SessionManager是顶层组件,由SecurityManager管理shiro提供了说那个默认实现:

1.DefaultSessionManager:用于JavaSE环境

2.ServletContainerSessionManager:用于Web环境,直接使用servlet容器的会话。

3.DefaultWebSessionManager:用于web环境,自己维护会话(相当于直接舍弃了Servlet容器的会话管理)。

在web程序中,通过shiro的Subject.login()方法登录成功后,用户的认证信息实际上是保存在HttpSession中的通过如下代码验证。

//登录成功后,打印所有session内容
    @ApiOperation("查看session中存储的shiro信息")
    @RequestMapping(value="/show",method = RequestMethod.GET)
    public String show(HttpSession session) {
        // 获取session中所有的键值
        Enumeration<?> enumeration = session.getAttributeNames();
        // 遍历enumeration中的
        while (enumeration.hasMoreElements()) {
            // 获取session键值
            String name = enumeration.nextElement().toString();
            // 根据键值取session中的值
            Object value = session.getAttribute(name);
            // 打印结果
            System.out.println("<B>" + name + "</B>=" + value + "<br>/n");
        }
        return "查看session成功";
    }

如果是分布式分布式系统或者微服务架构,简单的web session域已经不能实现功能,都是通过统一的认证中心尽显用户认证。如果使用默认的会话管理,用户信息只会保存到一台服务器上。那么其他的服务器就要进行会话同步。

会话管理器可以指定sessionId的生成以及获取方式。
通过sessionDao完成模拟session存入,取出等操作  

Shiro结合redis的统一会话管理

1.流程分析:

2. 整合redis:

<dependency>
    <groupId>org.crazycake</groupId>
        <artifactId>shiro-redis</artifactId>
    <version>3.0.0</version>
</dependency>

3. 在springboot配置文件中添加redis配置

4.自定义shiro会话管理器

 
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;
 
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
 
/**
 * @author YJC
 * @e-mail xiaochun235@qq.com
 * @Date 2022/12/12 22:58
 * @notes  自定义 Manager
 */
public class CustomSessionManager  extends DefaultWebSessionManager {
    /**
     * 头信息中具有sessionId
     *      请求头:Authorization:  sessionid
     *
     *
     * 指定sessionId的一个方法
     */
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //获取请求头中的信息
        String sessionId = WebUtils.toHttp(request).getHeader("Authorization");
        if (StringUtils.isEmpty(sessionId)){
            // 如果没有携带,生成新的session
            return super.getSessionId(request,response);
        }else {
            // 返回 sessionId
            // 将 sessionId 放到请求头中
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
            // 当前 sessionid 具体是什么
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            //该sessionId是否需要验证
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
 
            return sessionId;
        }
    }
}

5.总配置

package cn.itcast.shiro;
 
import cn.itcast.shiro.reaml.CustomReaml;
 
import cn.itcast.shiro.session.CustomSessionManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
 
/**
 * @author YJC
 * @e-mail xiaochun235@qq.com
 * @Date 2022/12/8 20:34
 * @notes shiro 配置
 */
 
@Configuration
public class ShiroConfig {
    // 1、创建 realm
    @Bean
    public CustomReaml getRealm(){
        return new CustomReaml(); //CustomReaml ==》自定义 customReaml
    }
    // 2、创建安全管理器
    @Bean
    public SecurityManager securityManager(CustomReaml reaml){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(reaml);
 
        //redis 将自定义的会话管理器注册到安全管理器中
        securityManager.setSessionManager(sessionManager());
        // redis 将自定义的 redis 缓存管理器注册到安全管理器中
        securityManager.setCacheManager(cacheManager());
 
        return securityManager;
    }
    // 3、配置 shiro 的过滤器工厂
 
    /**
     *  在 web 程序中,shiro 进行权限控制全部是通过一组过滤器集合进行控制
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
        // 1、创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        // 2、设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        // 3、通过配置(跳转登陆页面,为授权跳转的页面)
        filterFactory.setLoginUrl("/longinerror");// 跳转到url  路径 (没有登陆跳转)
        filterFactory.setUnauthorizedUrl("/autherror");//未授权的页面 (没有权限跳转)
        // 4、设置过滤器集合
        /**
         * 设置所有的过滤器:map
         *      key=拦截的 url 地址
         *      value=过滤器类型
         *      可以选择有序的 map 集合==》LinkedHashMap
         */
        Map<String,String> filterMap = new LinkedHashMap<>();
 
 
        // 不需要登录 即可访问
        //当前请求地址可以匿名访问
        filterMap.put("/user/home","anon");
        // 当前请求地址可以匿名访问  swagger 访问地址开放地址
        filterMap.put("/doc.htm**","anon");
        filterMap.put("/webjars/**","anon");
        filterMap.put("/swagger**","anon");
        filterMap.put("/v2/**","anon");
        // 登陆接口
        filterMap.put("/login","anon");
 
 
        // 当前请求地址必须认证之后才可以访问  需要登录才能访问
        filterMap.put("/user**","authc");
        filterMap.put("/test","authc");
 
        // 使用 过滤器 的形式来配置 权限
        // 需要当前用户 拥有 该角色  才能访问  role.getName()  在 realm 中的授权存入内容的
//        filterMap.put("/user/list","roles[系统管理员]");
 
        // 需要有权限才能访问
        // 具有 某种权限才能访问 查询用户 当有 user-find 该权限才能访问该接口
        // 也可以使用 注解的形式来配置 权限  user-find ==》为 permission.getCode() 在 realm 中的授权存入内容的
//        filterMap.put("/user/list","perms[user-list]");// 如果不具有这个权限则就跳转到 setUnauthorizedUrl 改接口
 
 
 
        filterFactory.setFilterChainDefinitionMap(filterMap);
        return filterFactory;
    }
    // 获取redis 配置
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.password}")
    private String password;
 
    /**
     *1、redis 配置 控制器
     */
    public RedisManager redisManager(){
        RedisManager redisManager = new RedisManager();
        redisManager.setPort(port);
        redisManager.setHost(host);
        redisManager.setPassword(password);
        return redisManager;
    }
    /**
     * 2、redis SessionDao
     * @return
     */
    public RedisSessionDAO redisSessionDAO(){
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }
 
    /**
     * 3、redis 配置自定义会话管理器
     * @return
     */
    public DefaultWebSessionManager sessionManager(){
        CustomSessionManager customSessionManager = new CustomSessionManager();
        customSessionManager.setSessionDAO(redisSessionDAO());
         // 禁用 cookie (可写)
        customSessionManager.setSessionIdCookieEnabled(false);
        // 禁用 url 重写 url;jsessionid=id (可写)
        customSessionManager.setSessionIdUrlRewritingEnabled(false);
        return customSessionManager;
    }
 
    /**
     * 4、Redis 缓存管理器
     * @return
     */
    public RedisCacheManager cacheManager(){
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }
 
 
    // 4、开启shiro 注解的支持
    //配置shiro注解支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
 
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/348891.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

5.列表选择弹窗(BottomListPopup)

愿你出走半生,归来仍是少年&#xff01; 环境&#xff1a;.NET 7、MAUI 从底部弹出的列表选择弹窗。 1.布局 <?xml version"1.0" encoding"utf-8" ?> <toolkit:Popup xmlns"http://schemas.microsoft.com/dotnet/2021/maui"xmlns…

防火墙在企业园区出口安全方案中的应用(ENSP实现)

拓扑图 需求&#xff1a; 1、企业出口网关设备必须具备较高的可靠性&#xff0c;为了避免单点故障&#xff0c;要求两台设备形成双机热备状态。当一台设备发生故障时&#xff0c;另一台设备会接替其工作&#xff0c;不会影响业务正常运行。 2、企业从两个ISP租用了两条链路&…

【QT】二进制文件读写

目录 1 实例功能概述 2 Qt预定义编石马文件的读写 2.1 保存为文件 2.2 stm文件格式 2.3 读取stm文件 3 标准编码文件的读写 3.1 保存为dat文件 3.2 dat文件格式 3.3 读取dat文件 文件的读写是很多应用程序具有的功能&#xff0c;甚至某些应用程序就是围绕着某一种格式文件的处 …

[docker] Docker的数据卷、数据卷容器,容器互联

一、数据卷&#xff08;容器与宿主机之间数据共享&#xff09; 数据卷是一个供容器使用的特殊目录&#xff0c;位于容器中。可将宿主机的目录挂载到数据卷上&#xff0c;对数据卷的修改操作立刻可见&#xff0c;并且更新数据不会影响镜像&#xff0c;从而实现数据在宿主机与容…

开发知识点-Flutter移动应用开发

支持 安卓 IOS Android 鸿蒙 第一章dart基础章节介绍 移动电商——Flutter-广告Banner组件制作 移动电商——Flutter实战课程介绍 Flutter实例——路由跳转的动画效果

禅道(HIS医疗系统)项目管理

文章目录 前言禅道的基本使用指南本次讲解举例参与人员&#xff1a;一、admin管理组织结构1.1批量新增用户 二、产品经理使用禅道2.1以陈雪燕账号去创建产品2.2添加产品模块2.3添加产品计划2.4添加产品需求2.5创建项目4.6设置团队 三、项目经理使用禅道3.1关联需求3.2分解任务 …

【寒假每日一题·2024】AcWing 4965. 三国游戏(补)

文章目录 一、题目1、原题链接2、题目描述 二、解题报告1、思路分析2、时间复杂度3、代码详解 一、题目 1、原题链接 4965. 三国游戏 2、题目描述 二、解题报告 1、思路分析 思路参考y总&#xff1a;y总讲解视频 &#xff08;1&#xff09;题目中的获胜情况分为三种&#xff…

【Servlet】如何编写第一个Servlet程序

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【Servlet】 本专栏旨在分享学习Servlet的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; Servlet是Java编写的服务器端…

《WebKit 技术内幕》学习之十三(1):移动WebKit

1 触控和手势事件 1.1 HTML5规范 随着电容屏幕的流行&#xff0c;触控操作变得前所未有的流行起来。时至今日&#xff0c;带有多点触控功能已经成为了移动设备的标准配置&#xff0c;基于触控的手势识别技术也获得巨大的发展&#xff0c;如使用两个手指来缩放应用的大小等。…

深度学习(6)---Transformer

文章目录 一、介绍二、架构2.1 Multi-head Attention2.2 Encoder(编码器)2.3 Decoder(解码器) 三、Encoder和Decoder之间的传递四、Training五、其他介绍5.1 Copy Mechanism5.2 Beam Search 一、介绍 1. Transformer是一个Seq2Seq&#xff08;Sequence-to-Sequence&#xff09;…

Christmas Log Village Pack (Interior / Exterior) - VR/Mobile

这个圣诞主题的包包含了建造一个美丽的雪村所需的一切! 现在已更新为Unity 2019.3(与Unity 4以来的所有Unity版本兼容) 该包针对移动设备进行了优化,每个道具仅有两个纹理图集(外部和内部),2个雪地纹理,2个房屋地面纹理。该包包含大约150个预制件,还包括演示场景。 每…

【计算机网络】协议,电路交换,分组交换

定义了在两个或多个通信实体之间交换的报文格式和次序,以及报文发送和/或接收一个报文或其他事件所采取的动作.网络边缘: 端系统 (因为处在因特网的边缘) 主机 端系统 客户 client服务器 server今天大部分服务器都属于大型数据中心(data center)接入网(access network) 指将端…

项目解决方案:非执法视频监控系统项目设计方案

目 录 一、概述 &#xff08;一&#xff09;前言 &#xff08;二&#xff09;设计思路 &#xff08;三&#xff09;设计原则 1、实用性 2、可靠性 3、安全性 4、先进性 5、开放性 6、易管理、易维护 &#xff08;四&#xff09;设计依据 二、方案总…

【QT+QGIS跨平台编译】之十:【libbz2+Qt跨平台编译】(一套代码、一套框架,跨平台编译)

文章目录 一、libbz2介绍二、文件下载三、文件分析四、pro文件五、编译实践一、libbz2介绍 bzip2是一个基于Burrows-Wheeler 变换的无损压缩软件,压缩效果比传统的LZ77/LZ78压缩算法来得好。它是一款免费软件。可以自由分发免费使用。 bzip2能够进行高质量的数据压缩。它利用…

Spring Boot如何统计一个Bean中方法的调用次数

目录 实现思路 前置条件 实现步骤 首先我们先自定义一个注解 接下来定义一个切面 需要统计方法上使用该注解 测试 实现思路 通过AOP即可实现&#xff0c;通过AOP对Bean进行代理&#xff0c;在每次执行方法前或者后进行几次计数统计。这个主要就是考虑好如何避免并发情况…

Gradle学习笔记:Gradle的使用方法

文章目录 1.初始化项目2.构建脚本语言选择3.项目命名4.项目构建过程 1.初始化项目 创建一个test空文件夹&#xff0c;在该文件夹下打开终端&#xff0c;并执行命令&#xff1a;gradle init. 会有一个选项让你选择项目的类型。下面是每个选项的含义和用途&#xff1a; basic&am…

腾讯LLaMA Pro大模型:突破大模型微调的知识遗忘难题

引言&#xff1a;大模型微调中的挑战 在人工智能的发展过程中&#xff0c;大型语言模型&#xff08;LLM&#xff09;的微调&#xff08;fine-tuning&#xff09;始终是提升模型在特定任务上性能的关键。然而&#xff0c;微调过程中常面临一个主要挑战&#xff1a;知识遗忘。这…

【TCP】传输控制协议

前言 TCP&#xff08;Transmission Control Protocol&#xff09;即传输控制协议&#xff0c;是一种面向连接的、可靠的、基于字节流的传输层通信协议。它由IETF的RFC 793定义&#xff0c;为互联网中的数据通信提供了稳定的传输机制。TCP在不可靠的IP层之上实现了数据传输的可…

HCIE之BGP正则表达式(四)

BGP 一、AS-Path正则表达式数字| 等同于或的关系[]和.$ 一个字符串的结束_代表任意^一个字符串的开始()括号包围的是一个组合\ 转义字符* 零个或多个&#xff1f;零个或一个一个或多个 二、BGP对等体组 一、AS-Path正则表达式 正则表达式是按照一定模版匹配字符串的公式 AR3上…

数字孪生系统的难点

数字孪生系统的开发和实施涉及一些技术难点&#xff0c;这些难点需要综合应用多个领域的知识和技术来克服。以下是一些数字孪生系统开发中的技术难点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1…
最新文章