传统登陆的方式:
1.登陆时,进行验证,验证完毕后将用户信息存储到session
/** * 用户登录 * @param user * @return * HttpSession因为在Survey_2_Component中使用, * 所以在Survey_2_Component工程中加入JSP-API的依赖后才能使用 */ @RequestMapping(value="/guest/user/login",method=RequestMethod.POST) public String userLogin(User user,HttpSession httpSession){ User queryUser = userService.login(user); //将带有id的用户数据存进session域 httpSession.setAttribute(GlobalNames.LOGIN_USER, queryUser); return "redirect:/index.jsp"; }
2.其它系统的登陆验证
用拦截器判断用户是否登陆
public class AuthorityCheckInterceptor implements HandlerInterceptor{ @Autowired private ResService resService; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //1.放行静态资源 if(handler instanceof DefaultServletHttpRequestHandler){ return true; } //6.如果不是公共资源,检查登录状态——区分guest/manager HttpSession session = request.getSession(); if(path.startsWith("/guest")){ User user = (User)session.getAttribute(GlobalNames.LOGIN_USER); if(user==null){ throw new AdminAccessForbiddenException(GlobalMessage.ADMIN_ACCESS_FORBIDDEN); } ..... } }
此方式在只有一个web工程时是没有问题。
jsp的四个作用域page、request、session、application的作用范围
都是同一个web工程中:
page:用户请求的当前页面;
Request:可以通过转发来进行传递。用户请求访问的当前组件,以及和当前web组件共享同一用户请求的web组件。
如:被请求的jsp页面和该页面用<include>指令包含的页面以及<forward>标记包含的其它jsp页面;
Session:同一个http会话,关闭浏览器就可以结束了。同一个http会话中的web组件共享它;
Application:整个web应用的所用web组件共享它。除非关闭tomcat,否则无法关闭。
单点登录
解决多web工程间的相互登陆问题。 即session共享问题
一、什么是单点登录SSO(Single Sign-On)
SSO是一种统一认证和授权机制,指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用中的受保护资源时,不再需要重新登录验证。
二、单点登录解决了什么问题
解决了用户只需要登录一次就可以访问所有相互信任的应用系统,而不用重复登录。
集群环境下会出现要求用户多次登录的情况。
解决方案:
可以使用Session服务器,保存Session信息,使每个节点是无状态。需要模拟Session。
单点登录系统是使用redis模拟Session,实现Session的统一管理。
具体实施
<form id="regForm_mod" name="regForm_mod" method="post" > <li class="regMb30"> <label><font>*</font> 账户名:</label> <span class="regM defaultBorder"> <input id="regName" name="username" class="regInput" type="text" onkeyup="mail_div(event);" onfocus="showtip('regName','userMamErr',8);" onblur="ckmem();" autocomplete="off" maxlength="80"/> <em></em> </span> <span class="regInput" id="userMamErr"></span> </li> <div node-type="layer" class="accountSearch" style="display:none;" id="person_mail"></div> <li> <label><font>*</font> 登录密码:</label> <span class="regM defaultBorder"> <input id="pwd" name="password" class="regInput" autocomplete="off" type="password" onfocus="showPwdtip('password','passwordErr',2);" onkeyup="ckpwd(0,1);" onblur="ckpwd(0,0);"/> <em ></em> </span> <span class="regInput" id="passwordErr"></span> </li> <li class="safetyLayer regPl191" id="pwdStrong"> <font style="font-size:12px;">安全程度:</font><em class="default">弱</em><em class="default">中</em><em class="default">强</em> </li> <li class="regMb30"> <label><font>*</font> 确认密码:</label> <span class="regM defaultBorder"> <input id="pwdRepeat" name="password2" autocomplete="off" class="regInput" type="password" onfocus="showtip('password2','password2Err',3);" onblur="ckpwd2();"/> <em></em> </span> <span class="regInput" id="password2Err"></span> </li> <li class="regMb30"> <label><font>*</font> 验证手机:</label> <span class="regM defaultBorder"> <input id="phone" name="phone" autocomplete="off" class="regInput" type="text" maxlength="11" onfocus="showtip('phone','phoneErr',1);" onblur="$('#phoneErr').html('')"/> <em></em> </span> <span class="regInput" id="phoneErr"></span> </li> <li class="regPl88"> <span class="regM" style="margin-left:29px"> <input id="AgreeId" name="AgreeId" type="checkbox" checked="" onclick="ckAgree();"> <a href="https://passport.e3mall.cn/xy.html" target="_blank" class="checkTitle">我已阅读并同意<font style="font-size:12px;">《宜立方商城用户注册协议》</font></a> </span> <span id="AgreeIdErr" ></span> </li> <li class="register regPl88 regMt10" id="sub_per" style="margin-left:29px"> <input type="hidden" id="tjuid" name="tjuid" value=""> <a href="javascript:void(0);" class="registerNow" id="reg_per_data" onclick="REGISTER.reg()">立即注册</a> </li> <br /><br /> </form>
1.触发注册
onclick="REGISTER.reg()"
2,script采用json变量存储代码
<script type="text/javascript"> var REGISTER={ param:{ //单点登录系统的url surl:"" }, inputcheck:function(){ var flag = true; //不能为空检查 if ($("#regName").val() == "") { showError("regName","userMamErr",defaultArr[8]); flag = false; } if ($("#pwd").val() == "") { showError("pwd","passwordErr",pwdArr[0]); flag = false; } if ($("#phone").val() == "") { showError("phone","phoneErr",memArr[0]); flag = false; } //密码检查 if ($("#pwd").val() != $("#pwdRepeat").val()) { showError("pwdRepeat","password2Err",pwd2Arr[1]); flag = false; } return flag; }, beforeSubmit:function() { //检查用户是否已经被占用 //escape() 函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。 //采用Math.random()为了防止浏览器默认为相同而缓存,包证每次url都不一样 $.ajax({ url : REGISTER.param.surl + "/user/check/"+escape($("#regName").val())+"/1?r=" + Math.random(), success : function(data) { if (data.data) { //检查手机号是否存在 $.ajax({ url : REGISTER.param.surl + "/user/check/"+$("#phone").val()+"/2?r=" + Math.random(), success : function(data) { if (data.data) { REGISTER.doSubmit(); } else { showError("phone","phoneErr",defaultArr[9]); } } }); } else { showError("regName","userMamErr",defaultArr[10]); } } }); }, doSubmit:function() { $.post("/user/register",$("#regForm_mod").serialize(), function(data){ if(data.status == 200){ jAlert('用户注册成功,请登录!',"提示", function(){ REGISTER.login(); }); } else { jAlert("注册失败!","提示"); } }); }, login:function() { location.href = "/page/login"; return false; }, reg:function() { if (this.inputcheck()) { this.beforeSubmit(); } } }; </script>
3.
REGISTER.reg();
3.1 先验证输入完整性;
this.inputcheck()
inputcheck:function(){ var flag = true; //不能为空检查 if ($("#regName").val() == "") { showError("regName","userMamErr",defaultArr[8]); flag = false; } if ($("#pwd").val() == "") { showError("pwd","passwordErr",pwdArr[0]); flag = false; } if ($("#phone").val() == "") { showError("phone","phoneErr",memArr[0]); flag = false; } //密码检查 if ($("#pwd").val() != $("#pwdRepeat").val()) { showError("pwdRepeat","password2Err",pwd2Arr[1]); flag = false; } return flag; }
3.2检查用户是否已经被占用(ajax)
beforeSubmit:function() { //检查用户是否已经被占用 //escape() 函数可对字符串进行编码,这样就可以在所有的计算机上读取该字符串。 //采用Math.random()为了防止浏览器默认为相同而缓存,包证每次url都不一样 $.ajax({ url : REGISTER.param.surl + "/user/check/"+escape($("#regName").val())+"/1?r=" + Math.random(), success : function(data) { if (data.data) { //检查手机号是否存在 $.ajax({ url : REGISTER.param.surl + "/user/check/"+$("#phone").val()+"/2?r=" + Math.random(), success : function(data) { if (data.data) { REGISTER.doSubmit(); } else { showError("phone","phoneErr",defaultArr[9]); } } }); } else { showError("regName","userMamErr",defaultArr[10]); } } });
controller
@RequestMapping("/user/check/{param}/{type}") @ResponseBody public E3Result checkData(@PathVariable String param, @PathVariable Integer type) { E3Result e3Result = userService.checkData(param, type); return e3Result; }
public E3Result checkData(String param, Integer type) { // 1、从tb_user表中查询数据 UserExample example = new UserExample(); Criteria criteria = example.createCriteria(); // 2、查询条件根据参数动态生成。 //1、2、3分别代表username、phone、email if (type == 1) { criteria.andUsernameEqualTo(param); } else if (type == 2) { criteria.andPhoneEqualTo(param); } else if (type == 3) { criteria.andEmailEqualTo(param); } else { return E3Result.build(400, "非法的参数"); } //执行查询 List<User> list = userMapper.selectByExample(example); // 3、判断查询结果,如果查询到数据返回false。 if (list == null || list.size() == 0) { // 4、如果没有返回true。 return E3Result.ok(true); } // 5、使用e3Result包装,并返回。 return E3Result.ok(false); }
3.3 提交验证
REGISTER.doSubmit();
doSubmit:function() {
$.post("/user/register",$("#regForm_mod").serialize(), function(data){
if(data.status == 200){
jAlert('用户注册成功,请登录!',"提示", function(){
REGISTER.login();
});
} else {
jAlert("注册失败!","提示");
}
});
}
/** * 注册 * @return */ @RequestMapping(value="/user/register",method=RequestMethod.POST) @ResponseBody public E3Result register(User user){ E3Result result = userService.createUser(user); return result; }
public E3Result createUser(User user) { // 1、使用TbUser接收提交的请求。 if (StringUtils.isBlank(user.getUsername())) { return E3Result.build(400, "用户名不能为空"); } if (StringUtils.isBlank(user.getPassword())) { return E3Result.build(400, "密码不能为空"); } //校验数据是否可用 E3Result result = checkData(user.getUsername(), 1); if (!(boolean) result.getData()) { return E3Result.build(400, "此用户名已经被使用"); } //校验电话是否可以 if (StringUtils.isNotBlank(user.getPhone())) { result = checkData(user.getPhone(), 2); if (!(boolean) result.getData()) { return E3Result.build(400, "此手机号已经被使用"); } } //校验email是否可用 if (StringUtils.isNotBlank(user.getEmail())) { result = checkData(user.getEmail(), 3); if (!(boolean) result.getData()) { return E3Result.build(400, "此邮件地址已经被使用"); } } // 2、补全TbUser其他属性。 user.setCreated(new Date()); user.setUpdated(new Date()); // 3、密码要进行MD5加密。 String md5Pass = DigestUtils.md5DigestAsHex(user.getPassword().getBytes()); user.setPassword(md5Pass); // 4、把用户信息插入到数据库中。 userMapper.insert(user); // 5、返回e3Result return E3Result.ok(); }
密码进行md5加密:import org.springframework.util.DigestUtils;
String md5Pass = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());
登陆功能
1.进行验证
获取用户名和密码
@RequestMapping(value="/user/login",method=RequestMethod.POST) @ResponseBody public E3Result login(String username, String password, HttpServletRequest request, HttpServletResponse response){ // 1、接收两个参数。 // 2、调用Service进行登录。 E3Result result = userService.login(username, password); //判断是否登录成功 if(result.getStatus()==200){ // 3、从返回结果中取token,写入cookie。Cookie要跨域。 String token = result.getData().toString(); CookieUtils.setCookie(request, response,TOKEN_KEY, token); } // 4、响应数据。Json数据。e3Result,其中包含Token。 return result; }
2. E3Result result = userService.login(username, password);
验证成功后,生成模拟的session存储于redis
public E3Result login(String username, String password) { // 1、判断用户名密码是否正确。 UserExample example = new UserExample(); Criteria criteria = example.createCriteria(); criteria.andUsernameEqualTo(username); //查询用户信息 List<User> list = userMapper.selectByExample(example); if (list == null || list.size() == 0) { return E3Result.build(400, "用户名或密码错误"); } User user = list.get(0); //校验密码 if (!user.getPassword().equals(DigestUtils.md5DigestAsHex(password.getBytes()))) { return E3Result.build(400, "用户名或密码错误"); } // 2、登录成功后生成token。Token相当于原来的jsessionid,字符串,可以使用uuid。 String token = UUID.randomUUID().toString(); // 3、把用户信息保存到redis。Key就是token,value就是TbUser对象转换成json。 // 4、使用String类型保存Session信息。可以使用“前缀:token”为key user.setPassword(null); jedisClient.set("SESSION:" + token, JsonUtils.objectToJson(user)); // 5、设置key的过期时间。模拟Session的过期时间。一般半个小时。 jedisClient.expire( "SESSION:" + token, SESSION_EXPIRE); // 6、返回e3Result包装token。 return E3Result.ok(token); }
user.setPassword(null);//为了密码的安全性
3.将模拟session存储于cookie
//判断是否登录成功 if(result.getStatus()==200){ // 3、从返回结果中取token,写入cookie。Cookie要跨域。 String token = result.getData().toString(); CookieUtils.setCookie(request, response,TOKEN_KEY, token); }
redis上的key值为了唯一性,key="SESSION:" +UUID.randomUUID().toString()
所以不用用户id
为什么要存储到cookie?
cookie存储到客户端,相当于开启免登录。如果存放到session是存放到服务器,每次都去取占用服务器资源。而且这里已经用redis模仿了session。
cookie 和session 的区别:
1、cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗
考虑到安全应当使用session。
3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能
考虑到减轻服务器性能方面,应当使用COOKIE。
4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。
5、所以个人建议:
将登陆信息等重要信息存放为SESSION
其他信息如果需要保留,可以放在COOKIE中
- 这里使用了自己写的CookieUtils
CookieUtils.setCookie(request, response,TOKEN_KEY, token);//不设置不设置生效时间默认浏览器关闭即失效,也不编码
/**
* 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue) {
setCookie(request, response, cookieName, cookieValue, -1);
}
//这里isEncode=false
/**
* 设置Cookie的值 在指定时间内生效, 编码参数
*/
public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
String cookieValue, int cookieMaxage, boolean isEncode) {
doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
}
/** * 设置Cookie的值,并使其在指定时间内生效 * * @param cookieMaxage cookie生效的最大秒数 */ private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) { try { if (cookieValue == null) { cookieValue = ""; } else if (isEncode) { cookieValue = URLEncoder.encode(cookieValue, "utf-8"); } Cookie cookie = new Cookie(cookieName, cookieValue);//设置cookie值 if (cookieMaxage > 0) cookie.setMaxAge(cookieMaxage);//设置cookie生存时间 if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); System.out.println(domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName); } } cookie.setPath("/"); response.addCookie(cookie); } catch (Exception e) { e.printStackTrace(); } }
cookie要做到域名隔离。这个方法是为了提取
获取域名:(为了指定cookie的作用范围)
/** * 得到cookie的域名 */ private static final String getDomainName(HttpServletRequest request) { String domainName = null; String serverName = request.getRequestURL().toString(); if (serverName == null || serverName.equals("")) { domainName = ""; } else { serverName = serverName.toLowerCase(); serverName = serverName.substring(7); final int end = serverName.indexOf("/"); serverName = serverName.substring(0, end); final String[] domains = serverName.split("\."); int len = domains.length; if (len > 3) { // www.xxx.com.cn domainName = "." + domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1]; } else if (len <= 3 && len > 1) { // xxx.com or xxx.cn domainName = "." + domains[len - 2] + "." + domains[len - 1]; } else { domainName = serverName; } } if (domainName != null && domainName.indexOf(":") > 0) { String[] ary = domainName.split("\:"); domainName = ary[0]; } return domainName; }
这里默认采用localhost作为域名设置,在不同工程间传递,cookie任然有效
if (null != request) {// 设置域名的cookie String domainName = getDomainName(request); System.out.println(domainName); if (!"localhost".equals(domainName)) { cookie.setDomain(domainName);// }
cookie.setPath("/");
response.addCookie(cookie);//cookie the Cookie to return to the client返回的客户端的路径
首页展示用户名
1、当用户登录成功后,在cookie中有token信息。
2、从cookie中取token根据token查询用户信息。
3、把用户名展示到首页。
根据token获取用户信息
cookie因为是在客户端,各个工程间是公用的,从cookie中得到token后去查询redis的用户数据。有两种方案:
1)在Controller中取cookie中的token数据,调用sso服务查询用户信息。(每个controller都要修改麻烦)
2)当页面加载完成后使用js取token的数据,使用ajax请求查询用户信息。
所以采用方案2,但面临js跨域读取数据的问题。
问题:服务接口在sso系统中。Sso.e3.com(localhost:8088),在首页显示用户名称,首页的域名是www.e3.com(localhost:8082),使用ajax请求跨域了。
Js不可以跨域请求数据。
Js不可以跨域请求数据。
什么是跨域:
1、域名不同
2、域名相同端口不同。
解决js的跨域问题可以使用jsonp。
Jsonp不是新技术,跨域的解决方案。使用js的特性绕过跨域请求。Js可以跨域加载js文件。
2.1. Json实现
2.1.1. 客户端
使用jQuery。
2.1.2. 服务端
1、接收callback参数,取回调的js的方法名。
2、业务逻辑处理。
3、响应结果,拼接一个js语句。
跨域传输,其它不变,增加dataType:"jsonp" 就可以实现跨域传送数据了
下例中的是到另外的web工程获取数据(端口不同)。
$.ajax({ url : "http://localhost:8089/user/token/" + _ticket, dataType : "jsonp", type : "GET", success : function(data){ if(data.status == 200){ var username = data.data.username; var html = username + ",欢迎来到宜立方购物网!<a href="http://www.e3mall.cn/user/logout.html" class="link-logout">[退出]</a>"; $("#loginbar").html(html); } } })
在跨域时,请求数据,默认会带参数callback
@RequestMapping("/user/token/{token}") @ResponseBody public Object getUserByToken(@PathVariable("token") String token,String callback){ E3Result result = userService.getUserByToken(token); //响应结果之前,判断是否为jsonp请求 if(StringUtils.isNotBlank(callback)){ //把结果封装成一个js语句响应 MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result); mappingJacksonValue.setJsonpFunction(callback); return mappingJacksonValue; } return result; }
在首页显示用户名查询cookie通过jQuery来实现:
<script type="text/javascript" src="/js/e3mall.js"></script>
json变量的js
var E3MALL = { checkLogin : function(){ var _ticket = $.cookie("token");//读取cookie:
if(!_ticket){//cokie值为空就返回空 return ; }
//不为空,就到redis获取 $.ajax({ url : "http://localhost:8089/user/token/" + _ticket, dataType : "jsonp", type : "GET", success : function(data){ if(data.status == 200){ var username = data.data.username; var html = username + ",欢迎来到宜立方购物网!<a href="http://www.e3mall.cn/user/logout.html" class="link-logout">[退出]</a>"; $("#loginbar").html(html); } } }); } } $(function(){ // 查看是否已经登录,如果已经登录查询登录信息 E3MALL.checkLogin(); });
加载页面就检查是否登陆
$(function(){
// 查看是否已经登录,如果已经登录查询登录信息
E3MALL.checkLogin();
});
登陆就通过redis获取数据重新设置redis模拟session数据存储时间后存储到cookie
@Override public E3Result getUserByToken(String token) { //从redis缓存中获取后存储到cookie中 String json = jedisClient.get("SESSION:"+token); if(StringUtils.isBlank(json)){ // 3、如果查询不到数据。返回用户已经过期。 return E3Result.build(400, "用户登录已经过期,请重新登陆"); } // 4、如果查询到数据,说明用户已经登录 // 5、需要重置key的过期时间 jedisClient.expire("SESSION:"+token, SESSION_EXPIRE); //把json字符串转化为pojo User user = JsonUtils.jsonToPojo(json, User.class); return E3Result.ok(user); }
返回数据包装为js语句返回:(创建callback函数)
//把结果封装成一个js语句响应 MappingJacksonValue mappingJacksonValue = new MappingJacksonValue(result); mappingJacksonValue.setJsonpFunction(callback); return mappingJacksonValue;
成功后对数据进行处理:(将首页的请登录html进行替换)
$.ajax({ url : "http://localhost:8089/user/token/" + _ticket, dataType : "jsonp", type : "GET", success : function(data){ if(data.status == 200){ var username = data.data.username; var html = username + ",欢迎来到宜立方购物网!<a href="http://www.e3mall.cn/user/logout.html" class="link-logout">[退出]</a>"; $("#loginbar").html(html); } } });
var html = username + ",欢迎来到宜立方购物网!<a href="http://www.e3mall.cn/user/logout.html" class="link-logout">[退出]</a>";
$("#loginbar").html(html);
替换掉以下的html
<span id="loginbar" style="margin-right: 15px;"> <a href="http://localhost:8089/page/login">请登录</a> </span>
就这样实现了首页显示用户信息,同时也实现了人为刷新后,重新设置redis用户时间。