1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/admin_yu-micro-svc

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
README.md 23 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
gitlife-traslator Отправлено 02.12.2024 06:08 3491e78

Микро-svc

Введение

Spring cloud + shiro фреймворк; Блог: Пошаговое руководство по интеграции spring cloud и shiro микросервисной архитектуры.

Фон

Предположим, у нас есть несколько проектов на Java, для аутентификации и авторизации используется фреймворк shiro. Возможно, также существует единая платформа SSO (Single Sign-On) для входа в систему.

Внезапно ваш руководитель проекта говорит, что нужно сделать микросервисы.

Вы предлагаете ему несколько вариантов, таких как Dubbo, Spring Cloud и другие. Обсуждаете все аспекты этой задачи.

Но! Руководитель проекта говорит: «Маленький Мин, ты можешь поработать сверхурочно сегодня вечером и немного изменить существующий проект. У нас не так много работы, чтобы преобразовать текущий проект в микросервисную архитектуру. Кроме того, изменения в проекте не должны влиять на текущий план разработки». В этот момент ваше сердце начинает биться быстрее.

Цель

В общем, цель состоит в том, чтобы с минимальными усилиями преобразовать существующий проект, основанный на безопасной структуре shiro, в микросервис, используя интеграцию Spring Cloud и Shiro.

PS: Текущая реализация, описанная в блоге, является решением, разработанным и реализованным автором на основе реальных условий компании. Некоторые решения могут быть не совсем оптимальными или не соответствовать вашим требованиям, но это решение всё ещё может быть полезным. Если вы считаете его полезным, пожалуйста, оставьте комментарий или лайк, чтобы поддержать автора.

План реализации

Общий план реализации:

  • Zuul шлюз, который служит точкой входа и выхода для доступа к одной и той же системе;
  • Реализация фильтра в Zuul для фильтрации всех запросов и проверки состояния входа и разрешений; аутентификация: проверка, вошёл ли пользователь в систему; авторизация: проверка наличия у пользователя необходимых разрешений.
  • Сервис-auth, который реализует функции аутентификации и авторизации. Поскольку все запросы проходят через этот сервис, его функциональность не должна быть слишком сложной. Необходимо обеспечить высокую доступность этого модуля. Этот сервис использует Feign для разработки интерфейсов и предоставления их Zuul.
  • Интеграция Shiro-Redis в сервис-auth. Другие модули сервисов могут не нуждаться в интеграции Shiro, но если это необходимо, то это возможно, хотя и требует решения проблемы совместного использования сеансов Shiro.

Процесс аутентификации и авторизации

  • На шлюзе Zuul настройте поддержку HTTPS-запросов, чтобы все сервисы могли одновременно поддерживать HTTP и HTTPS запросы.
  • Предпочтение отдаётся получению идентификатора сеанса из cookie, при этом также поддерживается проверка токена, поскольку при входе в систему через публичный аккаунт требуется перенаправление на бэкэнд-интерфейс входа, поэтому необходимо предоставить токен для идентификации клиента.
  • Авторизация осуществляется через URL. Уникальность определяется на основе имени сервиса, пути запроса и метода запроса. Также можно использовать requirePermissions, в зависимости от ваших потребностей.

Реализация плана

Версия: spring boot 2.1.5.RELEASE, spring cloud Greenwich.SR2, jdk1.8+, postgresql-10, redis-2.8.17.

Реестр Eureka

Простой реестр, без особых настроек. Класс запуска Application.java:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Конфигурация application.yml:

server:
  port: 7001
spring:
  application:
    name: eureka
  main:
    allow-bean-definition-overriding: true
eureka:
  instance:
    prefer-ip-address: true
    #hostname: svc-eureka #eureka服务端的实例名称
    instance-id: ${spring.cloud.client.ip-address}:${server.port}
  server:
    enable-self-preservation: false ## 中小规模下,自我保护模式坑比好处多,所以关闭它
    #renewal-threshold-update-interval-ms: 120000  ## 心跳阈值计算周期,如果开启自我保护模式,可以改一下这个配置
    eviction-interval-timer-in-ms: 5000 ## 主动失效检测间隔,配置成5秒
    use-read-only-response-cache: false ## 禁用readOnlyCacheMap
  client:
    register-with-eureka: false     #false表示不向注册中心注册自己。
    fetch-registry: false     #false表示自己端就是注册中心,我的职责就是维护服务实例,并不需要去检索服务
    service-url:
      defaultZone: http://${spring.cloud.client.ip-address}:${server.port}/eureka/
info:
  app.name: eureka
  company.name: test.com
  build.artifactId: "@project.artifactId@"
  build.version: "@project.version@"

Полный код см. в приложении.

Шлюз Zuul

Zuul реализует AuthFilter для фильтрации всех запросов, проверки входа и разрешения; поддерживает настройку адресов без аутентификации, адресов без авторизации, совместимости с проверкой cookie и токена и т. д. Поддерживает веб-аутентификацию, небольшие программы и аутентификацию публичных аккаунтов. Класс запуска Application.java:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableFeignClients
@EnableEurekaClient
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Ключевой код AuthFilter.java:


import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import

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

org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.alibaba.fastjson.JSONObject;
import com.fundway.auth.api.LoginCheckApi;
import com.google.common.collect.Maps;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

/**
* Пользовательский фильтр добавляет в запросы к нижестоящим сервисам информацию для аутентификации.
* 
* Вместе с чувствительными заголовками (которые определяют, какие заголовки не должны передаваться внутренним сервисам),
* этот способ, похоже, не может передавать имена Authorization, Cookie и Set-Cookie в заголовках запросов, которые не передаются нижестоящему сервису. Эти три заголовка управляются чувствительными заголовками и могут передавать только токены пользовательских заголовков.
*/
@Component
public class AuthFilter extends ZuulFilter {

   @Autowired(required=true)
   private LoginCheckApi loginCheckApi;

   // Белый список путей запроса, по которым не требуется проверка подлинности, задаётся в конфигурации application-url.
   private static Set<String> urlSet;
   // Белый список типов ресурсов запроса, по которым не требуется проверка подлинности, задаётся в конфигурации application-url.
   private static Set<String> fileSet;

   @Override
   public String filterType() {
       //pre-тип фильтра, который выполняется перед маршрутизацией к нижестоящей службе.
       return FilterConstants.PRE_TYPE;
   }

   @Override
   public int filterOrder() {
       // Приоритет, чем больше число, тем ниже приоритет.
       return 0;
   }

   @Override
   public boolean shouldFilter() {
       // Определяет, должен ли выполняться данный фильтр. true означает необходимость фильтрации.
       return true;
   }

   /**
    * Логика фильтрации.
    * Фильтр pre выполняется до маршрутизации route, RequestContext отвечает за связь, которая содержит информацию о запросе, отладке, context.addZuulRequestHeader.
    * Однако в RibbonRoutingFilter, фильтре маршрутизации к нижестоящему сервису, пользовательский заголовок не добавляется.
    * RibbonRoutingFilter — это стандартный фильтр, метод run которого можно увидеть. Логика заключается в создании нового RibbonCommandContext для отправки запроса на основе исходного RequestContext.
    */
   @Override
   public Object run() {

       // Zull использует RequestContext для передачи связи между фильтрами, внутренне используя ThreadLocal для хранения информации о каждом запросе, включая маршрут запроса, информацию об ошибках, HttpServletRequest и ответ.
       RequestContext ctx = RequestContext.getCurrentContext();
       HttpServletRequest request = this.getHttpServletRequest();

       // Вариант запроса, пропустить напрямую.
       if (request.getMethod().equals(RequestMethod.OPTIONS.name())) {
           return null;
       }

       // Определить, нужно ли пропускать URL или статические ресурсы.
       String url = request.getRequestURI();
       String end  = "";
       if(url.lastIndexOf("/") >= 0 ) {    // Определить, нужен ли пропуск запроса.
           end  = url.substring(url.lastIndexOf("/"));
           if(urlSet.contains(end)) {
               return null;
           }
       }
       if(end.lastIndexOf(".") > 0) {  //Определить, нужно ли пропустить статический файл.
           end  = end.substring(end.lastIndexOf(".") + 1);
           if(fileSet.contains(end)) {
               return null;
           }
       }

       // Получить токен пользователя.
       String cookie = request.getHeader("Cookie");    //Получить значение JSESSIONID.
       if(StringUtils.isEmpty(cookie)) {
           cookie = "";
       }

       String token = ctx.getRequest().getParameter("token");  //Получить значение.

       // Обработка входа в систему через публичный аккаунт WeChat, последующий внутренний вызов приведёт к перенаправлению, создающему недействительный файл cookie, а последующее внутреннее перенаправление также не сможет записать действительный файл cookie клиенту.
       if(!StringUtils.isEmpty(token) && !"undefined".equals(token) && !cookie.contains(token)) {
           cookie = "JSESSIONID=" + (ctx.getRequest().getParameter("token"));
       }
       if(StringUtils.isEmpty(token)) {    // Параметр пуст или равен нулю, feign вызовет интерфейс и сообщит об ошибке! яма
           token = "";
       }

       // Отфильтровать этот запрос, не пересылать его нижестоящей службе, завершить здесь.
       if(StringUtils.isEmpty(cookie)) { // Будет сообщено о междоменной проблеме.
           this.setCORS(ctx);
           ctx.setSendZuulResponse(false);  
           ctx.setResponseStatusCode(200);  
           Map<String, Object> result = Maps.newHashMap();
           result.put("code", 401);
           result.put("msg", "Не вошёл в систему");
           result.put("obj", "Сообщение от шлюза: не удалось получить действительный токен");
           result.put("success", false);
           ctx.setResponseBody(JSONObject.toJSONString(result));
           ctx.getResponse().setContentType("text/html;charset=UTF-8");
           return null;
       }

       // Добавить заголовок запроса.
       ctx.addZuulRequestHeader("Cookie", cookie);

       // Вызов унифицированного интерфейса аутентификации для определения того, вошёл ли пользователь в систему и имеет ли он функциональные разрешения.
       // Предпочтение отдаётся проверке файла cookie, если она не проходит, то проверяется токен. // Файл cookie берётся из запроса.
       Object check = loginCheckApi.checkPermission(token, this.getUrl(request));

       if(check instanceof HashMap) { **Вот перевод текста на русский язык:**

HashMap<String, Object> result = (HashMap) check;
if(Boolean.parseBoolean(result.get("success").toString())) {
   // Добавление сериализованной пользовательской информации
   // Запрос белого списка URL, невозможно получить эту информацию
   setReqParams(ctx, request, "userEntity", result.get("obj").toString());
   return null;
}
this.setCORS(ctx);
ctx.setSendZuulResponse(false);  
ctx.setResponseStatusCode(200);  
// Исключение интерфейса проверки прав доступа
ctx.setResponseBody(JSONObject.toJSONString(check));
ctx.getResponse().setContentType("text/html;charset=UTF-8");
return null;
} else {
this.setCORS(ctx);
ctx.setSendZuulResponse(false);  
ctx.setResponseStatusCode(200);  
Map<String, Object> result = Maps.newHashMap();
result.put("code", 401);
result.put("msg", "Нет прав доступа");
result.put("obj", "Сообщение от шлюза: у текущего пользователя нет разрешения на запрос");
result.put("success", false);
ctx.setResponseBody(JSONObject.toJSONString(result));
ctx.getResponse().setContentType("text/html;charset=UTF-8");
return null;
}
}

private String getUrl(HttpServletRequest request) {
// Получение связанных данных запроса, uri начинается с косой черты
String uri = request.getRequestURI().toLowerCase().replaceAll("//", "/");
String method = request.getMethod().toLowerCase();
return method.concat(uri);
}

private HttpServletRequest getHttpServletRequest() {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
return request;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static void setReqParams(RequestContext ctx, HttpServletRequest request, String key, String value) {
// Обязательно получить, следующая строка кода может получить значение... [Примечание 1]
request.getParameterMap();
Map<String, List<String>> requestQueryParams = ctx.getRequestQueryParams();
if (requestQueryParams==null) {
requestQueryParams=new HashMap<>();
}

// Добавить новый параметр, вызываемый микросервис может напрямую взять его, как обычно, фреймворк будет напрямую внедрять его
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add(value);
requestQueryParams.put(key, arrayList);
ctx.setRequestQueryParams(requestQueryParams);
}

private void setCORS(RequestContext ctx) {
// Обработка междоменных проблем
HttpServletRequest request = ctx.getRequest();
HttpServletResponse response = ctx.getResponse();

// Эти соответствуют заголовкам запросов, в Интернете есть много объяснений
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Access-Control-Allow-Methods","GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH");
response.setHeader("Access-control-allow-headers","authorization, content-type");
response.setHeader("Access-Control-Expose-Headers","X-forwared-port, X-forwarded-host");
response.setHeader("Vary","Origin,Access-Control-Request-Method,Access-Control-Request-Headers");
}

@Value("${whitelist.urlset}")
public void setUtlSet(Set<String> urlSet) {
this.urlSet = urlSet;
}

@Value("${whitelist.fileset}")
public void setFileSet(Set<String> fileSet) {
this.fileSet = fileSet;
} Получение микросервисного имени

String[] str = checkUrl.split("/");  
String module = str[1];  

// Решение, является ли ресурс непроверенным  
if (this.getIdByUrl(unCheckResMap.get(module), checkUrl) > 0) {  
   return Result.ok(JSONObject.toJSONString(user));  
}  

// Пользователь не имеет полномочий, и запрос ресурса не является открытым ресурсом  
if (null == permissionSet || permissionSet.size() <= 0) {  
   log.info("Текущий пользователь не получил разрешения: " + user.getLoginName());  
   return Result.error("Нет разрешения", 401);  
}  

// Получение системного ресурса модуля  
Integer resId = this.getIdByUrl(resMap.get(module), checkUrl);  

// Система не настроила это разрешение, или путь запроса не существует  
if (resId <= 0 && isPass) {  
   // log.info("Система не настроила разрешение для этого ресурса, но разрешила проход: " + uri);  
   return Result.ok(JSONObject.toJSONString(user));  
}  

// Система настроила разрешение  
if (permissionSet.contains(resId)) {  
   return Result.ok(JSONObject.toJSONString(user));  
}  

return Result.error("Нет разрешения", 401);  
}

public Integer getIdByUrl(HashMap<String, Integer> value, String url) {  
Integer result = 0;  
if (null != value && value.size() > 0) {  
Set<Integer> resultSet = Sets.newHashSet();  

if (value.containsKey(url)) {  
result = value.get(url);  
} else {  
// Проходить через, сопоставлять, обрабатывать @PathVariable аннотированный запрос  
value.entrySet().forEach(entry -> {  
String key1 = entry.getKey();  
if (key1.contains("{")) {  
AntPathMatcher matcher = new AntPathMatcher();  
if (matcher.match(key1, url)) {  
resultSet.add(entry.getValue());  
}  
}  
});  
}  
if (resultSet.size() > 0) {  
result = resultSet.stream().findFirst().get();  
}  
}  
return result;  
}

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/admin_yu-micro-svc.git
git@api.gitlife.ru:oschina-mirror/admin_yu-micro-svc.git
oschina-mirror
admin_yu-micro-svc
admin_yu-micro-svc
master