天天看點

阿裡淘寶天貓單點登入項目實戰(附源碼)

文章目錄

    • 一、簡介
    • 二、單點登入常見方案
    • 三、技術架構與實戰
    • 四、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

如果對大家有幫助,請幫忙點贊關注哈