第二十一篇:單點(diǎn)登錄系統(tǒng)的具體實(shí)現(xiàn)(2)

前言:
前面我們已經(jīng)實(shí)現(xiàn)了數(shù)據(jù)的校驗和注冊功能烟逊,下面是實(shí)現(xiàn)登錄功能;用戶的登錄涉及到三部分
1.前臺工程
2.單點(diǎn)登錄服務(wù)
3.redis服務(wù)眷唉;相比于傳統(tǒng)服務(wù)冬阳,我們沒有把用戶信息存在session中,而是放在redis中

image.png

登錄的流程:
登錄的處理流程:
1氯窍、登錄頁面提交用戶名密碼狼讨。
2、登錄成功后生成token布隔。Token相當(dāng)于原來的jsessionid衅檀,字符串计济,可以使用uuid学密。
3腻暮、把用戶信息保存到redis具垫。Key就是token筝蚕,value就是TbUser對象轉(zhuǎn)換成json起宽。
4坯沪、使用String類型保存Session信息≡逄牵可以使用“前綴:token”為key
5、設(shè)置key的過期時間潘拱。模擬Session的過期時間。一般半個小時禽最。
6呛占、把token寫入cookie中。
7帜篇、Cookie需要跨域。例如www.taotao.com\sso.taotao.com\order.taotao.com诫咱,可以使用工具類笙隙。
8、Cookie的有效期坎缭。關(guān)閉瀏覽器失效竟痰。
9、登錄成功掏呼。

1.實(shí)現(xiàn)登錄功能

  • 功能分析
    請求的url:/user/login
    請求的方法:POST
    參數(shù):username凯亮、password,表單提交的數(shù)據(jù)。可以使用方法的形參接收。
    返回值:json數(shù)據(jù)疏之,使用TaotaoResult包含一個token。
  • Dao層
    查詢tb_user表索抓。單表查詢。可以使用逆向工程另患。
  • Service層
    參數(shù):
    1薯嗤、用戶名:String username
    2玻褪、密碼:String password
    返回值:TaotaoResult捻浦,包裝token。
    業(yè)務(wù)邏輯:
    1、判斷用戶名密碼是否正確凡怎。
    2、登錄成功后生成token捆昏。Token相當(dāng)于原來的jsessionid举户,字符串拐云,可以使用uuid泳桦。
    3演痒、把用戶信息保存到redis兆沙。Key就是token龙誊,value就是TbUser對象轉(zhuǎn)換成json。
    4推溃、使用String類型保存Session信息助赞。可以使用“前綴:token”為key
    5掠手、設(shè)置key的過期時間做祝。模擬Session的過期時間轩性。一般半個小時脯厨。
    6壳澳、返回TaotaoResult包裝token铃彰。
@Override
    public TaotaoResult login(String username, String password) {
        // 1竹揍、判斷用戶名密碼是否正確。
        TbUserExample example = new TbUserExample();
        Criteria criteria = example.createCriteria();
        criteria.andUsernameEqualTo(username);
        //查詢用戶信息
        List<TbUser> list = userMapper.selectByExample(example);
        if (list == null || list.size() == 0) {
            return TaotaoResult.build(400, "用戶名或密碼錯誤");
        }
        TbUser user = list.get(0);
        //校驗密碼
        if (!user.getPassword().equals(DigestUtils.md5DigestAsHex(password.getBytes()))) {
            return TaotaoResult.build(400, "用戶名或密碼錯誤");
        }
        // 2邪铲、登錄成功后生成token鬼佣。Token相當(dāng)于原來的jsessionid,字符串霜浴,可以使用uuid晶衷。
        String token = UUID.randomUUID().toString();
        // 3、把用戶信息保存到redis阴孟。Key就是token晌纫,value就是TbUser對象轉(zhuǎn)換成json。
        // 4永丝、使用String類型保存Session信息锹漱。可以使用“前綴:token”為key
        user.setPassword(null);
        jedisClient.set(USER_INFO + ":" + token, JsonUtils.objectToJson(user));
        // 5慕嚷、設(shè)置key的過期時間哥牍。模擬Session的過期時間。一般半個小時喝检。
        jedisClient.expire(USER_INFO + ":" + token, SESSION_EXPIRE);
        // 6嗅辣、返回TaotaoResult包裝token。
        return TaotaoResult.ok(token);
    }

login接口中用到了配置文件挠说,如下圖所示澡谭。


image.png
#redis key的名稱
USER_SESSION=USER_SESSION
#session的過期時間
SESSION_EXPIRE=1800
  • 發(fā)布服務(wù)和引用服務(wù)
    不用做任何修改~
  • Controller
    請求的url:/user/login
    請求的方法:POST
    參數(shù):username、password损俭,表單提交的數(shù)據(jù)蛙奖。可以使用方法的形參接收杆兵。
    HttpServletRequest雁仲、HttpServletResponse
    返回值:json數(shù)據(jù),使用TaotaoResult包含一個token琐脏。
    我們在登錄的時候需要把token保存到cookie中攒砖,為了方便我們操作,我們封住了了一個工具類
    image.png
package com.taotao.utils;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


/**
 * 
 * Cookie 工具類
 *
 */
public final class CookieUtils {

    /**
     * 得到Cookie的值, 不編碼
     * 
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName) {
        return getCookieValue(request, cookieName, false);
    }

    /**
     * 得到Cookie的值,
     * 
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
        Cookie[] cookieList = request.getCookies();
        if (cookieList == null || cookieName == null) {
            return null;
        }
        String retValue = null;
        try {
            for (int i = 0; i < cookieList.length; i++) {
                if (cookieList[i].getName().equals(cookieName)) {
                    if (isDecoder) {
                        retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
                    } else {
                        retValue = cookieList[i].getValue();
                    }
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return retValue;
    }

    /**
     * 得到Cookie的值,
     * 
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
        Cookie[] cookieList = request.getCookies();
        if (cookieList == null || cookieName == null) {
            return null;
        }
        String retValue = null;
        try {
            for (int i = 0; i < cookieList.length; i++) {
                if (cookieList[i].getName().equals(cookieName)) {
                    retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
             e.printStackTrace();
        }
        return retValue;
    }

    /**
     * 設(shè)置Cookie的值 不設(shè)置生效時間默認(rèn)瀏覽器關(guān)閉即失效,也不編碼
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
            String cookieValue) {
        setCookie(request, response, cookieName, cookieValue, -1);
    }

    /**
     * 設(shè)置Cookie的值 在指定時間內(nèi)生效,但不編碼
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
            String cookieValue, int cookieMaxage) {
        setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
    }

    /**
     * 設(shè)置Cookie的值 不設(shè)置生效時間,但編碼
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
            String cookieValue, boolean isEncode) {
        setCookie(request, response, cookieName, cookieValue, -1, isEncode);
    }

    /**
     * 設(shè)置Cookie的值 在指定時間內(nèi)生效, 編碼參數(shù)
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
            String cookieValue, int cookieMaxage, boolean isEncode) {
        doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
    }

    /**
     * 設(shè)置Cookie的值 在指定時間內(nèi)生效, 編碼參數(shù)(指定編碼)
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
            String cookieValue, int cookieMaxage, String encodeString) {
        doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
    }

    /**
     * 刪除Cookie帶cookie域名
     */
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
            String cookieName) {
        doSetCookie(request, response, cookieName, "", -1, false);
    }

    /**
     * 設(shè)置Cookie的值,并使其在指定時間內(nèi)生效
     * 
     * @param cookieMaxage cookie生效的最大秒數(shù)
     */
    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);
            if (cookieMaxage > 0)
                cookie.setMaxAge(cookieMaxage);
            if (null != request) {// 設(shè)置域名的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();
        }
    }

    /**
     * 設(shè)置Cookie的值祭衩,并使其在指定時間內(nèi)生效
     * 
     * @param cookieMaxage cookie生效的最大秒數(shù)
     */
    private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
            String cookieName, String cookieValue, int cookieMaxage, String encodeString) {
        try {
            if (cookieValue == null) {
                cookieValue = "";
            } else {
                cookieValue = URLEncoder.encode(cookieValue, encodeString);
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if (cookieMaxage > 0)
                cookie.setMaxAge(cookieMaxage);
            if (null != request) {// 設(shè)置域名的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的域名
     */
    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;
    }

}

image.png

下面利用這個工具類實(shí)現(xiàn)登錄

@RequestMapping(value="/user/login", method=RequestMethod.POST)
    @ResponseBody
    public TaotaoResult login(String username, String password,
            HttpServletRequest request, HttpServletResponse response) {
        // 1灶体、接收兩個參數(shù)。
        // 2掐暮、調(diào)用Service進(jìn)行登錄蝎抽。
        TaotaoResult result = userService.login(username, password);
        // 3、從返回結(jié)果中取token路克,寫入cookie樟结。Cookie要跨域。
        String token = result.getData().toString();
        CookieUtils.setCookie(request, response, COOKIE_TOKEN_KEY, token);
        // 4精算、響應(yīng)數(shù)據(jù)瓢宦。Json數(shù)據(jù)。TaotaoResult灰羽,其中包含Token驮履。
        return result;
        
    }

image.png

下面我們來測試,由于在taotao-common中添加了工具類廉嚼,因此需要重新打包taotao-common工程到本地maven倉庫玫镐,由于在taotao-sso-interface添加了接口,因此我們需要把taotao-sso工程重新打包到本地maven倉庫怠噪。
在啟動工程前要保證zookeeper和redis服務(wù)器處于啟動狀態(tài)?炙啤!傍念!

  • 測試
    打包完后矫夷,我們啟動taotao-sso工程和taotao-sso-web工程,啟動之后憋槐,我們還是使用RestClient3.5來測試双藕。我們修改下URL地址將/user/register修改為/user/login,然后點(diǎn)擊下面那個圈住的圖標(biāo)秦陋。


    image.png

    可以看到彈出一個小框蔓彩,我們點(diǎn)擊"是"


    image.png

    登錄我們只用用戶名和密碼治笨,不用電話和郵箱驳概,因此我們把電話和郵箱兩個參數(shù)去掉,選中email和phone兩行旷赖,右鍵點(diǎn)擊"Remove Selected"顺又。
    image.png

    去掉email和phone之后,雙擊value那一個框進(jìn)入編輯狀態(tài)等孵,修改username的值為"zhangsan6"稚照,如下圖,點(diǎn)擊"Generate"。
    image.png

    點(diǎn)擊執(zhí)行按鈕果录,可以看到返回的結(jié)果狀態(tài)碼是200上枕,data中存放的是token的值。說明登錄成功了弱恒,同時也說明我們的登錄接口正確


    image.png

2.通過token獲取用戶的信息

  • 功能分析
    請求的url:/user/token/{token}
    參數(shù):String token需要從url中取辨萍。
    返回值:json數(shù)據(jù)。使用TaotaoResult包裝Tbuser對象返弹。
    業(yè)務(wù)邏輯:
    1锈玉、從url中取參數(shù)。
    2义起、根據(jù)token查詢redis拉背。
    3、如果查詢不到數(shù)據(jù)默终。返回用戶已經(jīng)過期椅棺。
    4、如果查詢到數(shù)據(jù)齐蔽,說明用戶已經(jīng)登錄土陪。
    5、需要重置key的過期時間肴熏。
    6鬼雀、把json數(shù)據(jù)轉(zhuǎn)換成TbUser對象,然后使用TaotaoResult包裝并返回蛙吏。
  • Dao層
    使用JedisClient對象源哩。(因為我們的數(shù)據(jù)已經(jīng)保存在redis中了,這樣就不用再訪問數(shù)據(jù)庫)
  • Service層
    參數(shù):String token
    返回值:TaotaoResult
@Override
    public TaotaoResult getUserByToken(String token) {
        // 2鸦做、根據(jù)token查詢redis励烦。
        String json = jedisClient.get(USER_INFO + ":" + token);
        if (StringUtils.isBlank(json)) {
            // 3、如果查詢不到數(shù)據(jù)泼诱。返回用戶已經(jīng)過期坛掠。
            return TaotaoResult.build(400, "用戶登錄已經(jīng)過期,請重新登錄治筒。");
        }
        // 4屉栓、如果查詢到數(shù)據(jù),說明用戶已經(jīng)登錄耸袜。
        // 5友多、需要重置key的過期時間。
        jedisClient.expire(USER_INFO + ":" + token, SESSION_EXPIRE);
        // 6堤框、把json數(shù)據(jù)轉(zhuǎn)換成TbUser對象域滥,然后使用TaotaoResult包裝并返回纵柿。
        TbUser user = JsonUtils.jsonToPojo(json, TbUser.class);
        return TaotaoResult.ok(user);
    }

-Controller
請求的url:/user/token/{token}
參數(shù):String token需要從url中取。
返回值:json數(shù)據(jù)启绰。使用TaotaoResult包裝Tbuser對象昂儒。

    @RequestMapping("/user/token/{token}")
    @ResponseBody
    public TaotaoResult getUserByToken(@PathVariable String token) {
        TaotaoResult result = userService.getUserByToken(token);
        return result;
    }

3.安全退出

所謂的安全退出就是將token從redis中刪除

  • 功能分析
    請求的url:/user/logout/{token}
    參數(shù):String token需要從url中取。
    返回值:json數(shù)據(jù)撤缴。使用TaotaoResult包裝Tbuser對象刹枉。
    業(yè)務(wù)邏輯:
    1、從url中取參數(shù)屈呕。
    2微宝、根據(jù)key刪除redis中token中的數(shù)據(jù)(直接設(shè)置為過期)
    4.使用TaotaoResult返回結(jié)果
  • Service層
 public TaotaoResult logout(String token) {
        String key = USER_SESSION + ":" + token;
        //根據(jù)key刪除redis中token中的數(shù)據(jù)(直接設(shè)置為過期)
        jedisClient.expire(key,0);
        return TaotaoResult.ok();
    }
  • Controller層
 //安全退出
    @RequestMapping(value = "/user/logout/{token}",method = RequestMethod.GET)
    @ResponseBody
    public TaotaoResult logout(@PathVariable String token){
        TaotaoResult result = userService.logout(token);
        return result;
    }  
  • 測試
    我們又增加了一個新的接口,記得要install一下虎眨,把工程打包到本地
    為了測試蟋软,我們還需要重新發(fā)送下登錄請求,獲取一個新的token(原來登錄的token已經(jīng)過期了嗽桩,設(shè)置的過期時間是30分鐘)岳守,如下圖所示。
    image.png

    我們使用新的token來測試安全退出碌冶,在地址欄訪問http://localhost:8088/user/logout/ba9bb30f-1a61-4b61-9931-6b94d1aeefdf湿痢,結(jié)果如下圖所示
    image.png

    這時我們再嘗試通過該token來獲取用戶信息,在地址欄訪問http://localhost:8088/user/token/ba9bb30f-1a61-4b61-9931-6b94d1aeefdf扑庞,結(jié)果如下圖所示譬重,顯示token已過期(從登錄到現(xiàn)在還不到30分鐘),這說明我們的安全退出沒問題了罐氨。
    image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末臀规,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子栅隐,更是在濱河造成了極大的恐慌塔嬉,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件租悄,死亡現(xiàn)場離奇詭異谨究,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)恰矩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門记盒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人外傅,你說我怎么就攤上這事纪吮。” “怎么了萎胰?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵碾盟,是天一觀的道長。 經(jīng)常有香客問我技竟,道長冰肴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任榔组,我火速辦了婚禮熙尉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘搓扯。我一直安慰自己检痰,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布锨推。 她就那樣靜靜地躺著铅歼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪换可。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機(jī)與錄音瓣距,去河邊找鬼响疚。 笑死,一個胖子當(dāng)著我的面吹牛译荞,可吹牛的內(nèi)容都是我干的套媚。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼磁椒,長吁一口氣:“原來是場噩夢啊……” “哼堤瘤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起浆熔,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤本辐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后医增,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體慎皱,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年叶骨,在試婚紗的時候發(fā)現(xiàn)自己被綠了茫多。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡忽刽,死狀恐怖天揖,靈堂內(nèi)的尸體忽然破棺而出夺欲,到底是詐尸還是另有隱情,我是刑警寧澤今膊,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布些阅,位于F島的核電站,受9級特大地震影響斑唬,放射性物質(zhì)發(fā)生泄漏市埋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一恕刘、第九天 我趴在偏房一處隱蔽的房頂上張望缤谎。 院中可真熱鬧,春花似錦褐着、人聲如沸坷澡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽洋访。三九已至,卻和暖如春谴餐,著一層夾襖步出監(jiān)牢的瞬間姻政,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工岂嗓, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留汁展,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓厌殉,卻偏偏與公主長得像食绿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子公罕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容