В последние два дня я писал глобальную проверку разрешений для проекта, используя Zuul в качестве сервисного шлюза и выполняя проверку в предварительном фильтре Zuul.
Токен должен быть упомянут для проверки разрешений или аутентификации.В настоящее время существует много способов проверить токен.Некоторые генерируют токен и хранят токен в Redis или базе данных, а многие используют JWT (веб-токен JSON).
Честно говоря, у меня не так много опыта в этой области, и я тороплюсь угнаться за проектом, поэтому сначала использую простое решение.
После успешного входа в систему токен возвращается во внешний интерфейс и сохраняется в Redis. Каждый раз, когда запрашивается интерфейс, токен извлекается из файла cookie или заголовка, а сохраненный токен извлекается из Redis для проверки его согласованности.
Я знаю, что это решение не самое идеальное, есть проблемы с безопасностью и его легко взломать. Тем не менее, текущая стратегия заключается в том, чтобы сначала завершить функции проекта, а затем постепенно оптимизировать их после запуска, не вычитая слишком много деталей о функциональной точке, чтобы гарантировать, что ход проекта не будет слишком медленным.
адрес проекта:GitHub.com/cache Cats/ из…
Эта статья будет разделена на четыре части
- Логика входа
- Логика проверки предварительной фильтрации AuthFilter
- Инструменты
- Проверка демонстрации
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/ из…
Ваша поддержка - самая большая движущая сила для меня, чтобы двигаться вперед, добро пожаловать, чтобы поставить лайк, добро пожаловать, чтобы отправить маленькие звездочки ✨ ~