(转自:http://www.cnblogs.com/sunshine-2015/p/5515429.html)

 

先说下背景,项目包含一个管理系统(web)和门户网站(web),还有一个手机APP(包括Android和IOS),三个系统共用一个后端,在后端使用shiro进行登录认证和权限控制。好的,那么问题来了web和APP都可以用shiro认证吗?两者有什么区别?如果可以,解决方案是什么?看着大家焦急的小眼神,接下来挨个解决上面的问题。

web和APP可以用shiro统一登录认证吗?

可以。假如web和APP都使用密码登录的话,那没的说肯定是可以的,因为对于shiro(在此不会介绍shiro详细知识,只介绍本文章必要的)来说,不管是谁登录,用什么登录(用户名密码、验证码),只要通过subject.login(token)中的token告诉shiro,然后在自己定义的Realm里面给出自己的认证字段就可以了,好吧说的云里雾里,看看代码

// 在自己登录的rest里面写,比如UserRest里面的login方法中,user为传递过来的参数
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
// 开始进入shiro的认证流程
currentUser.login(token);

上面的代码是开始使用shiro认证,调用subject.login(token)之后就交给shiro去认证了,接下来和我们相关的就是自定认证的Realm了,比如自定义UserRealm

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException {
//获取基于用户名和密码的令牌
//实际上这个token是从UserResource面currentUser.login(token)传过来的
//两个token的引用都是一样的
UsernamePasswordToken token = (UsernamePasswordToken)authcToken;
System.out.println("验证当前Subject时获取到token为" + ReflectionToStringBuilder.toString(token, ToStringStyle.MULTI_LINE_STYLE));
// 从数据库中获取还用户名对应的user
User user = userService.getByPhoneNum(token.getUsername());
if(null != user){
AuthenticationInfo authcInfo = new SimpleAuthenticationInfo(user.getPhoneNum(),user.getPassword(), getName());
return authcInfo;
}else{
return null;
}
}

再配一张图

图中描述的是使用shiro进行一个完整的登录过程

所以由以上代码看出目前我们还没有发现APP和web登录d区别,那么区别是什么呢?

web和APP登录认证的区别

好吧,标题不太准确,应该是登录的时候和登陆之后会话保持在web和APP之间的区别,先说登录:

登录

APP和PC web所需的设备不同很大程度上决定了两者之间的区别,web一般在PC上浏览,登录的时候使用用户名和密码,如果使用了记住密码就是用cookie认证,web登录有以下情况

  • 第一次登录,使用用户名和密码登录
  • 关闭浏览器、session过期,重新使用密码登录(如果有记住密码功能,可以使用cookie登录)
  • 用户删除cookie或者cookie过期,使用户名和密码登录

APP在移动设备上查看,第一次登录的时候使用用户名和密码,但是以后如果不是用户主动退出,都应该保持登录状态,这样才会有更好的用户体验,但是不可能一直保留该APP的会话,也不可能把密码保存在本地,所以APP应该以下的过程

  • 第一次登录,使用用户名密码
  • 以后用户打开应用之后,用户不需输入密码系统就可以自动登录
  • 用户主动退出(重装等情况视为主动退出)之后,使用用户名和密码登录

貌似没有看出什么区别,唯一的不同就是第二点:怎么不用密码登录,web使用的是cookie(由浏览器自动维护的),APP怎么登陆呢?由于APP本地不保存密码,那么也参考web,使用类似cookie的东西,我们叫他token吧,那问题就解决了,APP本地保存token,为了安全性,定期更新token,那再来看看会话的保持。

会话(session)(保持状态)

如果用户登录了,怎么保持登录状态呢,web有cookie和session配合解决这个问题,下面先简单说一下我对这两个东西的理解,因为APP会话就是参考这个原理设计的。

cookie:是由浏览器维护的,每次请求浏览器都会把cookie放在header里面(如果有的话),也可以看做js的可以访问本地存储数据的位置之一(另一个就是local storage)

session:由于http是无状态的,但是有时候服务器需要把这次请求的数据保存下来留给下一次请求使用,即需要维护连续请求的状态,这个时候服务器就借助cookie,当浏览器发送请求来服务器的时候,服务器会生成一个唯一的值,写到cookie中返回给浏览器,同时生成一个session对象,这样session和cookie值就有了一一对应关系了,浏览下一次访问的时候就会带着这个cookie值,这个时候服务器就会获得cookie的值,然后在自己的缓存里面查找是否存在和该cookie关联的session

因为cookie和session的配合,shiro可以本身很好的支持web的登录和会话保持,对于APP来说也可以借鉴cookie和session的这种实现方式,唯一存在的问题,就是web的cookie是由浏览器维护的,自动将cookie放在header里面,那我们APP只要把服务器返回的cookie放在header里面,每次访问服务器的时候带上就可以了。

免密码登录

解决了登录和会话保持的问题,还剩一个免密码登陆:

web:因为一般网页主需要记住7天密码(或者稍微更长)的功能就可以了,可以使用cookie实现,而且shiro也提供了记住密码的功能,在服务器端session不需要保存过长时间

APP:因为APP免密码登录时间需要较长(在用户不主动退出的时候,应该一直保持登录状态),这样子在服务器端就得把session保存很长时间,给服务器内存和性能上造成较大的挑战,存在的矛盾是:APP需要较长时间的免密码登录,而服务器不能保存过长时间的session,解决办法:

  • APP第一次登录,使用用户名和密码,如果登录成功,将cookie保存在APP本地(比如sharepreference),后台将cookie值保存到user表里面
  • APP访问服务器,APP将cookie添加在heade里面,服务器session依然存在,可以正常访问
  • APP访问服务器,APP将cookie添加在heade里面,服务器session过期,访问失败,由APP自动带着保存在本地的cookie去服务器登录,服务器可以根据cookie和用户名进行登录,这样服务器又有session,会生成新的cookie返回给APP,APP更新本地cookie,又可以正常访问
  • 用户手动退出APP,删除APP本次存储的cookie,下次登录使用用户名和密码登录

这种方法存在的问题:

  1. cookie保存在APP本地,安全性较低,可以通过加密cookie增加安全性
  2. 每次服务器session失效之后,得由APP再次发起登录请求(虽然用户是不知道的),但是这样本身就会增加访问次数,好在请求数量并不是很大,不过这种方式会使cookie经常更新,反而增加了安全性

这里给出另外一种实现方式:

实现自己的SessionDao,将session保存在数据库,这样子的好处是,session不会大量堆积在内存中,就不需要考虑session的过期时间了,对于APP这种需要长期保存session的情况来说,就可以无限期的保存session了,也就不用APP在每次session过期之后重新发送登录请求了。实现方式如下:

为了使用Hibernate将Session保存到数据库,新建一个SimpleSessionEntity

package org.lack.entity;

import java.io.Serializable;

import org.apache.shiro.session.mgt.SimpleSession;

import com.phy.em.user.entity.User;

public class SimpleSessionEntity {

    private Long id;
private String cookie;
private Serializable session; public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Serializable entity() {
return session;
}
public void setSession(Serializable session) {
this.session = session;
}
public String getCookie() {
return cookie;
}
public void setCookie(String cookie) {
this.cookie = cookie;
}
public Serializable getSession() {
return session;
}
}
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping package="org.lack.entity">
<class name="SimpleSessionEntity" table="session">
<!-- 标识 -->
<id name="id">
<column name="id"></column>
<generator class="increment"></generator>
</id> <property name="session">
<column name="session"></column>
</property> <property name="cookie">
<column name="cookie"></column>
</property> </class>
</hibernate-mapping>

以上贴出来的是SimpleSessionEntity的映射文件,特别要注意的是Hibernate也是支持把对象保存在数据库中的,但是该实体要实现Serializable,在取出来的时候强转为对应的对象即可,所以这里session的类型为Serializable

新建session缓存的方式的类,这里继承自EnterpriseCacheSessionDAO,可以使用ehcache作为二级缓存,一定要记得实现save、update、readSession、delete方法,特别是save方法只是保存一个基本的session,重要的attribute都是update的,在readSession中从数据库中读取即可

package org.lack.dao

import java.io.Serializable;
import java.util.Date;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SimpleSession;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.springframework.transaction.annotation.Transactional;
import com.phy.em.common.dao.IBaseDao;
import com.phy.em.common.shiro.entity.SimpleSessionEntity;
import com.phy.em.user.entity.User;
public class SessionEntityDao extends EnterpriseCacheSessionDAO { private IBaseDao<User> baseDao;
private IBaseDao<SimpleSessionEntity> sessionDao;
private Logger log = Logger.getLogger(SessionEntityDao.class); @Override
public Serializable create(Session session) {
// 先保存到缓存中
Serializable cookie = super.create(session);
// 新建一个SimpleSessionEntity,然后保存到数据库
SimpleSessionEntity entity = new SimpleSessionEntity();
entity.setSession((SimpleSession)session);
entity.setCookie(cookie.toString());
sessionDao.save(entity); return cookie;
} @Override
public void update(Session session) throws UnknownSessionException {
super.update(session);
SimpleSessionEntity entity = getEntity(session.getId());
if(entity != null){
entity.setSession((SimpleSession)session);
sessionDao.update(entity);
}
} @Override
public Session readSession(Serializable sessionId) throws UnknownSessionException {
Session session = null; try{
session = super.readSession(sessionId);
} catch(Exception e){ } // 如果session已经被删除,则从数据库中查询session
if(session == null){
SimpleSessionEntity entity = getEntity(sessionId);
if(entity != null){
session = (Session) entity.getSession();
}
}
     // 如果是APP则更新lastAccessTime
 User user = getUser(sessionId);
if(user != null){
  // 如果该用户是APP用户(user不为空说明就是),则判断session是否过期,如果过期则修改最后访问时间
  ((SimpleSession)session).setLastAccessTime(new Date());
}
     return session;
} @Override
public void delete(Session session) {
super.delete(session);
} private User getUser(Serializable sessionId){
String hql = "from User user where user.cookie ='" + sessionId + "'";
return baseDao.findUniqueByHQL(hql);
} private SimpleSessionEntity getEntity(Serializable sessionId){
String hql = "from SimpleSessionEntity entity where entity.cookie ='" + sessionId + "'";
return sessionDao.findUniqueByHQL(hql);
} private boolean isExpire(Session session){
long timeout = session.getTimeout();
long lastTime = session.getLastAccessTime().getTime();
long current = new Date().getTime();
if((lastTime + timeout) > current){
return false;
}
return true;
} public void setBaseDao(IBaseDao<User> baseDao) {
this.baseDao = baseDao;
} public void setSessionDao(IBaseDao<SimpleSessionEntity> sessionDao) {
this.sessionDao = sessionDao;
} }

我快被自己蠢哭了,在继承EnterpriseCacheSessionDAO 只实现了readSession,妄想自己新建一个SimpleSession来返回给shiro使用,尝试过很多次之后不行,跟着调试了很多shiro源码,发现在SimpleSession中Shiro不仅设置了基本的属性,更重要的是设置了Attribute,但是我自己新建的SimpleSession没有,所以认证是失败的,所以在此敬告各位一定要记得实现save和update方法。

虽然走了很多弯路,但是随着对shiro源码的调试学习,对shiro了解更深了,不再仅仅停留在只会使用的地步上,有深入。


好了到此为止,正文完了,我们开头提出的问题都解决完了,记下来掰扯掰扯在做APP登录过程中遇到的问题以及一些自己的体会。

关于系统安全

在考虑APP登录的时候考虑了很多安全因素

  • 在用户使用用户名和密码登录的时候,对密码进行加密
  • 会话保持如果使用cookie这种技术的话,存在被别人截取cookie之后就可以认证登录了
  • 在本地保存密码肯定是不合适的,如果保存cookie(token)的话,手机被root之后,很容易就可以看得到了,比如Android的就只是一个xml文件,所以cookie保存要加密,加密之后提高了破解门槛,加密就涉及到秘钥的问题了,秘钥如果写在代码里面,java被反编译之后就很容易秘钥找得到了,当然了google早就已经开始支持NDK(即Android原生开发,这个原生是指使用C/C++开发,编译成为so文件,在java中调用),这样又加大了破解难度,使用Hybrid就更不用说了,直接解压安装包就可以看到了。
  • cookie如果保存在本地,更新的时机(频率)是什么,这样就算是cookie泄露了,也只是在某一段时间内有用(当然了,对于“有心人”来说“这段时间”已经足够做一些事儿了)

在考虑这些问题的时候我意识到:

  • 安全只是相对的(攻与防本来就是一件你强我更强的事,有攻击,防御就会增强,防御增强了,攻击要想成功就得更强)
  • 安全不是在技术上越安全越好,要考虑实际应用场合、投入的成本(往往不是技术不能实现,而是要考虑实际情况,包括成本、信息的重要程度等等,这就是一种工程思维)
 
 

shiro实现app和web统一登录的更多相关文章

  1. shiro实现APP、web统一登录认证和权限管理

    先说下背景,项目包含一个管理系统(web)和门户网站(web),还有一个手机APP(包括Android和IOS),三个系统共用一个后端,在后端使用shiro进行登录认证和权限控制.好的,那么问题来了w ...

  2. 在Spring MVC Controller的同一个方法中,根据App还是WEB返回JSON或者HTML视图。

    如有高见,欢迎交流! 最近在做一个web的项目,web版已经开发完毕,现在正在进行手机APP的开发,开发中遇到一个问题: 就是web版和app版都有登录功能,本想着是分别走不同的URL,实际开发的时候 ...

  3. Shiro与CAS整合实现单点登录

    1.简介 CAS:Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法. Shiro:Apache Shiro是一个Java安全框架,可以帮助我们完成认证.授权.会话管 ...

  4. 搭建开发框架Express,实现Web网站登录验证

    NodeJS学习笔记(一)——搭建开发框架Express,实现Web网站登录验证   JS是脚本语言,脚本语言都需要一个解析器才能运行.对于写在HTML页面里的JS,浏览器充当了解析器的角色.而对于需 ...

  5. Shiro的原理及Web搭建

    shiro(java安全框架) 以下都是综合之前的人加上自己的一些小总结 Apache Shiro是一个强大且易用的Java安全框架,执行身份验证.授权.密码学和会话管理.使用Shiro的易于理解的A ...

  6. SSO-单点统一登录系统的设计与实现

    本文主要基于web类应用展开讨论,提供的是一种通用机制和方法,所以不论何种技术栈都可进行相应的具体实现. 实现目标 可以在相同或跨域环境下完成各应用的统一登录/注销 方案原理 本质上是采用了web应用 ...

  7. springboot+layui实现PC端用户的增删改查 & 整合mui实现app端的自动登录和用户的上拉加载 & HBuilder打包app并在手机端下载安装

    springboot整合web开发的各个组件在前面已经有详细的介绍,下面是用springboot整合layui实现了基本的增删改查. 同时在学习mui开发app,也就用mui实现了一个简单的自动登录和 ...

  8. 基于Shiro,JWT实现微信小程序登录完整例子

    小程序官方流程图如下,官方地址 : https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html ...

  9. 【Shiro】Apache Shiro架构之集成web

    Shiro系列文章: [Shiro]Apache Shiro架构之身份认证(Authentication) [Shiro]Apache Shiro架构之权限认证(Authorization) [Shi ...

随机推荐

  1. Lodash.js常用拷贝

    lodash.js 降低 array.number.objects.string 等等的使用难度从而让 JavaScript 变得更简单.非常适用于:遍历 array.object 和 string: ...

  2. SDUT 1309 不老的传说问题 (区间DP)

    题意: 有一个环形序列,n个数字表示一种颜色,要求将白板环刷成一模一样的环,限制是每次最多只能刷连续的K个位置,问最少需要刷几次? 思路: 跟2008长春那道painter string 差不多.只是 ...

  3. UWP开发:自动生成迷宫&自动寻路算法(1)

    (1)前端篇 首先,我们创建一个新的Universal Windows Platform程序.这些小方块是通过GridView来罗列的,这样可以避免MainPaga.xaml的<Rectangl ...

  4. jQuery UI -- Repeater & 手风琴(Accordion)效果

    jQuery UI -- Repeater & 手风琴(Accordion)效果 很简单的范例,完全不用写程序 直接套用就能做! 但是,基础不稳的人,连「复制贴上」直接套用, 对您而言,都困难 ...

  5. Spark 配置整理

    Spark 的配置有很多,这里一方面总结一下官方文档中的内容,一方面将网上查到的资料中用到的针对特定问题的配置整理一下. 先看一下官网的配置:http://spark.apache.org/docs/ ...

  6. js学习笔记-字符串

    1.需要注意的是,JavaScript 的字符串是不可变的(immutable),String 类定义的方法都不能改变字符串的内容.像 String.toUpperCase() 这样的方法,返回的是全 ...

  7. Bootstrap历练实例:标签页内的下拉菜单

    <!DOCTYPE html><html><head><meta http-equiv="Content-Type" content=&q ...

  8. 在Linux下安装redis

    http://www.cnblogs.com/xiaohongxin/p/6854095.html 追加: 通过配置文件启动最好先./redis.cli shutdown ,再到当前目录在./redi ...

  9. ★iOS 性能测试工具 SDK

    一.概括 1. 做一个类似GT的性能测试工具: 2. 第一期主要是CPU.内存功能,要求可以绘制曲线,可以选择曲线区间,自动计算最小值.最大值.均值等,支持曲线全屏显示 目标的视觉效果是类似股票走势图 ...

  10. 字节跳动后端开发实习生面试(Python)

    一面: 1.自我介绍. 2.介绍“工大小美”项目相关. 3.Python中的GIL(全局解释器锁),以及哪种情况下使用python的多线程性能有较大的提升. 4.项目中用到了SQLite数据库,如果有 ...