shiro安全控制目录
Http协议是无状态的,即一次用户请求时,浏览器/服务器无法知道之前这个用户做过什么,每一次请求均是新的请求。故我们使用Cookie—Session技术来交互存储状态。
1. cookie和session的区别
1.1 cookie简介
1. 什么叫做cookie
Cookie是客户端技术,程序把每个用户的数据以cookie的形式写给用户各自的浏览器。当用户使用浏览器再去访问服务器中的web资源时,就会带着各自的数据去。这样,web资源处理的就是用户各自的数据了。
2. cookie的生命周期
那么cookie的有效期只在一次会话过程中有效,用户开一个浏览器,点击多个超链接,访问服务器多个web资源,然后关闭浏览器,整个过程称之为一次会话,当用户关闭浏览器,会话就结束了,此时cookie就会失效,
1.2 session简介
1. 什么叫做session
Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的session对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在各自的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。
1.3 sessionId和cookie的关系
客户端用cookie保存了sessionID,我们上面知道cookie默认的生命周期是一次会话,注意此时session在浏览器可能没有超时,cookie被销毁后,我们无法再找到sessionId,下次在请求服务器的时候,服务器会生成一个新的sessionId,保存在cookie中,返回客户端。
2. Shiro如何处理session
用户在登录成功之后,shiro会将用户的一些信息存储到sessionDAO。保存形式为
sessionID:session
。用户在不同设备上登录成功之后
(sessionId不同,但是session对象里面的"主键"相同)
,此时证明用户已经在其他设备上已登录。我们就可以处理我们的业务逻辑。用户每次请求都会携带SESSIONID,服务器拦截到请求,判断是否要进行鉴权,如果进行鉴权的话,判断SESSION是否存在。
服务器将(旧的)session删除后,当用户再请求服务器时,服务端会抛出
there is no session
的异常,然后从新为请求建立一个新的session
【返回到重新登录页面】,就像用户很长时间没有点击浏览器,shiro的定时器定时将失效的session清除的时候也抛出这个异常一样。不过这个对用户是透明的,对用户的体验没有影响。
3. 实现单处登录
3.1 XML配置
这里实现一个简易的xml配置。
本质上是通过session来完成用户认证,那么我们可以通过配置SessionManager
来操纵session。
1. 加载sessionDao
<bean id="sessionDao"
class="org.apache.shiro.session.mgt.eis.MemorySessionDAO"/>
2. 加载我们的sessionManage,它需要依赖我们的sessionDao
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
<property name="sessionDAO" ref="sessionDao"/>
</bean>
3. 加载securityManage
<bean id="securityManager"
class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm"></property>
<!-- 注入缓存管理器 -->
<property name="cacheManager" ref="cacheManager"></property>
<property name="sessionManager" ref="sessionManager"></property>
</bean>
3.2 代码配置
这里需要重写认证代码
-
sessionDAO.getActiveSessions()
顾名思义获取到所有存活的session对象。 -
session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY)
可以获取simpleAuthenticationInfo
的第一个参数的值,其实就是principal
对象,我们自定义的参数对象(在Realm认证的时候创建)。 - 根据session创建subject对象。
- 获取subject的principal对象(此处是自定义对象)。
- 判断
principal
对象里面的参数是否相同。 - 相同的话,可以删除session。两种方式:
- session.setTimeout(0);设置session的失效时间;
- sessionDAO.delete(session);直接删除session;
@Autowired
private SessionDAO sessionDAO;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken aToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken)aToken;
String username = token.getUsername();
//判断用户名是否存在
User user = userService.selectUser(username);
//声明一个user用来获取sessionDao中的user
User userSession = null;
if(user != null){
//获取在线的session
Collection<Session> sessionCollection = sessionDAO.getActiveSessions();
for (Session session : sessionCollection){
if(session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) {
//根据session build出一个subject
Subject subject = new Subject.Builder().session(session).buildSubject();
//拿到这个登陆的对象
userSession = (User) subject.getPrincipal();
//判断她的code和我现在登陆的code是否一致 (code是我在数据库里面设置一个标识码 采用uuid 保证唯一性 这里你可以id)
if (user.getCode().equals(userSession.getCode())) {
//两者一致的时候,设置这个session的失效时间 (0:立刻)
session.setTimeout(0);
break;
}
}
}
AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
return authenticationInfo;
} else{
throw new AuthenticationException("用户名错误!");
}
}
4. 单处登录-改进版
若是1亿用户登录,每次登录的用户都要循环1亿次判断是否异处登录吗?显然性不好,如何改进呢?
在shiro整合redis中,将sessionId和session对象保存redis中后,我们将userId(业务字段)
和sessionId也存储在redis中。
注意:在登录成功后,也将userId和sessionId保存到Redis中。
这样的话,我们可以快速的获取到sessionId和session对象了。
1. 在doGetAuthenticationInfo认证的时候,调用该方法,判断用户是否已登录。使用业务id去Redis中获取sessionId,若包含,并且本次上送的sessionId不是redis存储的sessionId,则证明该用户在别的机器上登录。那我们便将别得机器上的sessionId保存下来。等到用户再次选择登录的时候,删除该sessionId。
private boolean isLogin(String userId, Session session) {
//获取新请求的sessionId值
Serializable httpSessionId = session.getId();
//在Redis中获取到旧的sessionId的值
String sessionId = jedisCluster.get(Constant.isLoginPrefix + userId);
//如果没有存入到Redis中
if(StringUtils.isBlank(sessionId)) {//未登录
return false;
}
if(httpSessionId != null) {
//格式化httpSessionId 的值
httpSessionId = Base64.encode(SerializeUtil.serialize(Constant.REDIS_SHIRO_SESSION + httpSessionId));
//若sessionId相等则是一个浏览器登录
if(sessionId.equals(httpSessionId)) {//同一个浏览器二次登陆
return false;
}
}
//如果sessionId和session对应中,其实session为空
String sessionObj = jedisCluster.get(sessionId);
if(StringUtils.isBlank(sessionObj)) {//未登录
//删除userId和sessionId对应关系
jedisCluster.del(userId);
return false;
}else{//已登录
//将旧的sessionId保存到新的session对象,后续删除该session对象。
session.setAttribute("user.old.sessionId", sessionId);
return true;
}
}
保存用户的token信息,然后抛出异常;
session.setAttribute("user.login.token", token);
throw new ConcurrentAccessException("用户已在其他地方登陆,是否继续登录");
2. 返回特殊的错误码
3. 前端收到特殊错误码后,要求用户是否执行再次登录,此时返回客户端一个新的sessionId。
4. 用户点击再次登录,请求携带新的sessionId到达Controller层
@RequestMapping("/continueLogin")
@ResponseBody
public ResponseVo continueLogin() {
ResponseVo vo = new ResponseVo(ResponseVo.FAIL, "登陆失败");
//此时拿到的是新的SESSIONID的SESSION对象
Session session = SecurityUtils.getSubject().getSession();
//获取登录用户的账号密码对象
Object tokenObj = session.getAttribute("user.login.token");
if (tokenObj == null) {
vo.setMessage("请重新点击登录!");
return vo;
}
UsernamePasswordToken token = (UsernamePasswordTokenEx) tokenObj;
if (StringUtils.isBlank(token.getUsername()) || token.getPassword() == null) {
vo.setMessage("用户名或密码不能为空");
return vo;
}
try {
//内部封装了subject.login()方法
if (loginService.login(token)) {
//再次验证成功之后,删除已登陆的sessionId
Object oldSessionIdObj = session.getAttribute("user.old.sessionId");
if (oldSessionIdObj != null) {
String oldSessionId = String.valueOf(oldSessionIdObj);
String newSessionId = Base64.encode(SerializeUtil.serialize(Constant.REDIS_SHIRO_SESSION + session.getId()));
if (!StringUtils.isBlank(oldSessionId) && !oldSessionId.equals(newSessionId)) {
//删除Redis中的sessionId和就是删除Session中的SessionId
jedisCluster.del(oldSessionId);
logger.info("删除已登陆Session【{}】成功", oldSessionId);
}
}
vo = new ResponseVo(200, "登陆成功");
return vo;
}
catch (Exception e) {
vo.setMessage("登录失败!");
logger.error("登陆失败", e);
}
return vo;
}
需要注意的是,因为redis整合了session,session最终保存在Redis中,而不是服务器内存中。
文章参考:
1. shiro账号安全(每一个账号只能同时存在于一台设备上)
2. cookie和session的区别
3. 用户Cookie和会话Session、SessionId的关系
4. Shiro教程(十一)Shiro 控制并发登录人数限制实现,登录踢出实现