首页 > 其他 > 详细

shrio总结

时间:2020-03-20 16:59:54      阅读:61      评论:0      收藏:0      [点我收藏+]

 

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

shrio总结

原文:https://www.cnblogs.com/wq-9/p/12532159.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!