• shrio总结


     

    AccessControlFilter(https://www.jianshu.com/p/9bfa22b0e905)

    SpringBoot+Shiro学习之自定义拦截器管理在线用户(踢出用户)

     

    应用场景

    1. 我们经常会有用到,当A 用户在北京登录 ,然后A用户在天津再登录 ,要踢出北京登录的状态。如果用户在北京重新登录,那么又要踢出天津的用户,这样反复。又或是需要限制同一用户的同时在线数量,超出限制后,踢出最先登录的或是踢出最后登录的。

    2. 第一个场景踢出用户是由用户触发的,有时候需要手动将某个在线用户踢出,也就是对当前在线用户的列表进行管理。

    ·························································································································································
    个人博客:http://z77z.oschina.io/

    此项目下载地址:https://git.oschina.net/z77z/springboot_mybatisplus
    ························································································································································

    实现思路

    spring security就直接提供了相应的功能;Shiro的话没有提供默认实现,不过可以很容易的在Shiro中加入这个功能。那就是使用shiro强大的自定义访问控制拦截器:AccessControlFilter,集成这个接口后要实现下面这三个方法。

    abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;  
    
    boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception; 
     
    abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;   
    

    isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;

    onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。

    onPreHandle:会自动调用这两个方法决定是否继续处理;

    另外AccessControlFilter还提供了如下方法用于处理如登录成功后/重定向到上一个请求:

    void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp  
    String getLoginUrl()  
    Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例  
    boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求  
    void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面  
    void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求  
    void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面   
    

    比如基于表单的身份验证就需要使用这些功能。

    到此基本的拦截器就完事了,如果我们想进行访问的控制就可以继承AccessControlFilter;如果我们要添加一些通用数据我们可以直接继承PathMatchingFilter。

    下面就是我实现的访问控制拦截器:KickoutSessionControlFilter:

    /**
     * @author 作者 z77z
     * @date 创建时间:2017年3月5日 下午1:16:38
     * 思路:
     * 1.读取当前登录用户名,获取在缓存中的sessionId队列
     * 2.判断队列的长度,大于最大登录限制的时候,按踢出规则
     *  将之前的sessionId中的session域中存入kickout:true,并更新队列缓存
     * 3.判断当前登录的session域中的kickout如果为true,
     * 想将其做退出登录处理,然后再重定向到踢出登录提示页面
     */
    public class KickoutSessionControlFilter extends AccessControlFilter {
    
        private String kickoutUrl; //踢出后到的地址
        private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
        private int maxSession = 1; //同一个帐号最大会话数 默认1
    
        private SessionManager sessionManager;
        private Cache<String, Deque<Serializable>> cache;
    
        public void setKickoutUrl(String kickoutUrl) {
            this.kickoutUrl = kickoutUrl;
        }
    
        public void setKickoutAfter(boolean kickoutAfter) {
            this.kickoutAfter = kickoutAfter;
        }
    
        public void setMaxSession(int maxSession) {
            this.maxSession = maxSession;
        }
    
        public void setSessionManager(SessionManager sessionManager) {
            this.sessionManager = sessionManager;
        }
        //设置Cache的key的前缀
        public void setCacheManager(CacheManager cacheManager) {
            this.cache = cacheManager.getCache("shiro_redis_cache");
        }
    
        @Override
        protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
            return false;
        }
    
        @Override
        protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
            Subject subject = getSubject(request, response);
            if(!subject.isAuthenticated() && !subject.isRemembered()) {
                //如果没有登录,直接进行之后的流程
                return true;
            }
    
            Session session = subject.getSession();
            SysUser user = (SysUser) subject.getPrincipal();
            String username = user.getNickname();
            Serializable sessionId = session.getId();
    
            //读取缓存   没有就存入
            Deque<Serializable> deque = cache.get(username);
            
            //如果队列里没有此sessionId,且用户没有被踢出;放入队列
            if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
                //将sessionId存入队列
                deque.push(sessionId);
                //将用户的sessionId队列缓存
                cache.put(username, deque);
            }
    
            //如果队列里的sessionId数超出最大会话数,开始踢人
            while(deque.size() > maxSession) {
                Serializable kickoutSessionId = null;
                if(kickoutAfter) { //如果踢出后者
                    kickoutSessionId = deque.removeFirst();
                } else { //否则踢出前者
                    kickoutSessionId = deque.removeLast();
                }
                //踢出后再更新下缓存队列
                cache.put(username, deque);
                
                
                try {
                    //获取被踢出的sessionId的session对象
                    Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                    if(kickoutSession != null) {
                        //设置会话的kickout属性表示踢出了
                        kickoutSession.setAttribute("kickout", true);
                    }
                } catch (Exception e) {//ignore exception
                }
            }
    
            //如果被踢出了,直接退出,重定向到踢出后的地址
            if ((Boolean)session.getAttribute("kickout")!=null&&(Boolean)session.getAttribute("kickout") == true) {
                //会话被踢出了
                try {
                    //退出登录
                    subject.logout();
                } catch (Exception e) { //ignore
                }
                saveRequest(request);
                //重定向
                WebUtils.issueRedirect(request, response, kickoutUrl);
                return false;
            }
            return true;
        }
    }
    
    

    将这个自定义的拦截器配置在ShiroConfig.java文件中:

    /**
      * 限制同一账号登录同时登录人数控制
      * @return
      */
     public KickoutSessionControlFilter kickoutSessionControlFilter(){
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        //使用cacheManager获取相应的cache来缓存用户登录的会话;用于保存用户—会话之间的关系的;
        //这里我们还是用之前shiro使用的redisManager()实现的cacheManager()缓存管理
        //也可以重新另写一个,重新配置缓存时间之类的自定义缓存属性
        kickoutSessionControlFilter.setCacheManager(cacheManager());
        //用于根据会话ID,获取会话进行踢出操作的;
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        //是否踢出后来登录的,默认是false;即后者登录的用户踢出前者登录的用户;踢出顺序。
        kickoutSessionControlFilter.setKickoutAfter(false);
        //同一个用户最大的会话数,默认1;比如2的意思是同一个用户允许最多同时两个人登录;
        kickoutSessionControlFilter.setMaxSession(1);
        //被踢出后重定向到的地址;
        kickoutSessionControlFilter.setKickoutUrl("/kickout");
         return kickoutSessionControlFilter;
      }
    

    将这个kickoutSessionControlFilter()注入到shiroFilterFactoryBean中:

    //自定义拦截器
    Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
    //限制同一帐号同时在线的个数。
    filtersMap.put("kickout", kickoutSessionControlFilter());
    shiroFilterFactoryBean.setFilters(filtersMap);
    

    由于我们链接权限的控制是动态存在数据库中的,这个可以去看我之前动态权限控制的博文,所以我们还要在数据库中修改链接的权限,将kickout这个自定义的权限配置在对应的链接上。如下图:

     
    权限表

    还要编写对应的被踢出的跳转页面:

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%
        String path = request.getContextPath();
        String basePath = request.getScheme() + "://"
                + request.getServerName() + ":" + request.getServerPort()
                + path;
    %>
    <!DOCTYPE html>
    <html>
    <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <script type="text/javascript"
        src="<%=basePath%>/static/js/jquery-1.11.3.js"></script>
    <title>被踢出</title>
    </head>
    <body>
    被踢出 或则在另一地方登录,或已经达到此账号登录上限被挤掉。
    <input type="button" id="login" value="重新登录" />
    </body>
    <script type="text/javascript">
    $("#login").click(function(){
        window.open("<%=basePath%>/login"); 
    });
    </script>
    </html>
    

    到此,第一个场景就实现了,写到这里实际第二个场景的实现思路已经就很明显了,可以通过sessionDAO获取到全部的shiro会话List,然后显示在前端页面,踢出对应用户就可以使用在对应sessionId的session域中设置key为kickout的值为true,上面的KickoutSessionControlFilter就会判断session域中的kickout值,做响应的处理。这里我就先不上代码了,大家可以自己试一试。之后再把代码同步到我的码云上,供大家学习交流。

    Shiro 之 HashedCredentialsMatcher 认证匹配

    Shiro 提供了用于加密密码和验证密码服务的 CredentialsMatcher 接口,而 HashedCredentialsMatcher 正是 CredentialsMatcher 的一个实现类。写项目的话,总归会用到用户密码的非对称加密,目前主流的非对称加密方式是 MD5 ,以及在 MD5 上的加盐处理,而 HashedCredentialsMatcher 也允许我们指定自己的算法和盐。本文将介绍 HashedCredentialsMatcher 的使用,以及对相关源码的进行解析,MD5 等加密知识请自行查阅。

    HashedCredentialsMatcher 的使用
    要使用 HashedCredentialsMatcher ,那么首先要进行配置。当然,前提是你已经引入了 Shiro 库。总共有三种配置方式:

    XML 格式

    <bean id="credentialsMatcher"
    class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    <!-- 加密方式 -->
    <property name="hashAlgorithmName" value="MD5" />
    <!-- 加密次数 -->
    <property name="hashIterations" value="2" />
    <!-- 存储散列后的密码是否为16进制 -->
    <property name="storedCredentialsHexEncoded" value="true" />
    </bean>


    ini 等配置文件

    首先在 web.xml 中自定义 shiro.ini 位置

    <filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
    <init-param>
    <param-name>configPath</param-name>
    <param-value>/WEB-INF/shiro.ini</param-value>
    </init-param>
    </filter>


    然后除了 shiro 的通常配置之外,需加上:

    credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
    ## 加密方式
    credentialsMatcher.hashAlgorithmName=md5
    ## 加密次数
    credentialsMatcher.hashIterations=2
    ## 存储散列后的密码是否为16进制
    credentialsMatcher.storedCredentialsHexEncoded=true


    建立 ShiroConfiguration 配置类,除了 shiro 的通常配置之外,需加上:

    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
    //加密方式
    hashedCredentialsMatcher.setHashAlgorithmName("md5");
    //加密次数
    hashedCredentialsMatcher.setHashIterations(2);
    //存储散列后的密码是否为16进制
    //hashedCredentialsMatcher.isStoredCredentialsHexEncoded();
    return hashedCredentialsMatcher;
    }

    然后,在登录方法或者自定义的输入中获取登录 token,我选择的方式是在/login中获取:

    public Object login(@RequestBody User userParam, HttpSession session) {
    String name = userParam.getName();
    name = HtmlUtils.htmlEscape(name);
    Subject subject = SecurityUtils.getSubject();
    // 生成token
    UsernamePasswordToken token = new UsernamePasswordToken(name, userParam.getPassword());
    try {
    // 从自定义Realm获取安全数据进行验证
    subject.login(token);
    User user = userService.getByName(name);
    session.setAttribute("user", user);
    return Result.success();
    } catch (AuthenticationException e) {
    String message ="账号密码错误";
    return Result.fail(message);
    }
    }

    接下来,是自定义 Realm,之前我也有博文写过相关知识,所以只贴下代码作参考:

    public class JPARealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    // 权限分配的相关知识在此不做介绍,重点在验证方面
    SimpleAuthorizationInfo s = new SimpleAuthorizationInfo();
    return s;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    String userName = token.getPrincipal().toString();
    User user = userService.getByName(userName);
    String passwordInDB = user.getPassword();
    String salt = user.getSalt();
    // 认证信息token里存放账号密码, getName() 是当前Realm的继承方法,通常返回当前类名
    // 盐也放进去
    // 这样通过配置中的 HashedCredentialsMatcher 进行自动校验
    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, passwordInDB, ByteSource.Util.bytes(salt),
    getName());
    return authenticationInfo;
    }
    }

    HashedCredentialsMatcher 的源码分析
    从开发者的角度来看,我们可以自己实现 CredentialsMatcher 的一个类来实现定制化的账户密码验证机制,例如:

    public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
    @Override
    public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
    Object tokenCredentials = getCredentials(token);
    Object accountCredentials = getCredentials(info);
    return super.equals(tokenCredentials, accountCredentials);
    }
    }

    SimpleCredentialsMatcher 是 CredentialsMatcher 这个类的默认实现,相信我,你基本上不会去自己实现 getCredentials 这种涉及到底层编码的方法,重点在于重写 doCredentialsMatch ,在这里你可以自定义账户密码验证机制。

    不过实现 doCredentialsMatch 你还是有可能觉得麻烦,HashedCredentialsMatcher 封装好了 doCredentialsMatch() 方法,你可以完全不用管它。

    上一节的使用中,流程就是:使用 token 类将用户输入的信息封装,然后采用 token 进行 login 操作。此时 shiro 将使用 token 中携带的用户信息调用 Realm 中自定义的 doGetAuthenticationInfo 方法进行校验比对,比对成功则登录成功。


    原文链接:https://blog.csdn.net/zx48822821/article/details/84325504

  • 相关阅读:
    关于权限控制
    关于<!DOCTYPE>
    Oracle恢复目录的管理使用简要
    绑定变量介绍
    重做日志时间戳说明
    UNDO表空间监控说明
    Oracle rac进阶管理专家指导系列文档
    延迟块清除介绍
    ORA12500内存耗尽一例
    undo自动调优介绍
  • 原文地址:https://www.cnblogs.com/wq-9/p/12532159.html
Copyright © 2020-2023  润新知