我是怎么做App Token認證的-SpringBoot版本

此前阻桅,我寫過一篇《我是怎么做App Token認證的》 的文章凉倚,使用的是PHP技術(shù),這次將本人的SpringBoot版本分享一下嫂沉,其中使用了Apache的Shiro作為權(quán)限管理組件稽寒。老規(guī)矩,關(guān)于基礎(chǔ)的知識比如Shiro的原理及應用趟章,同學們請自行百度杏糙。

在這里,我簡單介紹一下我是怎么具體實現(xiàn)的蚓土,重點描述結(jié)合Shiro宏侍,如何實現(xiàn)token生成、token校驗及token緩存蜀漆。

生成Token

服務(wù)端接收客戶端傳遞的username和password等請求谅河,在數(shù)據(jù)庫中檢查,如果用戶名密碼匹配的話确丢,表示登錄成功绷耍,服務(wù)端生成并返回一個token訪問令牌。

    @PostMapping("/login/wechat")
    public R loginWechat(@RequestParam String openid, @RequestParam(required = false) String nickname,
        @RequestParam(required = false, defaultValue = "0") int gender, @RequestParam(required = false) String avatar,
        HttpServletRequest request) {

        Auth auth = new Auth();
        auth.setIdentify(openid).setIdentifyType(Constants.AuthType.WECHAT);
        auth.setLastLoginTime(LocalDateTime.now());
        OAuthUserVO oAuthUserVO = new OAuthUserVO();
        oAuthUserVO.setAvatar(avatar);
        oAuthUserVO.setGender(gender);
        oAuthUserVO.setUsername(nickname);

        return loginThird(auth, oAuthUserVO, generateToken(request));
    }

    private R loginThird(Auth auth, OAuthUserVO oAuthUserVO, UserToken userToken) {
        User user = userService.loginThird(auth, oAuthUserVO); // 真正的登錄蠕嫁,數(shù)據(jù)校驗锨天,這里使用第三方登錄,Auth是第三方登錄的信息剃毒。
        if (user == null) {
            return R.fail("用戶不存在");
        }

        AuthUser authUser = BeanCopierUtils.copy(user, AuthUser.class);
        // 登錄成功病袄,發(fā)放令牌,寫入緩存
        authManager.onLoginSuccess(authUser, userToken);
        // 通知第三方權(quán)限框架進行認證和授權(quán)
        authManager.login(authUser, userToken);
        LoginVO data = new LoginVO();
        data.setToken(userToken.getToken());
        data.setUser(BeanCopierUtils.copy(user, UserVO.class));

        return R.ok(data);
    }

在上面的Controller代碼中赘阀,登錄成功益缠,服務(wù)端向客戶端返回token和user信息。token是通過AuthManager類的generateToken方法+onLoginSucess來生成的基公。

    // 收集客戶端數(shù)據(jù)
    public UserToken generateToken(HttpServletRequest request) {
        String ip = HttpRequestUtils.getIpAddr(request);
        String uuid = HttpRequestUtils.getParam(request, "uuid");
        String client = HttpRequestUtils.getParam(request, "client");

        UserToken userToken = new UserToken();
        userToken.setClient(client);
        userToken.setIp(ip);
        userToken.setUuid(uuid);
        userToken.setCreatedAt(LocalDateTime.now());

        return userToken;
    }

    public void onLoginSuccess(AuthUser authUser, UserToken userToken) {
        // 刷新用戶Token
        Wrapper<UserToken> wrapper = Wrappers.<UserToken>lambdaQuery().eq(UserToken::getUserId, authUser.getId());
        userTokenService.remove(wrapper);
        userToken.setUserId(authUser.getId());
        // 使用隨機UUID生成一個token
        userToken.setToken(CommonUtils.randomUUID());
        userTokenService.save(userToken);
        // 用戶附帶設(shè)備信息
        authUser.additional(userToken);
        // token寫入緩存
        redisCache.set(userToken.getToken(), authUser, Constants.Second.A_DAY);
    }

這里主要有3個步驟

  1. 收集設(shè)備uuid, ip等信息生成一個UserToken實例幅慌,后續(xù)隨機生成一個字符串作為token令牌
  2. 根據(jù)用戶id查詢用戶登錄表,不管用戶原來是否已經(jīng)存在token轰豆,新的token將替換舊的token
  3. 將token寫入緩存胰伍,因為token是每個請求都會解析齿诞,如果不使用緩存的話,會導致數(shù)據(jù)庫訪問瓶頸骂租。
  4. 用戶的UserToken會附加到AuthUser(作為Shiro的Principal)

客戶端應該保存token祷杈,然后在后續(xù)的訪問中都要帶上這個token(PHP版本中是可選的),后臺配置一個過濾器渗饮,將解析并校驗token的合法性但汞。

解析token

服務(wù)端接收客戶端傳遞的token,需要從中解析出相關(guān)的用戶及設(shè)備信息互站。

    @Override
    public boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response) {
        String token = HttpRequestUtils.getParam(request, Constants.Http.AUTHORIZE_NAME);
        if (!StringUtils.isEmpty(token)) {
            Object o = redisCache.get(token);
            AuthUser loginUser = (AuthUser)getLoginUser();
            UserToken newToken = generateToken(request);
            newToken.setToken(token);
            return verifyToken((AuthUser)o, loginUser, newToken);
        }
        return false;
    }

過程很簡單私蕾,從請求中獲取token,并生成一份新的UserToken胡桃,再取出緩存和shiro的當前登錄用戶信息踩叭,進入token驗證方法。

驗證token

    private boolean loginWithToken(String token) {
        Wrapper<UserToken> wrapper = Wrappers.<UserToken>lambdaQuery().eq(UserToken::getToken, token);
        UserToken userToken = userTokenService.getOne(wrapper);
        if (userToken != null && userToken.isExpired()) {
            log.info("AuthFilter 數(shù)據(jù)庫中Token有效");
            // TODO 判斷UUID标捺,IP是否一致
            User user = userService.getById(userToken.getUserId());

            if (user != null) {
                log.info("AuthFilter 數(shù)據(jù)庫中用戶有效懊纳,重新登錄并且更新緩存及數(shù)據(jù)庫中的token信息");
                userTokenService.updateById(userToken);
                AuthUser authUser = BeanCopierUtils.copy(user, AuthUser.class);
                // 用戶附帶設(shè)備信息
                authUser.additional(userToken);
                // token寫入緩存
                redisCache.set(userToken.getToken(), authUser, Constants.Second.A_DAY);
                // 通知第三方權(quán)限框架進行認證和授權(quán)
                login(authUser, userToken);
                return true;
            }
        }
        return false;
    }

    public boolean verifyToken(AuthUser cacheUser, AuthUser loginUser, UserToken comingToken) {
        if (loginUser == null) {
            log.info("AuthFilter 用戶未登錄,檢查緩存");
            if (cacheUser == null) {
                log.info("AuthFilter 緩存中沒有Token亡容,檢查數(shù)據(jù)庫");
                return loginWithToken(comingToken.getToken());
            } else {
                log.info("AuthFilter 緩存中有Token");

                // TODO 判斷UUID嗤疯,IP是否一致
                login(cacheUser, comingToken);
                return true;
            }
        } else {
            log.info("AuthFilter 用戶已登錄");
            if (loginUser.getToken().equals(comingToken.getToken())) {
                log.info("AuthFilter token相同,通過");
                return true;
            } else {
                log.info("AuthFilter token不一致闺兢,先注銷茂缚,再登錄");
                logout();
                return loginWithToken(comingToken.getToken());
            }
        }
    }

緩存

緩存相對簡單,緩存的key為token值屋谭,也即登錄成功后脚囊,服務(wù)端向客戶端發(fā)送的token值。緩存有效值為1天桐磁,如果token校驗通過悔耘,則應該在更新緩存有效期(上面的代碼中沒有)。

// token寫入緩存
        redisCache.set(userToken.getToken(), authUser, Constants.Second.A_DAY);

備注

以上僅為代碼片斷我擂,核心代碼為我對shiro的二次封裝庫衬以。此庫反向依賴于start模塊的IAuthManager實現(xiàn)。


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

/**
 * @author jamling
 */
public interface IAuthManager {

    /**
     * 登錄校摩,交第三方權(quán)限框架去登錄(認證)
     *
     * @param user        當前用戶
     * @param credentials 憑據(jù)
     */
    void login(IAuthUser user, Object credentials);

    void logout();

    /**
     * 獲取當前登錄的用戶信息
     *
     * @return 用戶信息
     */
    IAuthUser getLoginUser();

    /**
     * 從原始請求中檢查用戶token看峻,由權(quán)限過濾器調(diào)用
     *
     * @param request  http請求
     * @param response http響應
     * @return 檢查結(jié)果 true表示檢查通過
     */
    boolean isAccessAllowed(HttpServletRequest request, HttpServletResponse response);

    boolean verifyToken(String token);

    /**
     * 檢查token不通過,使用何種方式登錄
     *
     * @param request  http請求
     * @param response http響應
     * @return 處理結(jié)果 true表示已處理衙吩,不會再繼續(xù)提交父類處理
     */
    boolean executeLogin(HttpServletRequest request, HttpServletResponse response);

    /**
     * 第三方權(quán)限框架執(zhí)行授權(quán)
     *
     * @param principal 當前用戶
     * @return
     * @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
     */
    IAuthUser doGetAuthorizationInfo(Object principal);

}


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末互妓,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌冯勉,老刑警劉巖澈蚌,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異珠闰,居然都是意外死亡惜浅,警方通過查閱死者的電腦和手機瘫辩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門伏嗜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人伐厌,你說我怎么就攤上這事承绸。” “怎么了挣轨?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵军熏,是天一觀的道長。 經(jīng)常有香客問我卷扮,道長荡澎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任晤锹,我火速辦了婚禮摩幔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鞭铆。我一直安慰自己或衡,他們只是感情好,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布车遂。 她就那樣靜靜地躺著封断,像睡著了一般。 火紅的嫁衣襯著肌膚如雪舶担。 梳的紋絲不亂的頭發(fā)上坡疼,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天,我揣著相機與錄音衣陶,去河邊找鬼柄瑰。 笑死,一個胖子當著我的面吹牛祖搓,可吹牛的內(nèi)容都是我干的狱意。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼拯欧,長吁一口氣:“原來是場噩夢啊……” “哼详囤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤藏姐,失蹤者是張志新(化名)和其女友劉穎隆箩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體羔杨,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡捌臊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了兜材。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片理澎。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖曙寡,靈堂內(nèi)的尸體忽然破棺而出糠爬,到底是詐尸還是另有隱情,我是刑警寧澤举庶,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布执隧,位于F島的核電站,受9級特大地震影響户侥,放射性物質(zhì)發(fā)生泄漏镀琉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一蕊唐、第九天 我趴在偏房一處隱蔽的房頂上張望屋摔。 院中可真熱鬧,春花似錦刃泌、人聲如沸凡壤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽亚侠。三九已至,卻和暖如春俗扇,著一層夾襖步出監(jiān)牢的瞬間硝烂,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工铜幽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留滞谢,地道東北人。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓除抛,卻偏偏與公主長得像狮杨,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子到忽,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

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