Реализация аутентификации пользователя и аутентификации с помощью шлюза

распределенный

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;
}

Пять, дизайн базы данных

Исходный код фронтенда и бэкэнда проекта выложен на GitHub,портал

Если этот блог будет полезен для вас, я надеюсь дать автору лайк и звезду!