1. Предисловие
В традиционном проекте SpringBoot превосходными фреймворками с открытым исходным кодом для аутентификации и аутентификации пользователей являются SpringSecurity и Shiro.В Интернете уже есть множество руководств и справочных материалов для изучения и использования.В распределенном проекте SpringCloud две основные технологии шлюза являются Zuul1 .x и Gateway, основное различие между ними заключается в том, что Gateway реализован на основе WebFlux Spring 5.x, в то время как нижний уровень WebFlux использует Netty для высокопроизводительного взаимодействия в режиме Reactor.С другой стороны, Zuul1. x основан на сервлете 3.1 Блокировка ввода-вывода.
Простое сравнение состоит в том, что Gateway использует NIO, а Zuul1.x использует BIO.Основываясь на принципе «учить старое хуже, чем изучать новое», автор выбрал Gateway в качестве шлюза при попытке построить архитектуру системы RPC и был отвечает за аутентификацию и аутентификацию пользователей.
Примечание: во избежание смещения центра тяжести в этой статье не будут представлены базовые знания NIO, Netty, Gateway и WebFlux (на самом деле автор не до конца понимает WebFlux).
2. Спрос
1. При первом входе в систему аутентификация входа выполняется в соответствии с паролем учетной записи пользователя.Если он правильный, токен будет сгенерирован и сохранен в Redis.
2. Когда токен переносится в заголовке запроса, оценивается, является ли токен законным и просроченным, и запрос выпускается после аутентификации.
3. По мнению пользователя, имеют ли они право на доступ к формулировке запроса, если есть, он будет выпущен, а если нет, то будет предупрежден.
3. Идеи реализации
Давайте сначала поговорим о выводе: Хотя автор и пытался, но в итоге он не использовал фреймворк с открытым исходным кодом.Из-за отсутствия кейсов уровень найденной информации также неравномерен, и бизнес-потребности автора не могут быть идеально Конкретные причины будут подробно описаны позже.
1. Реализовано с помощью SpringSecurity, диаграмма дорожки выглядит следующим образом
2. Несколько проблем, с которыми столкнулся автор при использовании SpringSecurity
(1) Новый порог обучения: основным конфигурационным файлом Security является уже не @Security, а @SecurityReactor, очевидно, это набор методов, специально реализованных Security for Reactor.
(2) Сложно разобрать параметры входа в фоновом режиме: шлюз получает запрос и ответ от объекта с именем ServerWebExchange. Полученный класс запроса называется ServerHttpRequest. получить параметры, переданные запросом POST.Stream, и могут быть прочитаны только один раз, данные будут потеряны при втором чтении, а также очень хлопотно извлечь имя пользователя и пароль для постоянной работы. Таким образом, автор напрямую изменил /login на Get request (escape
(3) Результат аутентификации не может быть получен: после успешной аутентификации значение isAuthenticated в объекте Authenticate не может быть изменено напрямую.
3. Исходя из вышеперечисленных причин, использование SpringSecurity в Gateway слишком сложно, поэтому автор решил написать от руки набор простых и удобных в использовании аутентификаций, основанных на результатах обучения в процессе прогрызания исходного кода. дизайн не умный, но его можно использовать.
4. Код
Don`t talk too much, show me the code !
1. Структура каталога:
2. Подробный код
Комментарии в коде написаны очень подробно, поэтому автор не будет объяснять слишком много, а именно:
(1) Основной фильтр использует простое принудительное суждение, если/иначе, чтобы определить следующий шаг программы.
/**
* @author 郭超
* Date:2020-09-29 9:50
* Description: 认证授权主配置类,使用过滤器链需要中间存储对象
*/
@Slf4j
@Component
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
public class SecurityFilter implements WebFilter {
/**
* 处理直接放行的请求
*/
@Resource
private ReleaseRequestHandler releaseRequestHandler;
/**
* 登陆请求处理类
*/
@Resource
private UserPassAuthenticationHandler userPassAuthenticationHandler;
/**
* 根据Token完成认证的处理类
*/
@Resource
private TokenAuthenticationHandler tokenAuthenticationHandler;
/**
* 用户鉴权处理类
*/
@Resource
private DynamicVerification dynamicPermission;
/**
* 鉴权成功处理类
*/
@Resource
private AuthorizationSuccessHandler authorizationSuccessHandler;
/**
* 认证失败异常处理类
*/
@Resource
private AuthenticationExceptionHandler authenticationExceptionHandler;
/**
* 鉴权失败异常处理类
*/
@Resource
private AuthorizationExceptionHandler authorizationExceptionHandler;
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
// 0. 判断是否为直接放行的请求
boolean isReleaseRequest = releaseRequestHandler.isReleaseRequest(request);
if (isReleaseRequest) {
// 如果是则直接放行
return chain.filter(exchange);
}
// 1. 为response装填JSONHeader
ServerHttpResponse response = SecurityHelper.setResponseHeader(exchange.getResponse());
// 2. 拦截请求,判断是否为第一次登陆
boolean isLoginPath = userPassAuthenticationHandler.isLoginPath(request);
boolean isStaticResourcePath = tokenAuthenticationHandler.isResourceRequest(request);
Authentication authentication;
if (isLoginPath) {
// 2.1 如果是,则进入通过账号密码认证的逻辑
authentication = userPassAuthenticationHandler.authenticate(exchange);
} else {
// 2.2 再判断是否访问静态资源
if (isStaticResourcePath) {
// 2.2.1 如果是,则直接放行
return chain.filter(exchange);
} else {
// 2.2.2 如果都不是,则进入通过token认证的逻辑
authentication = tokenAuthenticationHandler.authenticate(request);
}
}
int authenticatedStatus = authentication.getAuthenticatedStatus();
String userName = authentication.getUserName();
// 3 根据认证结果,判断是执行鉴权还是回写警告
DataBuffer dataBuffer;
if (authenticatedStatus == AuthenticatedStatus.AUTHENTICATION_SUCCESS ||
authenticatedStatus == AuthenticatedStatus.FIRST_LOGIN_SUCCESS) {
// 3.1 认证成功,进入鉴权逻辑
int authorizeStatus = dynamicPermission.check(userName, request);
if (authorizeStatus == AuthorizeStatus.AUTHORIZE_SUCCESS) {
if (authenticatedStatus == AuthenticatedStatus.AUTHENTICATION_SUCCESS) {
// 3.1.1 鉴权成功,且为Token,则直接放行
return chain.filter(exchange);
}
dataBuffer = authorizationSuccessHandler.onAuthorizationSuccess(userName, response);
} else {
// 3.1.2 鉴权失败,返回警告信息
dataBuffer = authorizationExceptionHandler.getWarningInfo(authorizeStatus, response);
}
} else {
// 3.2 认证失败,则根据返回状态,返回警告信息
dataBuffer = authenticationExceptionHandler.getWarningInfo(authenticatedStatus, response);
}
return response.writeWith(Mono.just(dataBuffer));
}
}
(2) Конфигурация запроса на освобождение
/**
* @author 郭超
* Date:2020-11-03 9:40
* Description: 处理直接放行的请求
*/
@Slf4j
@Component
public class ReleaseRequestHandler {
private List<String> releaseRequestPath = new ArrayList<>(Arrays.asList("/agent/"));
/**
* 判断是否为代理模块发送的请求
*
* @param request request
* @return boolean
*/
public boolean isReleaseRequest2(ServerHttpRequest request) {
// 获取request请求的Path
String path = request.getPath().toString();
AntPathMatcher antPathMatcher = new AntPathMatcher();
boolean result = releaseRequestPath.stream().anyMatch(item -> {
return antPathMatcher.match(item, path);
});
log.info("path.equals(agentPath) = " + result);
return result;
}
}
(3) Первый логин, класс обработки запроса пароля учетной записи.
/**
* @author 郭超
* Date:2020-09-29 10:35
* Description: 登陆请求处理类
*/
@Slf4j
@Component
public class UserPassAuthenticationHandler {
@Resource
private SysUserService userService;
/**
* 根据请求路径判断是否为第一次登陆
* 需满足以下两个条件才可认为是第一次登陆
* 1.请求路径为"/login"
* 2.ContentType为JSON
*
* @param request 当前请求
* @return result
*/
public boolean isLoginPath(ServerHttpRequest request) {
// 获取request请求的Path和ContentType
String path = request.getPath().toString();
HttpHeaders headers = request.getHeaders();
MediaType contentType = headers.getContentType();
log.info("path 0 =========== " + request.getPath());
/*if (contentType != null) {
log.info("contentType ============ " + contentType);
if (contentType.toString().equals(MediaType.APPLICATION_JSON_VALUE)
|| contentType.toString().equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {*/
String loginPath = "/login";
boolean result = path.equals(loginPath);
log.info("path.equals(loginPath) = " + result);
return result;
/*}
}
return false;*/
}
/**
* 账号密码登陆认证
*
* @param exchange ServerWebExchange
* @return 认证结果:是否存在该用户
*/
public Authentication authenticate(ServerWebExchange exchange) {
Authentication authentication = new Authentication();
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
log.info("userMap.toString()" + params.toString());
String userName = params.get("username").toString();
if (userName != null && !"".equals(userName)) {
userName = userName.replaceAll("[\\[\\]]", "");
authentication.setUserName(userName);
String password = params.get("password").toString().replaceAll("[\\[\\]]", "");
log.info("<===========用户:" + userName);
// 先判断用户是否存在
SysUser user = userService.getUserByUserName(userName);
if (user != null) {
// 执行认证
try {
if (userService.checkLogin(userName, password)) {
log.info("<===========用户:" + userName + "存在,身份验证通过!===========>");
authentication.setAuthenticatedStatus(AuthenticatedStatus.FIRST_LOGIN_SUCCESS);
} else {
authentication.setAuthenticatedStatus(AuthenticatedStatus.PASSWORD_NOT_MATCH);
}
} catch (Exception e) {
authentication.setAuthenticatedStatus(AuthenticatedStatus.UNKNOWN_EXCEPTION);
e.printStackTrace();
}
} else {
authentication.setAuthenticatedStatus(AuthenticatedStatus.USERNAME_NOT_EXSIT);
}
} else {
authentication.setAuthenticatedStatus(AuthenticatedStatus.NULL_USERNAME);
}
log.info("认证后得到的Authentication对象 = " + authentication.toString());
return authentication;
}
private Map<String, Object> decodeBody(String body) {
return Arrays.stream(body.split("&"))
.map(s -> s.split("="))
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
}
private String encodeBody(Map<String, Object> map) {
return map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
}
}
(4) Класс обработки, который завершает аутентификацию в соответствии с токеном.
/**
* @author 郭超
* Date:2020-09-29 10:42
* Description: 根据Token完成认证的处理类
*/
@Slf4j
@Component
public class TokenAuthenticationHandler {
@Resource
private JwtTokenUtil jwtTokenUtil;
/**
* 判断请求是否为访问静态资源的请求
*
* @param request request
* @return 判断结果
*/
public boolean isResourceRequest(ServerHttpRequest request) {
String path = request.getPath().toString();
log.info("path =========== " + request.getPath());
String staticResourcePath = ".*/images/.*";
boolean result = Pattern.matches(staticResourcePath, path);
log.info("判断当前路径是否为静态资源的结果为: " + result);
return result;
}
/**
* 根据token执行认证
*
* @param request ServerHttpRequest
* @return 认证结果
*/
public Authentication authenticate(ServerHttpRequest request) {
List<String> auth = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
Authentication authentication = new Authentication();
if (auth != null && auth.size() > 0 && !StringUtils.isEmpty(auth.get(0))) {
log.info("获取到的 headerToken = " + auth.get(0));
String headerToken = auth.get(0);
// postMan测试时,自动加入的前缀,要去掉。
String token = headerToken.replace("Bearer", "").trim();
// 先判断令牌是否过期
boolean isExpired = jwtTokenUtil.isTokenExpired(token);
if (isExpired) {
// 过期刷新
jwtTokenUtil.refreshToken(token);
}
// 通过令牌获取用户名称
String username = jwtTokenUtil.getUsernameFromToken(token);
log.info("从token令牌中获取到的username = " + username);
if (username != null) {
// 没过期则刷新令牌,重置有效期,然后放行
jwtTokenUtil.refreshToken(token);
authentication.setUserName(username);
authentication.setAuthenticatedStatus(AuthenticatedStatus.AUTHENTICATION_SUCCESS);
log.info("认证成功:Token认证成功!");
} else {
authentication.setAuthenticatedStatus(AuthenticatedStatus.INVALID_TOKEN);
log.info("认证失败:无效Token,无法从中获取Token!");
}
} else {
authentication.setAuthenticatedStatus(AuthenticatedStatus.NULL_TOKEN);
log.info("认证失败:当前请求头没有Token信息!");
}
return authentication;
}
}
(5) Класс обработки аутентификации пользователя
/**
* @author 郭超
* Date:2020-09-29 10:45
* Description: 用户鉴权处理类
*/
@Slf4j
@Component
public class DynamicVerification {
@Resource
private SysBackendApiService apiService;
/**
* 动态鉴权,根据用户名查询可访问API集合,然后与当前访问API进行匹配
*
* @param userName 当前登陆用户名
* @param request 当前请求
* @return 鉴权结果
*/
public int check(String userName, ServerHttpRequest request) {
log.info("动态权限校验 DynamicPermission");
// 通过账号获取资源鉴权
List<SysBackendApi> apiUrls = apiService.getApiUrlByUserName(userName);
if (apiUrls != null) {
AntPathMatcher antPathMatcher = new AntPathMatcher();
// 当前访问路径
RequestPath requestUri = request.getPath();
log.info("动态权限校验 => 当前访问路径 = " + requestUri);
// 提交类型
HttpMethod urlMethod = request.getMethod();
// 判断当前路径中是否在资源鉴权中
boolean rs = apiUrls.stream().anyMatch(item -> {
// 判断URL是否匹配
boolean hashAntPath = antPathMatcher.match(item.getApiUrl(), String.valueOf(requestUri));
// 判断请求方式是否和数据库中匹配(数据库存储:GET,POST,PUT,DELETE)
String dbMethod = item.getApiMethod();
// 处理null,万一数据库存null值
dbMethod = (dbMethod == null) ? "" : dbMethod;
int hasMethod = dbMethod.indexOf(String.valueOf(urlMethod));
log.info("hashAntPath = " + hashAntPath);
log.info("hasMethod = " + hasMethod);
log.info("hashAntPath && hasMethod = " + (hashAntPath && hasMethod != -1));
// 两者都成立,返回真,否则返回假
boolean result = hashAntPath && (hasMethod != -1);
if (result) {
log.info("result == " + true + "<==============URL匹配且用户具有访问权限,权限校验通过!============>");
}
return result;
});
if (rs) {
return AuthorizeStatus.AUTHORIZE_SUCCESS;
} else {
return AuthorizeStatus.AUTHORIZE_INSUFFICIENT;
}
} else {
return AuthorizeStatus.NULL_AUTHORIZATION;
}
}
}
(6) Класс обработки успешной аутентификации
/**
* @author 郭超
* Date:2020-09-29 11:00
* Description: 鉴权成功处理类
*/
@Slf4j
@Component
public class AuthorizationSuccessHandler {
@Resource
private SysFrontendMenuService menuService;
@Resource
private JwtTokenUtil jwtTokenUtil;
@Resource
private RedisUtil redisUtil;
/**
* 根据用户名查询出用户详情,并转换为DataBuffer返回
*
* @param userName 用户名
* @param response 响应
* @return 用户详细信息流
*/
public DataBuffer onAuthorizationSuccess(String userName, ServerHttpResponse response) {
String userToken = (String) redisUtil.get(userName);
if (userToken == null || "".equals(userToken.trim())) {
// 如果token为空,则通过JWT创建一个新的token
userToken = jwtTokenUtil.generateToken(userName);
// 把新的token存储到Redis中
redisUtil.set(userName, userToken);
log.info("第一次登陆,token为空,创建并存储到Redis中的Token:" + userToken);
} else {
log.info("3.2 Redis中已有该用户, 根据用户名从Redis中取到的token = " + userToken);
}
// 设置body,封装数据
List<SysFrontendMenu> menus = menuService.getMenusByUserName(userName);
Map<String, Object> map = new HashMap<>(8);
map.put("username", userName);
map.put("menus", menus);
map.put("token", userToken);
log.info("4. 存入Map中返回到前台的数据内容为: " + map);
ResponseResult result = ResponseResult.success(map);
byte[] dataBytes;
ObjectMapper mapper = new ObjectMapper();
try {
dataBytes = mapper.writeValueAsBytes(result);
} catch (JsonProcessingException e) {
dataBytes = ResponseResult.fail("授权异常").toString().getBytes();
}
return response.bufferFactory().wrap(dataBytes);
}
}
(7) Класс определения исключения отказа аутентификации
/**
* @author 郭超
* Date:2020-09-29 11:09
* Description: 异常定义类
*/
@Slf4j
@Component
public class AuthenticationExceptionHandler {
/**
* 根据认证时返回的状态值不同,返回不同的警告信息
*
* @param authenticatedStatus 认证异常状态
* @param response 响应
* @return 警告信息Buffer
*/
public DataBuffer getWarningInfo(int authenticatedStatus, ServerHttpResponse response) {
String exceptionMessage;
switch (authenticatedStatus) {
case AuthenticatedStatus.USERNAME_NOT_EXSIT:
exceptionMessage = "认证失败: 用户不存在!";
break;
case AuthenticatedStatus.PASSWORD_NOT_MATCH:
exceptionMessage = "认证失败: 密码不正确!";
break;
case AuthenticatedStatus.LOGIN_CONTENT_TYPE_NOT_JSON:
exceptionMessage = "认证失败: 登陆请求必须为JSON方式!";
break;
case AuthenticatedStatus.NULL_USERNAME:
exceptionMessage = "认证失败: 未能从JSON中获取用户名!";
break;
case AuthenticatedStatus.UNKNOWN_EXCEPTION:
exceptionMessage = "认证失败: 登陆发生未知异常!";
break;
case AuthenticatedStatus.NULL_TOKEN:
exceptionMessage = "认证失败: Token为空,请重新登陆!";
break;
case AuthenticatedStatus.INVALID_TOKEN:
exceptionMessage = "认证失败: 无效Token,请尝试重新登陆!";
break;
case AuthenticatedStatus.TOKEN_EXPIRED:
exceptionMessage = "认证失败: Token已过期,请重新登陆!";
break;
default:
exceptionMessage = "";
break;
}
return response.bufferFactory().wrap(exceptionMessage.getBytes());
}
}
(8) Класс определения исключения ошибки аутентификации
/**
* @author 郭超
* Date:2020-09-29 11:06
* Description: 鉴权失败异常处理类
*/
@Slf4j
@Component
public class AuthorizationExceptionHandler {
/**
* 根据鉴权时返回的状态值不同,返回不同的警告信息
*
* @param authorizeStatus 鉴权异常状态
* @param response 响应
* @return 警告信息Buffer
*/
public DataBuffer getWarningInfo(int authorizeStatus, ServerHttpResponse response) {
String exceptionMessage;
switch (authorizeStatus) {
case AuthorizeStatus.AUTHORIZE_INSUFFICIENT:
exceptionMessage = "访问失败,权限不足!";
break;
case AuthorizeStatus.NULL_AUTHORIZATION:
exceptionMessage = "访问失败,权限为空!";
break;
default:
exceptionMessage = "";
break;
}
return response.bufferFactory().wrap(exceptionMessage.getBytes());
}
}
(9) Код статуса результата аутентификации
/**
* @author 郭超
* Date:2020-09-29 11:33
* Description: 认证结果状态码
*/
public interface AuthenticatedStatus {
/**
* 第一次登陆成功,特用于成功后存放用户详细信息
*/
int FIRST_LOGIN_SUCCESS = 0;
/**
* 认证成功
*/
int AUTHENTICATION_SUCCESS = 1;
/**
* 用户名不存在
*/
int USERNAME_NOT_EXSIT = 2;
/**
* 密码不正确
*/
int PASSWORD_NOT_MATCH = 3;
/**
* Login请求类型不为JSON
*/
int LOGIN_CONTENT_TYPE_NOT_JSON = 4;
/**
* 未能从requestBody中获取用户名
*/
int NULL_USERNAME = 5;
/**
* 未知认证异常
*/
int UNKNOWN_EXCEPTION = 6;
//========================以下为Token认证的异常==========================
/**
* 未能从header中获取Token
*/
int NULL_TOKEN = 7;
/**
* 无效TOKEN,无法从中获取Token
*/
int INVALID_TOKEN = 8;
/**
* Token已过期
*/
int TOKEN_EXPIRED = 9;
}
(10) Код статуса результата авторизации
/**
* @author 郭超
* Date:2020-09-29 11:25
* Description: 授权状态码
*/
public interface AuthorizeStatus {
/**
* 授权成功
*/
int AUTHORIZE_SUCCESS = 1;
/**
* 权限不足
*/
int AUTHORIZE_INSUFFICIENT = 2;
/**
* 空权限
*/
int NULL_AUTHORIZATION = 3;
}
(11) Пользовательский объект аутентификации
/**
* @author 郭超
* Date:2020-09-29 11:13
* Description: 自定义认证对象
*/
@Data
public class Authentication {
/**
* 认证状态
*/
private int authenticatedStatus;
/**
* 正在认证的用户名
*/
private String userName;
}