单点登录的一种具体实现

来源:互联网 发布:淘宝能用微信付款吗 编辑:程序博客网 时间:2024/06/06 04:55

单点登录的一种具体实现

本文主要是讲单点登陆的具体实现,原理请参考
单点登录原理与简单实现
本文是在该原理基础上进行修改和实现,为后续需要单点登录的小伙伴提供一个参考。
本人使用的测试程序时用maven搭建,基于springMVC,使用了redis用于存储用户信息。
访问登陆流程
整个访问流程用下图说明:
这里写图片描述
业务系统主要使用过滤器来拦截请求,并根据请求做不同的操作
在未登录的情况下,访问系统1任意私有资源
过滤器判断当前用户未登录(具体判断过程后面解释)

response.sendRedirect("http://localhost:8080/user-center/loginPage?oldUrl="+request.getRequestURL());

跳转到用户中心的登陆页面操作,如果当前用于已经登录,附带全局令牌(globalToken),调转到用户想要登陆的页面。
注:
response.sendRedirect(newUrl)相当于浏览器重新请求了newUrl,所以可以通过session来保存globalToken,用来判断用户是否登陆。
这种只适合一个浏览器,如果两个浏览器就不能判断。

@RequestMapping(value = "/loginPage")    public String login(HttpServletRequest request,String oldUrl,    HttpServletResponse response    ,Model model) throws Exception {        //传递 历史url 到页面        model.addAttribute("oldUrl",oldUrl);        //检查当前请求的浏览器是否登陆        String sessionID=(String) request.getSession().getAttribute("userCenterSession");        if(sessionID!=null){            //如果已经登录返回sessionID(全局令牌)            response.sendRedirect(oldUrl+"?globalToken="+sessionID);            return null;        }        return "loginPage";    }

未登陆用户跳转到登陆页面,下面是一个简单的登陆页面模拟,记得附带oldUrl

<body>    user-center登陆页面    <form action="/user-center/login" method="POST">        用户名:<input name="userName" value="admin">        密码:<input name="password" value="admin">        <input type="hidden" name="oldUrl" value="${oldUrl}">        <button type="submit">登陆</button>    </form></body>

点击登录后执行登陆操作

@RequestMapping(value = "/login", method = RequestMethod.POST)    public void login(HttpServletRequest request, String userName,     String password, String oldUrl,            HttpServletResponse response) throws Exception {        //查看数据库        //用户中心只用来校验账号密码,提供一种登陆状态,        //至于每一个其他系统的权限和数据需要查询其对应的数据库        if (userName.equals("admin") && password.equals("admin")) {        // 生成登录用户唯一标识,全局令牌            String sessionID = UUID.randomUUID().toString();            //根据查询其他数据库需求,把相应的 公用的用户信息  转变为json字符串保存到redis            //为了增加安全性,可以对sessionID进行加密            //没有redis可以考虑存在内存中,如果放在内存中需要增加一个获取信息过程            //如果直接在sendRedirect后面跟用户信息,会增加数据泄露风险            //所以采用httpClient方式  校验令牌或者叫获取用户信息   可以增加安全性            jedisCluster.setex(sessionID, 60 * 30, "userName=" + userName             + "password=" + password);            //存储全局令牌,用于判断浏览器是否登陆            request.getSession().setAttribute("userCenterSession", sessionID);            response.sendRedirect(oldUrl+"?globalToken="+sessionID);        }else {            response.sendRedirect("loginPage");        }    }

用户中心登陆操作完成后,附带全局令牌(globalToken)跳转到系统1的过滤器,判断reque里参数globalToken是否为空,不为空说明是登陆操作之后跳转到当前系统或者是已经登录了跳转到当前系统,并把globalToken存入cookie中。
同时需要判断redis里是否有公共用户信息,如果globalToken存在,但是redis里为空,表明该globalToken是伪造的或者redis存储时间过期,用户需要重新登录。
同时系统1局部令牌Token1的判断亦如此,非空且redis不为空

        //获取全局令牌        Cookie CAllToken = CookieUtil.getCookie(request, "globalToken");        //获取局部令牌        Cookie CSystem1= CookieUtil.getCookie(request, "Token1");        //登陆后带有全局Token        String globalToken=request.getParameter("globalToken");        @SuppressWarnings("resource")        ApplicationContext context = new                FileSystemXmlApplicationContext("classpath:spring/spring-web.xml");        JedisCluster jedisCluster = context.getBean("jedisCluster", JedisCluster.class);        if(globalToken!=null&& jedisCluster.exists(globalToken)){            CookieUtil.saveCookie(response, "globalToken", globalToken);            if (null == CSystem1 || null == CSystem1.getValue()                    ||jedisCluster.exists(CSystem1.getValue())) {                //获取当前用户在当前子系统中的用户信息和权限                //如果需要跳转到之前的页面,参数带上旧的URL                response.sendRedirect("initUserInfo?oldUrl="+uri);                return;            }            filterChain.doFilter(request, response);            return;        }

由于sendRedirect又会重新进入过滤器,所以在

 //登陆后带有全局Token        String globalToken=request.getParameter("globalToken");

前面加一段,用以忽略某些请求

String[] uris = request.getRequestURI().split("/");        if (uris.length < 1) {            response.sendRedirect("http://localhost:8080/user-center/loginPage");;            return;        }        String uri = uris[uris.length - 1];        if (uri.contains(".")){            uri = uri.split("\\.")[uri.split("\\.").length - 1];        }        if (Constants.FILTER_IGNORE.IGNORES.contains(uri)) {            filterChain.doFilter(request, response);            return;        }

其中Constants是一些需要忽略的请求,如js,css等资源,并把initUserInfo请求加入其中。

public interface Constants {    interface FILTER_IGNORE {        @SuppressWarnings("all")        List<String> IGNORES = new ArrayList<String>() {            {                add("css");                add("js");                add("passport");                add("redirect");                add("png");                add("jpg");                add("gif");                add("vm");                add("initUserInfo");            }        };    }   }

进入系统1中的initUserInfo方法:
该方法主要是生产当前用户在系统1(Token1)的局部令牌,并保存用户在系统1 中的私有信息到redis,把Token1作为redis存储的键。然后把Token1保存到cookie中,最后把系统1产生的局部令牌(Token1)注册到用户中心,让用户中心统一管理所以令牌,方便用户退出时清除。

@RequestMapping(value="/initUserInfo")    public void initUserInfo(HttpServletRequest request,String oldUrl,            String password,HttpServletResponse response) throws IOException{        Cookie cookie=CookieUtil.getCookie(request, "globalToken");        //防止用户没有登陆直接在地址栏输入initUserInfo进入此方法        if(cookie==null){            response.sendRedirect("http://localhost:8080/user-center/loginPage            ?oldUrl=http://localhost:8080/system1/index");            return;        }        String userPubliInfo=jedisCluster.get(cookie.getValue());        //根据公共信息,到子系统相应数据库查询当前子系统下的用户信息        String userPriveateInfo="system1 info";        // 生成登录系统1用户的唯一标识        String system1Id = UUID.randomUUID().toString();        //保存用户在系统1的私有信息到redis        jedisCluster.setex(system1Id, 60*30, userPriveateInfo);        //保存系统1的token        CookieUtil.saveCookie(response, "Token1", system1Id);        //创建HttpClient        CloseableHttpClient httpclient = HttpClients.createDefault();        //创建Post请求        HttpPost httpPost=new HttpPost("http://localhost:8080/user-center/registered");        //创建参数列表        List<NameValuePair> formparams = new ArrayList<NameValuePair>();          formparams.add(new BasicNameValuePair("sysId", system1Id));        formparams.add(new BasicNameValuePair("globalId", cookie.getValue()));         //为请求添加参数        httpPost.setEntity(new UrlEncodedFormEntity(formparams));  //进行转码        //执行请求并获取系统注册结果        CloseableHttpResponse result=httpclient.execute(httpPost);        //获得结果字符串        String isSuccess=EntityUtils.toString(result.getEntity());        if(!isSuccess.equals("success")){             response.sendRedirect("http://localhost:8080/user-center/loginPage");        }        if(oldUrl==null||oldUrl.equals("initUserInfo")){            response.sendRedirect("index");            return;        }else{            response.sendRedirect(oldUrl);            return;        }    }

用户中心的系统注册方法,本次存储子系统的令牌是直接放在用户中心的内存中。

@RequestMapping(value = "/registered")    public void registered(HttpServletRequest request, HttpServletResponse response) {        /**         * 如果全局令牌存储在  用户中心 的内存中,本方法用于校验全局令牌是否有效、         * 获取用户公共信息、注册子系统(添加局部令牌)         * 如果令牌存储在 redis,本方法用于注册子系统         */        String sysId = request.getParameter("sysId");        String globalId = request.getParameter("globalId");        List<String> sysIds=RegisteredSystem.registereSysInfo.get(globalId);        if(sysIds==null){            sysIds=new ArrayList<String>();        }        sysIds.add(sysId);        RegisteredSystem.registereSysInfo.put(globalId, sysIds);        try {            response.getWriter().write("success");        } catch (IOException e) {            // TODO Auto-generated catch block            e.printStackTrace();        }    }

子系统注册成功后,会跳转到用户本来想访问的页面,又会重新进入系统1 的过滤器,

//判断全局令牌是否存在及有效if (null != CAllToken && null != CAllToken.getValue()                && jedisCluster.exists(CAllToken.getValue())) {                //判断当前系统的令牌是否存在及有效            if (null == CSystem1 || null == CSystem1.getValue()                    || !jedisCluster.exists(CSystem1.getValue())) {                response.sendRedirect("initUserInfo?oldUrl="+uri);                return;            }            filterChain.doFilter(request, response);            return;        }

局部令牌和全局令牌都存在且有效,最终跳转到用户想要访问的页面。
登出过程
由于登出流程比较简单,所以直接上代码:
在每个子系统的过滤器增加一个对logout的请求拦截

if(uri.equals("logout")){            response.sendRedirect("http://localhost:8080/user-center/logout");            return;        }

用户中心处理来自任何子系统的登出请求,并清除相关数据

@RequestMapping(value = "/logout")    public String logout(HttpServletRequest request, HttpServletResponse response) {        //获取全局令牌        String globalId = (String) request.getSession().getAttribute("userCenterSession");        //获取存储子系统令牌的list        List<String> sysIds=RegisteredSystem.registereSysInfo.get(globalId);        //删除所有子系统令牌,即子系统存储的用户私有信息        for(String sysId:sysIds){            jedisCluster.del(sysId);        }        //删除所有全局令牌,即用户中心存储的用户公共信息        jedisCluster.del(globalId);        //移除用户中心内存中子系统令牌信息        RegisteredSystem.registereSysInfo.remove(globalId);        //移除session中全局令牌信息        request.getSession().removeAttribute("userCenterSession");        return "loginPage";    }

源码参考
其他子系统和系统1类似

总结:
1、用户中心作为一个单独站点,可以通过session判断当前浏览器是否登陆
2、存在多个token,全局token用户存储用户从用户中心查询出来的公共信息,局部token用户存储单个系统的用户私有信息
3、由于token只是随机生成的一个字符串,需要使用token去redis或者内存取相应的信息,所以即使在地址栏中展示token,安全性也可以保障

本人由于经验和能力有限,存在的不足和错误还请指出。

0 0