基于Spring及Redis的Token鑒權(quán)

為什么用 Token

一般來說都是用 session 來存儲(chǔ)登錄信息的,但是移動(dòng)端使用 session 不太方便,所以一般都用 token 。另外現(xiàn)在前后端分離,一般都用 token 來鑒權(quán)。用 token 也更加符合 RESTful 中無狀態(tài)的定義。

交互流程

  1. 客戶端通過登錄請(qǐng)求提交用戶名和密碼,服務(wù)端驗(yàn)證通過后生成一個(gè) Token 與該用戶進(jìn)行關(guān)聯(lián),并將 Token 返回給客戶端。
  2. 客戶端在接下來的請(qǐng)求中都會(huì)攜帶 Token,服務(wù)端通過解析 Token 檢查登錄狀態(tài)。
  3. 當(dāng)用戶退出登錄、其他終端登錄同一賬號(hào)(被頂號(hào))、長時(shí)間未進(jìn)行操作時(shí) Token 會(huì)失效,這時(shí)用戶需要重新登錄。

程序示例

Token的生成算法

服務(wù)端生成的 Token 一般為隨機(jī)的非重復(fù)字符串,根據(jù)應(yīng)用對(duì)安全性的不同要求,會(huì)將其添加時(shí)間戳(通過時(shí)間判斷 Token 是否被盜用)或 url 簽名(通過請(qǐng)求地址判斷 Token 是否被盜用)后加密進(jìn)行傳輸。因?yàn)橹皇莻€(gè) demo,所以這里簡單寫了

public class TokenUtil {

    private static final String SEPARATOR = "-";

    /**
     * Token格式:時(shí)間戳-userId-隨機(jī)字符串
     */
    public static String createToken(long userId) {
        return new Date().getTime() + SEPARATOR + userId + SEPARATOR + RandomStringUtils.random(10, true, true);
    }

    /**
     * 解析Token,從中取得userId
     */
    public static Long getUserIdFromToken(String token) {
        if (StringUtils.isEmpty(token)) {
            return null;
        }
        String[] param = token.split(SEPARATOR);
        if (param.length != 3) {
            return null;
        }
        try {
            return NumberUtils.createLong(param[1]);
        } catch (NumberFormatException e) {
            return null;
        }
    }
}

Token的CRUD操作

Redis 是一個(gè) Key-Value 結(jié)構(gòu)的內(nèi)存數(shù)據(jù)庫,用它維護(hù) userId 和 Token 的映射表會(huì)比傳統(tǒng)數(shù)據(jù)庫速度更快,這里使用 Spring-Data-Redis 封裝的 RedisTokenManager 對(duì) Token 進(jìn)行基礎(chǔ)操作:

首先定義一個(gè) DAO 接口

package com.owen.favorite.repository;

import com.owen.favorite.domain.Token;

public interface TokenRepository {

    /**
     * 創(chuàng)建一個(gè) token 并關(guān)聯(lián)上指定用戶
     */
    Token createToken(long userId);

    /**
     *  檢查 token 是否有效
     */
    boolean checkToken(Token token);

    /**
     * 清除 token
     */
    void deleteToken (long userId);
}

然后是實(shí)現(xiàn)類

package com.owen.favorite.repository.impl;

import com.owen.favorite.constant.ApiConstant;
import com.owen.favorite.domain.Token;
import com.owen.favorite.repository.TokenDao;
import com.owen.favorite.util.TokenUtil;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.concurrent.TimeUnit;

/**
 * 通過 Redis 管理 token 的實(shí)現(xiàn)類
 */
@Repository
public class RedisTokenDaoImpl implements TokenDao {

    private RedisTemplate<Long, String> redisTemplate;

    @Override
    public Token createToken(long userId) {
        String token = TokenUtil.createToken(userId);
        // 存儲(chǔ)到 redis 并設(shè)置過期時(shí)間
        redisTemplate.boundValueOps(userId).set(token, ApiConstant.Token.EXPIRE_DAYS, TimeUnit.DAYS);
        return new Token(userId, token);
    }

    @Override
    public boolean checkToken(String tokenFromClient) {
        if (StringUtils.isEmpty(tokenFromClient)) {
            return false;
        }
        Long userId = TokenUtil.getUserIdFromToken(tokenFromClient);
        if (userId == null) {
            return false;
        }
        String tokenInRedis = redisTemplate.boundValueOps(userId).get();
        if (tokenFromClient.equals(tokenInRedis)) {
            // 如果驗(yàn)證成功,說明此用戶進(jìn)行了一次有效操作,延長 token 的過期時(shí)間
            redisTemplate.boundValueOps(userId).expire(ApiConstant.Token.EXPIRE_DAYS, TimeUnit.DAYS);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void deleteToken(long userId) {
        redisTemplate.delete(userId);
    }

    @Autowired
    public void setRedisTemplate(RedisTemplate<Long, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

登錄與注冊(cè)

登錄與注冊(cè)的 Controller

package com.owen.favorite.controller;

import com.owen.favorite.domain.APIResult;
import com.owen.favorite.domain.Token;
import com.owen.favorite.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @Autowired
    private TokenService tokenService;

    @PostMapping("/login")
    public APIResult login(@RequestParam String username, @RequestParam String password) {
        User user = userService.findByUsername(username);
        if (user == null /* 未注冊(cè) */ || !user.getPassword().equals(password) /* 密碼錯(cuò)誤 */) {
            return APIResult.createNg("用戶名或密碼錯(cuò)誤");
        }
        //生成一個(gè)token,保存用戶登錄狀態(tài)
        Token token = tokenService.createToken(user.getId());
        return APIResult.createOk(token);
    }

    @PostMapping("/logout")
    public APIResult logout(@RequestParam String token) {
        Long userId = TokenUtil.getUserIdFromToken(token);
        if (userId == null) {
            return APIResult.createNg("退出失敗");
        }
        tokenService.deleteToken(userId);
        return APIResult.createOKMessage("退出成功");
    }
}

token驗(yàn)證

客戶端訪問一些需要用戶登錄之后才能調(diào)用的接口,比如在數(shù)據(jù)庫中插入一條記錄,那么就需要判斷 token 的合法性。而這樣的接口又有很多,那么豈不是每一次都需要及你想那個(gè)判斷,代碼要重復(fù)寫很多遍。這時(shí)候可以使用自定義注解和攔截器來實(shí)現(xiàn)。

首先定義一個(gè)注解

package com.owen.favorite.anno;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 在 Controller 的方法上使用此注解,該方法在映射時(shí)會(huì)檢查用戶是否登錄,未登錄返回 401 錯(cuò)誤
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {
}

攔截器的實(shí)現(xiàn)

package com.owen.favorite.interceptor;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.owen.favorite.anno.Authorization;
import com.owen.favorite.constant.ApiConstant;
import com.owen.favorite.domain.APIResult;
import com.owen.favorite.service.TokenService;
import com.owen.favorite.util.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * 自定義攔截器,判斷此次請(qǐng)求的用戶是否已登錄
 */
@Component
public class AuthorizationInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private TokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            // 如果不是映射到方法直接通過
            return true;
        }
        //從header中得到token
        String token = request.getHeader(ApiConstant.RequestParam.TOKEN);
        // 驗(yàn)證 token
        if (tokenService.checkToken(token)) {
            //如果token驗(yàn)證成功,將token對(duì)應(yīng)的用戶id存在request中,便于之后注入
            request.setAttribute(ApiConstant.RequestParam.USER_ID, TokenUtil.getUserIdFromToken(token));
            return true;
        } else {
            // 如果驗(yàn)證token失敗,并且方法注明了Authorization,就告訴客戶端token不對(duì)
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            if (handlerMethod.getMethodAnnotation(Authorization.class) != null) {
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json;charset=utf-8");

                ObjectMapper objectMapper = new ObjectMapper();
                objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

                PrintWriter writer = response.getWriter();
                writer.write(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(APIResult.createNg("請(qǐng)登錄")));
                writer.close();
                return false;
            } else {
                return true;
            }
        }
    }
}

一些細(xì)節(jié)

  • 登錄請(qǐng)求一定要使用 HTTPS,否則無論 Token 做的安全性多好密碼泄露了也是白搭
  • Token 的生成方式有很多種,例如比較熱門的有 JWT(JSON Web Tokens)、OAuth 等。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容