最近项目中遇到一个有意思的问题:
描述如下:
1. 产品详情页使用了Freemarker页面静态化技术, 所以为了提高静态页面的并发访问性能, 将其部署在了nginx服务器中;
2. 同时要使用CAS做单点登录功能, 但是CAS是Server+Client的模式, 直接部署静态页面就不存在Client, 无法直接使用CAS做单点登陆登出;
在实际开发环境中, 我们往往遇到这样活那样的问题, 但是: 只要思想不滑坡, 方法总比困难多. 经过思考, 做出如下解决方案, 可能不是最优方案, 但肯定是一种实际可行的方案;
问题剖析及方案构思:
1. 先确定, 万变不离其宗, 浏览器登录还是得靠Cookie, 怎么登录呢? 没有Client那借一个可以吗? 既然是登录, 那就借用User模块的吧;
2. 怎么借? 首先, 要跳转到登录页面; 然后, 登录完再跳转回原来的静态页面; 最后, 静态页面还能动态获取当前登录的用户名;
跳转到登录页: 访问User模块中一个要求登录才能访问的页面, 来实现跳转到登录页面的目的;
跳转回原来的静态页面: 使用跳转页方案, 在地址栏将静态页地址作为参数传参给跳转页, 在登录后由跳转页获取地址后跳回静态页;
动态获取用户名: 既然借了User模块的登录页面, 不妨再借用User模块的获取登录名接口;
登出: 访问CAS服务器登出即可;
3. 要想实现 2 中所述获取用户名功能, 需要面对一个新的问题: 跨域. 但是解决这个问题已经是常规操作啦, 本博采用W3C标准 CROS(跨域资源共享) 来解决, 另外jsonp也是一种颗星的纯前端解决方案;
其它需要交代的项目背景:
整体技术方案: Angularjs+SSM+(CAS+Spring Security)+Freemarker+...
方案实施:
1. 实现跳转到User模块的登录页面:
在ftl页面中给登录添加点击事件, 然后跳转到User模块的 "跳转页" 跳转页不被user模块放行, 所以需要登录. 跳转方法如下(地址栏操作不涉及跨域):
$scope.login=function(){
location.href="http://192.168.25.1:8097/jumpBack.html?backPage="+location.href;
}
为什么要带上参数呢? 那么就引出了第2个操作, "跳转页":
2. 当访问 "跳转页" 时, 被要求登录, 登录后便进入跳转页, 跳转页立即获取地址栏参数: backPage, 然后再通过地址栏操作跳转回原静态页面, 此时借用的 User 模块已处于登录状态. 跳转页代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>跳转中</title>
</head>
<body>
<script type="text/javascript">
<!--获取地址栏参数的方法-->
function GetQueryString(name) {
var reg = new RegExp("(^|&)"+ name +"=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if(r!=null)return unescape(r[2]); return null;
}
location.href=GetQueryString("backPage");
</script>
</body>
</html>
当跳转回静态页面后, 静态页面需要通过 ajax 异步方式在页面加载时即获取用户名, 又要借User模块获取用户名方法医用, 此时即出现跨域问题, 引出第 3 个操作: 跨域获取用户名:
3. 跨域获取用户名:
springMVC为我们提供了简便的跨域解决方案, 在允许被跨域访问的方法上添加注解, 如下 User 模块 LoginController 中的获取用户名方法 (其中对跨域起着作用的是 @CrossOrigin 注解, 其中 allowCredentials = "true" 为默认值, 代表允许携带Cookie):
@RestController
@RequestMapping("login")
public class LoginController {
@RequestMapping("findUsername")
@CrossOrigin(origins = "http://item.pinyougou.com",allowCredentials = "true")
public Map<String,String> findUsername(){
String username = SecurityContextHolder.getContext().getAuthentication().getName();
HashMap<String, String> loginHashMap = new HashMap<>();
loginHashMap.put("username",username);
return loginHashMap;
}
}
但是, 跨域时前端是默认不携带Cookie的, 故前端获取用户名方法如下 (其中cookie的配置为: {'withCredentials':true}):
$scope.getUsername=function(){
$http.get("http://192.168.25.1:8097/login/findUsername.do",{'withCredentials':true}).success(function(response){
$scope.username=response.username;
})
}
登录效果:
登录前:
登录页面:
地址栏实际为User模块的登录:
登录后:
4. 至此, 登录方案实施完毕, 继续实施登出方案, 登出直接访问CAS服务器, 不需要借模块, 唯一需要解决的问题是, 登录后继续留在当前静态页面:
故, 需要对CAS服务端进行少许配置, CAS服务端的 WEB-INF/cas_servlet.xml中:
<bean id="logoutAction" class="org.jasig.cas.web.flow.LogoutAction"
p:servicesManager-ref="servicesManager"
p:followServiceRedirects="${cas.logout.followServiceRedirects:true}"/>
然后添加登出后跳转的页面作为service参数即可, 故登出页面js方法如下:
$scope.logout=function(){
location.href="http://192.168.25.129:8082/cas/logout?service="+location.href;
}
登出效果 (回到未登录状态):
总结:
1. 重要的不是具体的解决方案, 而是解决方案的思路, 抓住问题本质, 比如, 本博描述的问题的本质是: 要单点登录, 要获取用户名, 要单点登出! 那就去想办法得到这三个功能即可.
2. 与其说是借用该模块的登录功能, 不如说是将nginx代理的静态页面模块作为了User模块的资模块中;
啰嗦几句: 有人会疑问, name这样不是额外增加了User模块的压力吗?
1. 静态页面的每一次登录需要调用一次获取用户名方法, 所以, 调用谁不是调用呢?
2. 当用户未登录时, CAS直接拦截了该方法的访问, 所以, 每一次的方法访问成功都会是有效的, 不会出现无意义的方法调用;
3. 若同一浏览器登录后, 每刷新一次就需要调用一次获取用户名方法, 这个只能依靠本地cookie存储用户名来解决, 存储的数据模型为Map(username:"张三", isLogin: 布尔类型) , cookie有效期与CAS登录有效时长相同;
第一次登录后, cookie被设置, 当第二次登录时, 检测该Map, 已得出是否登录结论, 若参数2位false, 还可以实现提醒用户登录的功能, 类似京东效果: 如图:
----------------------------------------------------------------------------全篇完------------------------------------------------------------------------------------------------
转载请注明出处: 划船一哥;