Сервисный шлюз Zuul и Redis объединяются для реализации проверки разрешений токена.

Redis задняя часть Spring

В последние два дня я писал глобальную проверку разрешений для проекта, используя Zuul в качестве сервисного шлюза и выполняя проверку в предварительном фильтре Zuul.

Токен должен быть упомянут для проверки разрешений или аутентификации.В настоящее время существует много способов проверить токен.Некоторые генерируют токен и хранят токен в Redis или базе данных, а многие используют JWT (веб-токен JSON).

Честно говоря, у меня не так много опыта в этой области, и я тороплюсь угнаться за проектом, поэтому сначала использую простое решение.

После успешного входа в систему токен возвращается во внешний интерфейс и сохраняется в Redis. Каждый раз, когда запрашивается интерфейс, токен извлекается из файла cookie или заголовка, а сохраненный токен извлекается из Redis для проверки его согласованности.

Я знаю, что это решение не самое идеальное, есть проблемы с безопасностью и его легко взломать. Тем не менее, текущая стратегия заключается в том, чтобы сначала завершить функции проекта, а затем постепенно оптимизировать их после запуска, не вычитая слишком много деталей о функциональной точке, чтобы гарантировать, что ход проекта не будет слишком медленным.

адрес проекта:GitHub.com/cache Cats/ из…

Эта статья будет разделена на четыре части

  1. Логика входа
  2. Логика проверки предварительной фильтрации AuthFilter
  3. Инструменты
  4. Проверка демонстрации

1. Логика входа

После успешного входа сохраните сгенерированный токен в Redis. Хранится в ключе, формат значения типа String, ключTOKEN_userId, если userId пользователя222222, ключTOKEN_222222; значение — сгенерированный токен.

Публикуйте только логин Serive code

@Override
public UserInfoDTO loginByEmail(String email, String password) {

    if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {
        throw new UserException(ResultEnum.EMAIL_PASSWORD_EMPTY);
    }

    UserInfo user = userRepository.findUserInfoByEmail(email);
    if (user == null) {
        throw new UserException(ResultEnum.EMAIL_NOT_EXIST);
    }
    if (!user.getPassword().equals(password)) {
        throw new UserException(ResultEnum.PASSWORD_ERROR);
    }

    //生成 token 并保存在 Redis 中
    String token = KeyUtils.genUniqueKey();
    //将token存储在 Redis 中。键是 TOKEN_用户id, 值是token
    redisUtils.setString(String.format(RedisConsts.TOKEN_TEMPLATE, user.getId()), token, 2l, TimeUnit.HOURS);

    UserInfoDTO dto = new UserInfoDTO();
    BeanUtils.copyProperties(user, dto);
    dto.setToken(token);

    return dto;
}

2. Предварительный фильтр AuthFilter

AuthFilterунаследовано отZuulFilter, должны быть реализованыZuulFilterиз четырех методов.

filterType(): тип фильтра, возвращаемый предварительным фильтром.PRE_TYPE

filterOrder(): Порядок фильтров, чем меньше значение, тем первым выполняется. Надпись здесьPRE_DECORATION_FILTER_ORDER - 1, который также является официально рекомендуемым способом написания.

shouldFilter(): нужно ли его фильтровать. Верните true, чтобы фильтровать, и false, чтобы не фильтровать. В этом методе вы можете определить, какие интерфейсы не нужно фильтровать.В этом примере исключаются интерфейсы регистрации и входа.Кроме этих двух интерфейсов, все остальные интерфейсы необходимо фильтровать.

run(): специфическая логика фильтра

Чтобы упростить внешний интерфейс, учитывая необходимость предоставления услуг на разных платформах, таких как ПК, приложение и апплет, токен может быть установлен либо в файле cookie, либо в заголовке. Сначала он будет взят из файла cookie, а затем cookie не будет взят из заголовка.

package com.solo.coderiver.gateway.filter;

import com.google.gson.Gson;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import com.solo.coderiver.gateway.VO.ResultVO;
import com.solo.coderiver.gateway.consts.RedisConsts;
import com.solo.coderiver.gateway.utils.CookieUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

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

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;

/**
 * 权限验证 Filter
 * 注册和登录接口不过滤
 *
 * 验证权限需要前端在 Cookie 或 Header 中(二选一即可)设置用户的 userId 和 token
 * 因为 token 是存在 Redis 中的,Redis 的键由 userId 构成,值是 token
 * 在两个地方都没有找打 userId 或 token其中之一,就会返回 401 无权限,并给与文字提示
 */
@Slf4j
@Component
public class AuthFilter extends ZuulFilter {

    @Autowired
    StringRedisTemplate stringRedisTemplate;

    //排除过滤的 uri 地址
    private static final String LOGIN_URI = "/user/user/login";
    private static final String REGISTER_URI = "/user/user/register";

    //无权限时的提示语
    private static final String INVALID_TOKEN = "invalid token";
    private static final String INVALID_USERID = "invalid userId";

    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        log.info("uri:{}", request.getRequestURI());
        //注册和登录接口不拦截,其他接口都要拦截校验 token
        if (LOGIN_URI.equals(request.getRequestURI()) ||
                REGISTER_URI.equals(request.getRequestURI())) {
            return false;
        }
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        //先从 cookie 中取 token,cookie 中取失败再从 header 中取,两重校验
        //通过工具类从 Cookie 中取出 token
        Cookie tokenCookie = CookieUtils.getCookieByName(request, "token");
        if (tokenCookie == null || StringUtils.isEmpty(tokenCookie.getValue())) {
            readTokenFromHeader(requestContext, request);
        } else {
            verifyToken(requestContext, request, tokenCookie.getValue());
        }

        return null;
    }

    /**
     * 从 header 中读取 token 并校验
     */
    private void readTokenFromHeader(RequestContext requestContext, HttpServletRequest request) {
        //从 header 中读取
        String headerToken = request.getHeader("token");
        if (StringUtils.isEmpty(headerToken)) {
            setUnauthorizedResponse(requestContext, INVALID_TOKEN);
        } else {
            verifyToken(requestContext, request, headerToken);
        }
    }

    /**
     * 从Redis中校验token
     */
    private void verifyToken(RequestContext requestContext, HttpServletRequest request, String token) {
        //需要从cookie或header 中取出 userId 来校验 token 的有效性,因为每个用户对应一个token,在Redis中是以 TOKEN_userId 为键的
        Cookie userIdCookie = CookieUtils.getCookieByName(request, "userId");
        if (userIdCookie == null || StringUtils.isEmpty(userIdCookie.getValue())) {
            //从header中取userId
            String userId = request.getHeader("userId");
            if (StringUtils.isEmpty(userId)) {
                setUnauthorizedResponse(requestContext, INVALID_USERID);
            } else {
                String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userId));
                if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
                    setUnauthorizedResponse(requestContext, INVALID_TOKEN);
                }
            }
        } else {
            String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userIdCookie.getValue()));
            if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
                setUnauthorizedResponse(requestContext, INVALID_TOKEN);
            }
        }
    }


    /**
     * 设置 401 无权限状态
     */
    private void setUnauthorizedResponse(RequestContext requestContext, String msg) {
        requestContext.setSendZuulResponse(false);
        requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());

        ResultVO vo = new ResultVO();
        vo.setCode(401);
        vo.setMsg(msg);
        Gson gson = new Gson();
        String result = gson.toJson(vo);

        requestContext.setResponseBody(result);
    }
}

3. Инструменты

Инструменты MD5

package com.solo.coderiver.user.utils;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 生成 MD5 的工具类
 */
public class MD5Utils {

    public static String getMd5(String plainText) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(plainText.getBytes());
            byte b[] = md.digest();

            int i;

            StringBuffer buf = new StringBuffer("");
            for (int offset = 0; offset < b.length; offset++) {
                i = b[offset];
                if (i < 0)
                    i += 256;
                if (i < 16)
                    buf.append("0");
                buf.append(Integer.toHexString(i));
            }
            //32位加密
            return buf.toString();
            // 16位的加密
            //return buf.toString().substring(8, 24);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 加密解密算法 执行一次加密,两次解密
     */
    public static String convertMD5(String inStr){

        char[] a = inStr.toCharArray();
        for (int i = 0; i < a.length; i++){
            a[i] = (char) (a[i] ^ 't');
        }
        String s = new String(a);
        return s;

    }
}

Класс инструмента для генерации клавиш

package com.solo.coderiver.user.utils;

import java.util.Random;

public class KeyUtils {

    /**
     * 产生独一无二的key
     */
    public static synchronized String genUniqueKey(){
        Random random = new Random();
        int number = random.nextInt(900000) + 100000;
        String key = System.currentTimeMillis() + String.valueOf(number);
        return MD5Utils.getMd5(key);
    }
}

4. Демонстрационная проверка

Начните с порта 8084api_gatewayпроект при запускеuserпроект.

Используйте postman для доступа к интерфейсу входа через шлюз. Поскольку фильтр исключает интерфейсы входа и регистрации, токены этих двух интерфейсов не будут проверены.

Как видите, адрес доступаhttp://localhost:8084/user/user/loginУспешный вход и информация о пользователе и возврат токена.

На этом этапе токен должен храниться в Redis, а идентификатор пользователя111111, так что ключTOKEN_111111, значение представляет собой только что сгенерированное значение токена

Затем, если вы хотите запросить другой интерфейс, вы должны пройти через фильтр.

Токен и userId не передаются в заголовке и возвращается 401

Пройдите только токен, но не userid, вернуть 401 и подскажите неверный usid

Передаются и токен, и идентификатор пользователя, но токен неверный, возвращается 401 и отображается недопустимый токен

Передайте правильный токен и идентификатор пользователя одновременно, запрос выполнен успешно

Выше приведена простая проверка токена. Если у вас есть лучшее решение, сообщите об этом в комментариях.


Код из проекта с открытым исходным кодомCodeRiver, стремится создать полноплатформенный бутик-проект с открытым исходным кодом.

coderiver Китайское название 河CODE — это платформа, на которой программисты и дизайнеры могут совместно работать над проектами. Независимо от того, являетесь ли вы разработчиком интерфейса, бэкенда или мобильного приложения, дизайнером или менеджером по продукту, вы можете публиковать проекты на платформе и сотрудничать с партнерами-единомышленниками для завершения проектов.

Coderiver River Code похож на гостиницу программиста, но его основная цель — способствовать техническому обмену между талантами в различных подобластях, расти вместе и совместно выполнять проекты. Никаких денежных операций не происходит.

Планируется, что это будет полноплатформенный проект с полным стеком, включая терминал для ПК (Vue, React), мобильный H5 (Vue, React), гибридную разработку ReactNative, родной Android, апплет WeChat и бэкэнд java. Добро пожаловать, обратите внимание.

адрес проекта:GitHub.com/cache Cats/ из…


Ваша поддержка - самая большая движущая сила для меня, чтобы двигаться вперед, добро пожаловать, чтобы поставить лайк, добро пожаловать, чтобы отправить маленькие звездочки ✨ ~