单点登录

传统登陆的方式:

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用户时间。







原文地址:https://www.cnblogs.com/limingxian537423/p/7664428.html