Микро-svc
Spring cloud + shiro фреймворк; Блог: Пошаговое руководство по интеграции spring cloud и shiro микросервисной архитектуры.
Предположим, у нас есть несколько проектов на Java, для аутентификации и авторизации используется фреймворк shiro. Возможно, также существует единая платформа SSO (Single Sign-On) для входа в систему.
Внезапно ваш руководитель проекта говорит, что нужно сделать микросервисы.
Вы предлагаете ему несколько вариантов, таких как Dubbo, Spring Cloud и другие. Обсуждаете все аспекты этой задачи.
Но! Руководитель проекта говорит: «Маленький Мин, ты можешь поработать сверхурочно сегодня вечером и немного изменить существующий проект. У нас не так много работы, чтобы преобразовать текущий проект в микросервисную архитектуру. Кроме того, изменения в проекте не должны влиять на текущий план разработки». В этот момент ваше сердце начинает биться быстрее.
В общем, цель состоит в том, чтобы с минимальными усилиями преобразовать существующий проект, основанный на безопасной структуре shiro, в микросервис, используя интеграцию Spring Cloud и Shiro.
PS: Текущая реализация, описанная в блоге, является решением, разработанным и реализованным автором на основе реальных условий компании. Некоторые решения могут быть не совсем оптимальными или не соответствовать вашим требованиям, но это решение всё ещё может быть полезным. Если вы считаете его полезным, пожалуйста, оставьте комментарий или лайк, чтобы поддержать автора.
Версия: spring boot 2.1.5.RELEASE
, spring cloud Greenwich.SR2
, jdk1.8+
, postgresql-10
, redis-2.8.17
.
Простой реестр, без особых настроек. Класс запуска 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 реализует 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 )