RBAC权限与shiro中登录与session的思考
1. 用户登录状态的思考
1.1 Session
多态电脑登录同一个用户/一台电脑登录多个用户
服务端如何识别并处理?
cookie/session 来保存交互记录从而实现更多功能
Session 提供了一种用户跨多个页面请求/访问某个web 服务器的用户信息跟踪、识别机制。
Session在网络协议中表示面向连接、保持状态的含义
session的实现也许借助cookie在客户端保存标识以便用户身份认证
1.2 cookie
由浏览器存储的源自于web服务器的少量信息交互机制,常用于session管理
用于记录用户信息形成特定访问状态
- 名称 key
- 值 value
- 域
- 路径 域+路径取顶作用范围
- 过期时间
用于记录session的cookie 例如 jsessionid
浏览器关闭不会让cookie失效,因此服务器自己必须设置失效时间,也可在登出时手动情况登录状态
Weblogic Server对于HttpSession的默认实现是存在内存中,服务器一旦重启就丢失这些登录信息的session信息,因此shiro交给redis管理后即使重启依旧可以重复获取从而在一定程度上保证登录可靠
1.3 shiro中的session
通过配置DefaultWebSessionManager
来控制session的执行规则
在DefaultWebSessionManager$storeSessionId
方法中可看到默认的session设置是通过cookie
自定义cookie / 请求头
shiro会寻找session id,源码中默认使用JSESSIONID 作为shiro的默认sessionId
可通过重写DefaultWebSessionManager#getSessionId
方法来自定义session ID
/**
* 自定义session管理
*/
@Configuration
@Slf4j
public class MySessionManager extends DefaultWebSessionManager {
@Resource
LoginUtil loginUtil;
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
/**
* api接口使用公共token替代登录session,作为登录标识
*
* @param request
* @param response
* @return
*/
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader("cSession");
//如果请求头中有 token 则其值为sessionId
if (!CommonUtil.isNullOrEmpty(id)) {
try {
id = loginUtil.decryptText(id);
} catch (NoAuthException e) {
id = "";
}
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
}
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
2.数据库设计
字典表设计属于EAV模型
使用key+value+valuetype表示可扩展的行,无法保证数据一致性,数据膨胀、笨重
解决方案
- 单表集成所有属性(由于属性扩展无法达成) type区分
- 多表扩展,元数据膨胀
- 类表集成,外键指向共同属性表的某个字段
下面介绍设计中改动较大的部分
2.1 sys_permission
通过operator_visibility(操作员是否可见)、permission_system_visibility(权限系统是否可见)两个字段定义菜单可见性,解决两个问题
-
识别权限管理系统自身菜单在各个系统空间的可见性
permission_system_visibility1 permission_system_visibility-1 operator_visibility1 权限系统/其他系统空间均可见 例如角色管理 权限系统不可见,其他系统可见 例如操作员管理 operator_visibility-1 权限系统可见,其他系统不可见 例如系统管理 权限系统,其他系统在权限管理系统内都不可见 例如其他系统自身的权限 -
区分权限系统的菜单/其他系统自身配置的菜单
其他系统自身配置的菜单对权限系统/操作员 均不可见
2.2 sys_role
同样通过两个字段区分普通角色/操作员角色/其他系统普通角色
-
app_id
所属系统
-
use_role_app_id
使用该角色的系统(主要用于区分普通角色/操作员角色 操作员角色的使用角色的系统默认为1 (权限管理系统) 普通角色则系统默认与所属系统一致
同样分为四种情况(假定权限管理系统的系统代码为1,其他系统为-1)
-
app_id = 1 | use_role_app_id = 1
所属系统为权限管理系统,使用该角色的系统也是权限管理系统,那么这个角色就是为权限管理系统自身配置的角色,可登录权限管理系统在权限管理系统空间内操作
-
app_id = 1 | use_role_app_id = -1
所属系统为权限管理系统,使用该角色的系统非权限管理系统(这种情况暂时未遇到)
-
app_id = -1 | use_role_app_id = 1
例如操作员角色,所属系统非权限管理,但需要到权限管理系统来做操作配置
-
app_id = -1 | use_role_app_id = -1
例如非权限管理系统空间内配置的普通角色,服务于其他系统
3. shiro用户登录信息变更
-
从缓存获取当前用户登录信息变更后假设以另一个身份登录(切换了系统空间)
// 更新用户信息 StaffLoginVo validStaffInfo = loginCacheUtil.getValidStaffInfo(); // log 添加关键字,手机号去除,占位符拼接 log.info("模块:用户系统空间切换 操作参数:{},切换前用户信息{}",appId,validStaffInfo); validStaffInfo.setAppId(appId); Subject subject = SecurityUtils.getSubject(); String newLoginInfo = JSON.toJSONString(validStaffInfo); PrincipalCollection principals = subject.getPrincipals(); String realmName = principals.getRealmNames().iterator().next(); // 生成新的principal PrincipalCollection newPrincipalCollection = new SimplePrincipalCollection(newLoginInfo, realmName); // 用户身份切换 subject.runAs(newPrincipalCollection); log.info("模块:用户系统空间切换 操作参数:{},切换后用户信息{}",appId,validStaffInfo);
-
删除旧的授权信息,在访问shiro权限校验注解标注的controller方法时重新获取用户权限
/** * 根据principalCollection删除授权缓存 * @param attribute */ public void deleteCacheBySimplePrincipalCollection(SimplePrincipalCollection attribute) { //删除Cache,再访问受限接口时会重新授权 DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager(); Authenticator authc = securityManager.getAuthenticator(); ((LogoutAware) authc).onLogout(attribute); }