混合开发方式的APP中使用WebView来访问登录服务器,需要做到session同步处理。
在解决问题前先简要回顾下Session与Cookie:
Cookie和Session都为了用来保存状态信息,都是保存客户端状态的机制,它们都是为了解决HTTP无状态的问题而所做的努力。
Session可以用Cookie来实现,也可以用URL回写的机制来实现。
Cookie和Session有以下明显的不同点:
1)Cookie将状态保存在客户端,Session将状态保存在服务器端;
2)Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器。网络服务器用HTTP头向客户端发送cookies,在客户终端,浏览器解析这些cookies并将它们保存为一个本地文件,它会自动将同一服务器的任何请求缚上这些cookies。
3)Session是针对每一个用户的,变量的值保存在服务器上,用一个sessionID来区分是不同用户session变量,这个值是通过用户的浏览器在访问的时候返回给服务器,当客户禁用cookie时,这个值也可能设置为由get来返回给服务器;
4)就安全性来说:当你访问一个使用session 的站点,同时在自己机器上建立一个cookie,建议在服务器端的SESSION机制更安全些.因为它不会任意读取客户存储的信息。
Session机制
Session机制是一种服务器端的机制,服务器使用一种类似于散列表的结构(也可能就是使用散列表)来保存信息。
当程序需要为某个客户端的请求创建一个session的时候,服务器首先检查这个客户端的请求里是否已包含了一个session标识 - 称为 session id,如果已包含一个session id则说明以前已经为此客户端创建过session,服务器就按照session id把这个 session检索出来使用(如果检索不到,可能会新建一个),如果客户端请求不包含session id,则为此客户端创建一个session并且生成一个与此session相关联的session id,session id的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个 session id将被在本次响应中返回给客户端保存。
Session的实现方式
1 ) 使用Cookie来实现
服务器给每个Session分配一个唯一的JSESSIONID,并通过Cookie发送给客户端。
当客户端发起新的请求的时候,将在Cookie头中携带这个JSESSIONID。这样服务器能够找到这个客户端对应的Session。
2 )使用URL回显来实现
URL回写是指服务器在发送给浏览器页面的所有链接中都携带JSESSIONID的参数,这样客户端点击任何一个链接都会把JSESSIONID带给服务器。
如果直接在浏览器中输入url来请求资源,Session是匹配不到的。
Tomcat对 Session的实现,是一开始同时使用Cookie和URL回写机制,如果发现客户端支持Cookie,就继续使用Cookie,停止使用URL回写。如果发现Cookie被禁用,就一直使用URL回写。jsp开发处理到Session的时候,对页面中的链接记得使用 response.encodeURL() 。
解决方案:
使用Shiro访问控制过滤器AccessControlFilter
如果在web应用程序中部署应用,默认情况下,应用将以HttpSession为基础。
在企业级应用中,你在多个应用中可以使用相同的API,无论部署环境。而且使用任何客户端技术你都可以共享会话数据。首先需要判断登录会话信息;
如果正确可以向下执行方法saveRequestAndRedirectToLogin(request, response);
如果不正确,就会对不同的业务进行处理:比如信息不正确、重新登录,用户被锁定的异常,当然也可以使用自定义抛出的异常。
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import com.common.utils.UserAgentUtils;
import com.modules.sys.utils.UserUtils;
public class UserFilter extends AccessControlFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) throws Exception {
if (isLoginRequest(request, response)) {
return true;
} else {
Subject subject = getSubject(request, response);
return subject.getPrincipal() != null;
}
}
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
HttpServletRequest request2 = (HttpServletRequest) request;
HttpServletResponse repResponse2 = (HttpServletResponse) response;
HttpSession session = request2.getSession();
// 判断请求客户端来自iOS或者Android
if ("APPLE_WEB_KIT".equals(UserAgentUtils.getBrowser(request2).toString())
|| "CHROME_MOBILE".equals(UserAgentUtils.getBrowser(request2).toString())) {
// 若无会话信息,则跳转到登录页面
if (subject.getPrincipal() == null) {
request.getRequestDispatcher("/static/appNativeToInvoke.jsp")
.forward(request, response);
}
} else {
saveRequestAndRedirectToLogin(request, response);
}
return false;
}
}
appNativeToInvoke.jsp中的JS代码部分:
webview调用服务端登录接口,获取服务端session,刷新之后就可以共享到服务端session并登录成功;
<script>
//app native调用
function login(userPhone,password){
$.ajax(
{
url: "/interface/appLogin/webviewlogin",
type: "post",
dataType: "json",
async: false ,
data:{"username":phone,"password":password},
success: function (data) {
//loginSuccess();
}
})
return "";
}
function getWebhSession() {
var message = { 'message' : 'iOS_Login'};
window.webkit.messageHandlers.iOS_Handler.postMessage(message);
}
function getWebhSession2() {
appInstance.doLogin();
}
window.setTimeout("getWebhSession()",00001);
window.setTimeout("getWebhSession2()",00001);
</script>
LoginController.java
@Controller
@ResponseBody
@RequestMapping(value = "httpInterface/appLogin")
public class loginController extends BaseController {
@RequestMapping(value = { "webviewlogin" })
public Map<String, Object> webviewlogin(String username, String password) {
System.err.println("app调用登录接口登录");
Map<String, Object> result=null;
Subject user = SecurityUtils.getSubject();
UsernamePasswordToken token1 = new UsernamePasswordToken();
token1.setUsername(username);
token1.setRememberMe(false);
try {
user.login(token1);
Map<String, Object> dataMap = new HashMap<>();
User user2 = UserUtils.getUser();
System.err.println("登录接口的sessionid为:"
+ UserUtils.getSession().getId());
} catch (AuthenticationException e) {
result = getErrResultMap("登录失败");
}
return result;
}
}
user.login(token);的方法调用,调用到Subjectsubject = securityManager.login(this, token);
这是一个简单的Shiro的登录认证过程,其实这部分功能也就是帮助我们验证此用户是否能登录本系统,和我们普通的登录完成的是同样的功能,Shiro是帮我们封装了这部分内容,让我们无需将登录的验证均写到程序中,而是使用配置的方式,更加灵活的应对变化,符合我们所说的OCP(开闭)原则(“Closed for Modification; Open for Extension”);
public void login(AuthenticationToken token) throws AuthenticationException {
clearRunAsIdentitiesInternal();
Subject subject = securityManager.login(this, token);
PrincipalCollection principals;
String host = null;
if (subject instanceof DelegatingSubject) {
DelegatingSubject delegating = (DelegatingSubject) subject;
//we have to do this in case there are assumed identities - we don't want to lose the 'real' principals:
principals = delegating.principals;
host = delegating.host;
} else {
principals = subject.getPrincipals();
}
if (principals == null || principals.isEmpty()) {
String msg = "Principals returned from securityManager.login( token ) returned a null or " +
"empty value. This value must be non null and populated with one or more elements.";
throw new IllegalStateException(msg);
}
this.principals = principals;
this.authenticated = true;
if (token instanceof HostAuthenticationToken) {
host = ((HostAuthenticationToken) token).getHost();
}
if (host != null) {
this.host = host;
}
Session session = subject.getSession(false);
if (session != null) {
this.session = decorate(session);
} else {
this.session = null;
}
}