天天看点

阿里淘宝天猫单点登录项目实战(附源码)

文章目录

    • 一、简介
    • 二、单点登录常见方案
    • 三、技术架构与实战
    • 四、github地址

一、简介

  • 背景

    在企业发展初期,企业使用的系统很少,通常有一个或者两个,每个系统都有自己的登录模块,运营人员每天用自己的账号登录很方便。

    随着企业的发展,用到的系统随之增多,运行人员在操作自己的系统时,需要多次登录,而且每个系统的账号可能不一样,这对于运营人员来说很不方便。于是,于是就想到是不是可以在一个系统登录,其它系统就不用登录了呢?这就是单点登录要解决的问题

  • 简介

    单点登录英文全称Single Sign On,简称SSO。是目前比较流行的企业业务整合的解决方案之一。SSO不是一种架构,而是一种解决方案。它的解释是:在多个系统中,只需要登录一次,就可以访问其他相互信任的应用系统(同一浏览器内)

  • 应用场景
    阿里淘宝天猫单点登录项目实战(附源码)

二、单点登录常见方案

1、cas(单点登录)

解决问题:多个系统只需登录一次,无需重复登录

原理:授权服务器、被授权客户端

①、授权服务器(一个)保存了全局的一份session,客户端(多个)各自保存自己的session

②、客户端登录时判断自己的session是否已登录,若未登陆,则(告诉浏览器)重定向到授权服务器(参数带上自己的地址,用于回调)

③、授权服务器判断全局的session是否已登录,若未登录则定向到登录页面,提示用户登录,登录成功后,授权服务器重定向到客户端(参数带上ticket【一个凭证号】)

④、客户端收到ticket后,请求服务器获取用户信息

⑤、服务器同意授权后,服务端保存用户信息至全局session,客户端将用户信息保存至本地session

⑥、默认不支持http请求,仅支持https。

**缺点:**CAS单点登录适用于传统应用场景,对微服务化应用,前后端分离应用、支持性较差。

2、oauth2(第三方登录授权)

解决问题:第三方系统访问主系统资源,用户无需在主系统的账号告知第三方,只有通过主系统的授权,第三方就可以使用主系统的资源。

如:APP1需要微信支付,微信支付会提示用户是否授权,用户授权后,APP1就可以用微信支付功能了。

OAuth是用来允许用户授权第三方应用访问他在另一个服务器上的资源的一种协议,他不是用来做单点登录的,但是我们可以利用他来实现单点登录。

原理:主系统、授权系统(给主系统授权用的,也可以跟主系统是同一个系统),第三方系统

①、第三方系统需要使用主系统的资源。第三方重定向到授权系统

②、根据不同的授权方式,授权系统提示用户授权

③、用户授权后,授权系统返回一个授权凭证(AccessToken)给第三方授权系统)(accesstoken是有效期的)

④、第三方使用accesstoken访问主系统资源(accesstoken失效后,第三方需要重新请求授权系统,以获取新的accesstoken)

3、jwt(客户端)

个性化

Json Web Token(JWT),是为了在网络应用环境间传递声明而执行的一种基于json的开放标准。

该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(sso)场景。jwt的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

三、技术架构与实战

  • 实验准备

    SSO : http://localhost:8082

    tianmao: http://localhost:8083

    taobao: http://localhost:8084

1、登录架构图

阿里淘宝天猫单点登录项目实战(附源码)

1.1、 搭建SSO认证中心服务

  • 后端登录接口:
/**
     * 登录接口
     * @param username 用户名
     * @param password  密码
     * @param redirectUrl 跳转地址
     * @param redirectAttributes 因为使用重定向的跳转方式的情况下,跳转到的地址无法获取 request 中的值。RedirecAtrributes 很好的解决了这个问题
     * @param session
     * @param model
     * @return
     */
    @RequestMapping("/login")
    public String login(String username, String password, String redirectUrl,RedirectAttributes redirectAttributes, HttpSession session, Model model){
        if (redirectUrl == null){
            return "login";
        }
        //模拟数据
        if ("admin".equals(username) && "123456".equals(password)){
            //1、给用户创建一个token令牌:唯一,保存到数据库,模拟数据库
            String token = UUID.randomUUID().toString();
            //2、保存到数据库
            MockDb.T_TOKEN.add(token);
            //3、在服务器中存在回话信息
            session.setAttribute("token",token);
            //4、返回给客户端
            redirectAttributes.addAttribute("token",token);
            //从哪里来回到那里去
            return "redirect:"+redirectUrl;
        }
        //登录不成功
        System.out.println("用户名密码不正确");
        model.addAttribute("redirectUrl","redirectUrl");
        return "login";
    }
           
  • 服务端校验token:
/**
     * 校验token
     * @param token 
     * @param clientUrl  客户端退出登录的url
     * @param jsessionId
     * @return
     */
    @RequestMapping("verify")
    @ResponseBody
    public String verify(String token,String clientUrl,String jsessionId){
        if (MockDb.T_TOKEN.contains(token)){
            //保存用户xinxi
            List<ClientInfoVo> clientInfoVoList = MockDb.T_CLIENT_INFO.get("token");
            if (clientInfoVoList == null){
                clientInfoVoList = new ArrayList<ClientInfoVo>();
                MockDb.T_CLIENT_INFO.put(token,clientInfoVoList);
            }
            ClientInfoVo clientInfoVo = new ClientInfoVo();
            clientInfoVo.setClientUrl(clientUrl);
            clientInfoVo.setJsessionId(jsessionId);
            clientInfoVoList.add(clientInfoVo);;
            return "true";
        }
        return "false";
    }
           
  • 判断用户是否存在全局会话
/**
     *  判断用户是否存在全局会话
     * @param redirectUrl
     * @param redirectAttributes
     * @param session
     * @param model
     * @return
     */
    @RequestMapping("/checkLogin")
    public String checkLogin(String redirectUrl,RedirectAttributes redirectAttributes ,HttpSession session, Model model){
        //1、判断用户是否登录,是否拥有全局会话 token
        String token = (String) session.getAttribute("token");
        if (StringUtils.isEmpty(token)){
            //没有全局会话,去登录页面,我从哪里来不能丢
            model.addAttribute("redirectUrl",redirectUrl);
            return "login";
        }else {
            //存在全局会话,返回到来的地方
            redirectAttributes.addAttribute("token",token);
            return "redirect:"+redirectUrl;
        }
    }
           

1.2、搭建客户端

  • 访问客户端任意接口
@RequestMapping("/taobao")
    public ModelAndView index(Model model){
        model.addAttribute("logoutURL", SSOClientUtil.getServerLogoutUrl());
        return new ModelAndView("taobao");
    }

    @RequestMapping("/")
    public ModelAndView index1(Model model){
        model.addAttribute("logoutURL", SSOClientUtil.getServerLogoutUrl());
        return new ModelAndView("taobao");
    }
           
  • 过滤拦截接口
/**
     * true:放行    false:拦截
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、判断用户是否存在会话, isLogin=true
        HttpSession session = request.getSession();
        Boolean isLogin = (Boolean) session.getAttribute("isLogin");
        if (isLogin!=null && isLogin){
            return true;
        }

        //2、判断token
        String token  = request.getParameter("token");
        if (!StringUtils.isEmpty(token)){
            //防止token被伪造,拿到服务器去校验
            //服务器的地址
            String httpUrl = SSOClientUtil.SERVER_URL_PREFIX+"/verify";
            //需要验证的参数
            HashMap<String,String> params = new HashMap<>();
            params.put("token",token);
            params.put("clientUrl",SSOClientUtil.getClientLogoutUrl());
            params.put("jsessionId",session.getId());
            try{
                String verify = HttpUtil.httpRequest(httpUrl,params);
                if ("true".equals(verify)){
                    System.out.println("服务器验证token通过");
                    session.setAttribute("isLogin",true);
                    return true;
                }
            }catch (Exception e){
                System.out.println("服务器验证token异常");
                e.printStackTrace();
            }
        }

        //没有会话,或者token校验失败,跳转到统一认证中心,检测系统是否登录
        SSOClientUtil.redirectToSSOUrl(request,response);
        return false;
    }
}
           

2、登出架构图

阿里淘宝天猫单点登录项目实战(附源码)

客户端登出操作,本质上是调用SSO服务端的登出操作,服务端监听到有退出操作,就一一通知所有客户端都退出。

  • 客户端退出接口
@RequestMapping("/logOut")
    public String logout(HttpSession session){
        String token = (String) session.getAttribute("token");
        List<ClientInfoVo> clientInfoVoList = MockDb.T_CLIENT_INFO.get(token);
        session.invalidate();
        //通知淘宝天猫销毁session,监听器
        return "login";
    }
           
  • 监听器
public class MySessionListener implements HttpSessionListener {
    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        HttpSession session = se.getSession();
        String token = (String) session.getAttribute("token");
        //销毁用户信息
        MockDb.T_TOKEN.remove(token);
        List<ClientInfoVo> clientInfoVoList = MockDb.T_CLIENT_INFO.remove(token);
        if (clientInfoVoList!=null){
            for (ClientInfoVo vo:clientInfoVoList){
                try{
                    //便利通知客户端注销
                    System.out.println("注销地址:"+vo.getClientUrl());
                    HttpUtil.sendHttpRequest(vo.getClientUrl(),vo.getJsessionId());
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}
           

四、github地址

欢迎大家访问源码地址:https://github.com/goodjuan-xj/unified-login-system

如果对大家有帮助,请帮忙点赞关注哈