参考连接:
单一用户登陆实现:https://www.cnblogs.com/leechenxiang/p/6171151.html
shiro拦截器介绍:http://jinnianshilongnian.iteye.com/blog/2025656
shiro全配置文件:http://www.cnblogs.com/leechenxiang/p/6136017.html
需求:
现在项目应用在web端和app端,一个用户要么登陆在web端要么app端,并且不能重复登陆,如果重复登陆,新登陆的客户端会踢掉已经登陆的客户端用户。即一个用户只能同时在线一个客户端
实现:
第一种:不适用框架,待补充
第二种:使用shiro框架来实现
如果使用shiro框架来实现,系统的认证授权模块都由shiro框架来实现,可能需要重构权限框架
直接看如下连接:
https://www.cnblogs.com/leechenxiang/p/6171151.html
权限相关知识和shiro框架介绍见shiro权限
sso单点登陆见单点登陆
单一用户登陆实现步骤如下:
- 导包,maven或者直接下载,直接百度
- 利用shiro框架搭建权限模块,见上文
- 利用shiro新建自定义拦截器
自定义拦截器可以继承shiro相关拦截器,扩展AccessControlFilter,其继承了PathMatchingFilter,并扩展了了两个方法:
isAccessAllowed:即是否允许访问,返回true表示允许;
onAccessDenied:表示isAccessAllowed方法返回为false时,即用户访问拒绝时,是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。
//导包省略
//自定义单一登陆shiro拦截器,可以继承AccessControlFilter类
public class SessionKickoutControlFilter extends AccessControlFilter{
//缓存,存入登陆用户的sessionId,参数string为kickout+用户名+web/app,Deque<Serializable>为sessionId集合(可能同时多个)
private Cache<String, Deque<Serializable>> sessionKickoutCache;
private boolean kickoutAfter = false;
private int maxSession = 1;//最大登陆session人数
private SessionManager sessionManager;
private static final String keyPrefix = "kickout";
@Autowired
AdminService adminService;
@Autowired
UserLogService userLogService;
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//request获得当前用户
Subject subject = getSubject(request,response);
if(!subject.isAuthenticated() && !subject.isRemembered()) {
//如果没有登录,返回true登陆即可
return true;
}
Session session = subject.getSession();
String username = (String) subject.getPrincipal();
Serializable sessionId = session.getId();
String client = null;
String[] clients = request.getParameterValues("client");
if(clients != null && clients.length > 0){
client = "1".equals(clients[0]) ? "APP" : "WEB";
}else {
client = ShiroUtil.getLoginClient();
}
//需要同步缓存值
synchronized(this.sessionKickoutCache){
Deque<Serializable> deque = sessionKickoutCache.get(keyPrefix + client + username);
if(deque == null || deque.size() == 0) {
deque = new LinkedList<Serializable>();
}
//如果队列里没有此sessionId,且用户没有被踢出;放入队列
if(!deque.contains(sessionId) && session.getAttribute("kickout") == null) {
deque.push(sessionId);
}
//如果队列里的sessionId数超出最大会话数,开始踢人
while(deque.size() > maxSession) {
Serializable kickoutSessionId = null;
if(kickoutAfter) { //如果踢出后者
kickoutSessionId = deque.removeFirst();
} else { //否则踢出前者
kickoutSessionId = deque.removeLast();
}
try {
Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
if(kickoutSession != null) {
//设置会话的kickout属性表示踢出了
kickoutSession.setAttribute("kickout", true);
}
} catch (Exception e) {//ignore exception
}
}
sessionKickoutCache.put(keyPrefix + client + username, deque);
}
//如果被踢出了,直接退出,重定向到踢出后的地址
if (session.getAttribute("kickout") != null && (Boolean)session.getAttribute("kickout")) {
session.setAttribute("kickout", false);
//会话被踢出了
try {
String loginIp = BaseUtil.getIpAddr((HttpServletRequest)request);
userLogService.insertUserLog(ShiroUtil.getUserNo(),ShiroUtil.getUserName(), ShiroUtil.getLoginClient(), loginIp, UserLogService.USER_LOG_LOGOUT);
subject.logout();
} catch (Exception e) { //ignore
}
saveRequest(request);
response.getWriter().write(JSONObject.toJSONString(adminService.kickoutLogin()));
//如果踢出以前登陆用户,可以让他重定向kickoutUrl(登陆页面)
WebUtils.issueRedirect(request, response, kickoutUrl);
return false;
}
return true;
}
//返回false不允许直接访问,要进行拦截,返回true,则可以直接访问,一般继承这个类就是为了不能直接访问,要进行拦截所以要返回false
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) throws Exception {
return false;
}
public void setSessionManager(SessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setKickoutAfter(boolean kickoutAfter) {
this.kickoutAfter = kickoutAfter;
}
public void setMaxSession(int maxSession) {
this.maxSession = maxSession;
}
public void setCacheManager(CacheManager cacheManager){
this.sessionKickoutCache = cacheManager.getCache("sessionKickoutCache");
}
}
- 配置SessionKickoutControlFilter拦截器
第一种:未整合spring
在shrio.ini文件中加入
[filters]
kickout=cn.paic.rep.pare.shiro.SessionKickoutControlFilter
[urls]
/**=kickout//表示对所有的连接都需要进行拦截判断
第二种:spring整合(常用)
在application-shiro.xml文件中配置
<!--第一步:在spring中注入自定义的拦截器
楼主定义了多个拦截器:先判断单点登陆,然后单一用户登陆,最后权限验证,形成一个责任链
-->
<bean id="ssoFilter" class="cn.paic.rep.pare.shiro.SSOFilter" />
<bean id="_permissionsFilter" class="cn.paic.rep.pare.shiro._PermissionsFilter"></bean>
<!--这是的单一用户登陆配置项-->
<bean id="sessionKickoutControlFilter" class="cn.paic.rep.pare.shiro.SessionKickoutControlFilter">
<property name="cacheManager" ref="cacheManager"/>
<property name="sessionManager" ref="sessionManager"/>
<property name="kickoutAfter" value="false"/>
<property name="maxSession" value="1"/>
<!--跳转页面 <property name="kickoutUrl" value="/login"/>
--> </bean>
<!-- 配置shiro的过滤器工厂类,id- shiroFilter要和我们在web.xml中配置的过滤器一致 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 调用我们配置的权限管理器 -->
<property name="securityManager" ref="securityManager" />
<!-- 配置我们的登录请求地址 -->
<!-- <property name="loginUrl" value="/login" /> -->
<!-- 配置我们在登录页登录成功后的跳转地址,如果你访问的是非/login地址,则跳到您访问的地址 -->
<!-- <property name="successUrl" value="/home" /> -->
<!-- 如果您请求的资源不再您的权限范围,则跳转到/403请求地址 -->
<!-- <property name="unauthorizedUrl" value="/api/admin/unauthorized" /> -->
<!-- 权限配置 -->
<!-- 第二步:以下为配置自定义的拦截器 -->
<property name="filters">
<map>
<entry key="ssoFilter" value-ref="ssoFilter" />
<entry key="perms" value-ref="_permissionsFilter" />
<entry key="kickout" value-ref="sessionKickoutControlFilter"/>
</map>
</property>
</bean>
<!-- 第三步:权限资源配置 -->
<bean id="filterChainDefinitionsService" class="cn.paic.rep.pare.shiro.SimpleFilterChainDefinitionsService">
<property name="definitions">
<value>
<!--不需要登陆和拦截即可访问-->
/api/file/getBbsImg/*=anon
<!--左边对api下的请求都要进行拦截判断-->
/api/** =kickout,ssoFilter
</value>
</property>