На основе весенней безопасности для обеспечения контроля разрешений проекта разделения интерфейса и сервера.

Java Spring API

Передняя часть имеет меню (меню), задняя часть имеет API (backendApi), а страница, соответствующая меню, поддерживается интерфейсами API N. В этой статье рассказывается, как реализовать управление разрешениями на синхронизацию для передней и задней частей. на основе весенней безопасности.

Реализовать идеи

Он по-прежнему реализуется на основе роли.Конкретная идея заключается в том, что роль имеет несколько меню, а меню имеет несколько бэкендапи, из которых роль и меню, а также меню и бэкендапи являются отношениями ManyToMany.

Аутентификация и авторизация также очень просты.Когда пользователь входит в систему, он получает меню, связанное с ролью.Когда страница обращается к внутреннему API, затем проверяется, есть ли у пользователя разрешение на доступ к API.

Определение домена

Мы используем JPA для достижения, сначала определить роль

public class Role implements Serializable {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 名称
     */
    @NotNull
    @ApiModelProperty(value = "名称", required = true)
    @Column(name = "name", nullable = false)
    private String name;

    /**
     * 备注
     */
    @ApiModelProperty(value = "备注")
    @Column(name = "remark")
    private String remark;

    @JsonIgnore
    @ManyToMany
    @JoinTable(
        name = "role_menus",
        joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
        inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    @BatchSize(size = 100)
    private Set<Menu> menus = new HashSet<>();
    
    }скопировать код

и меню:

public class Menu implements Serializable {


    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "parent_id")
    private Integer parentId;

    /**
     * 文本
     */
    @ApiModelProperty(value = "文本")
    @Column(name = "text")
    private String text;
    
    @ApiModelProperty(value = "angular路由")
    @Column(name = "link")
    private String link;
    
    @ManyToMany
    @JsonIgnore
    @JoinTable(name = "backend_api_menus",
        joinColumns = @JoinColumn(name="menus_id", referencedColumnName="id"),
        inverseJoinColumns = @JoinColumn(name="backend_apis_id", referencedColumnName="id"))
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private Set<BackendApi> backendApis = new HashSet<>();

    @ManyToMany(mappedBy = "menus")
    @JsonIgnore
    private Set<Role> roles = new HashSet<>();
    }
    
    скопировать код

Последним является BackendApi, который различает метод (метод запроса HTTP), тег (какой контроллер) и путь (путь запроса API):

public class BackendApi implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "tag")
    private String tag;

    @Column(name = "path")
    private String path;

    @Column(name = "method")
    private String method;

    @Column(name = "summary")
    private String summary;

    @Column(name = "operation_id")
    private String operationId;

    @ManyToMany(mappedBy = "backendApis")
    @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
    private Set<Menu> menus = new HashSet<>();
    
    }скопировать код

Реализация страницы администратора

Меню меню определяется потребностями в бизнесе, поэтому достаточно для обеспечения редактирования CRUD.
Backendapi может быть получен с помощью чванство.
Внешний интерфейс выбирает ng-algin, см.Решение Angular для среднего и внешнего интерфейса — введение Нг Алена

Получить BackendAPI через swagger

Есть много способов получить swagger api. Самый простой способ — получить доступ к интерфейсу http, чтобы получить json, а затем проанализировать его. Это очень просто, и я не буду здесь вдаваться в подробности. Другой способ — напрямую вызвать соответствующий API для получения объекта Swagger.

Глядя на официальный веб-код, можно увидеть, что полученные данные, вероятно, такие:

        String groupName = Optional.fromNullable(swaggerGroup).or(Docket.DEFAULT_GROUP_NAME);
        Documentation documentation = documentationCache.documentationByGroup(groupName);
        if (documentation == null) {
            return new ResponseEntity<Json>(HttpStatus.NOT_FOUND);
        }
        Swagger swagger = mapper.mapDocumentation(documentation);
        UriComponents uriComponents = componentsFrom(servletRequest, swagger.getBasePath());
        swagger.basePath(Strings.isNullOrEmpty(uriComponents.getPath()) ? "/" : uriComponents.getPath());
        if (isNullOrEmpty(swagger.getHost())) {
            swagger.host(hostName(uriComponents));
        }
        return new ResponseEntity<Json>(jsonSerializer.toJson(swagger), HttpStatus.OK);скопировать код

Документацию по кешу, окружению, мапперу и т. д. можно получить напрямую через Autowired:

@Autowired
    public SwaggerResource(
        Environment environment,
        DocumentationCache documentationCache,
        ServiceModelToSwagger2Mapper mapper,
        BackendApiRepository backendApiRepository,
        JsonSerializer jsonSerializer) {

        this.hostNameOverride = environment.getProperty("springfox.documentation.swagger.v2.host", "DEFAULT");
        this.documentationCache = documentationCache;
        this.mapper = mapper;
        this.jsonSerializer = jsonSerializer;

        this.backendApiRepository = backendApiRepository;

    }скопировать код

Затем нам легко загрузиться автоматически, написать интерфейс updateApi, прочитать объект swagger, разобрать его в BackendAPI и сохранить в базе данных:

@RequestMapping(
        value = "/api/updateApi",
        method = RequestMethod.GET,
        produces = { APPLICATION_JSON_VALUE, HAL_MEDIA_TYPE })
    @PropertySourcedMapping(
        value = "${springfox.documentation.swagger.v2.path}",
        propertyKey = "springfox.documentation.swagger.v2.path")
    @ResponseBody
    public ResponseEntity<Json> updateApi(
        @RequestParam(value = "group", required = false) String swaggerGroup) {

        // 加载已有的api
        Map<String,Boolean> apiMap = Maps.newHashMap();
        List<BackendApi> apis = backendApiRepository.findAll();
        apis.stream().forEach(api->apiMap.put(api.getPath()+api.getMethod(),true));

        // 获取swagger
        String groupName = Optional.fromNullable(swaggerGroup).or(Docket.DEFAULT_GROUP_NAME);
        Documentation documentation = documentationCache.documentationByGroup(groupName);
        if (documentation == null) {
            return new ResponseEntity<Json>(HttpStatus.NOT_FOUND);
        }
        Swagger swagger = mapper.mapDocumentation(documentation);

        // 加载到数据库
        for(Map.Entry<String, Path> item : swagger.getPaths().entrySet()){
            String path = item.getKey();
            Path pathInfo = item.getValue();
            createApiIfNeeded(apiMap, path,  pathInfo.getGet(), HttpMethod.GET.name());
            createApiIfNeeded(apiMap, path,  pathInfo.getPost(), HttpMethod.POST.name());
            createApiIfNeeded(apiMap, path,  pathInfo.getDelete(), HttpMethod.DELETE.name());
            createApiIfNeeded(apiMap, path,  pathInfo.getPut(), HttpMethod.PUT.name());
        }
        return new ResponseEntity<Json>(HttpStatus.OK);
    }скопировать код

Среди них createApiIfNeeded, сначала оцените, существует ли он, и добавьте его, если он не существует:

 private void createApiIfNeeded(Map<String, Boolean> apiMap, String path, Operation operation, String method) {
        if(operation==null) {
            return;
        }
        if(!apiMap.containsKey(path+ method)){
            apiMap.put(path+ method,true);

            BackendApi api = new BackendApi();
            api.setMethod( method);
            api.setOperationId(operation.getOperationId());
            api.setPath(path);
            api.setTag(operation.getTags().get(0));
            api.setSummary(operation.getSummary());

            // 保存
            this.backendApiRepository.save(api);
        }
    }скопировать код

Наконец, сделайте простое отображение страницы:

enter description here

управление меню

Добавляйте и изменяйте страницу, вы можете выбрать предыдущее меню, фоновый API, созданный с помощью группировки тегов, множественный выбор:

enter description here

страница со списком

enter description here

управление ролями

Обыкновенный CRUD, самое главное добавить страницу авторизации меню, причем меню можно отображать слоями:

enter description here

Реализация аутентификации

Страница управления может быть разного вида, и самое главное, как реализовать аутентификацию.

в предыдущей статьеБезопасность Spring - это два способа получить права URL-адреса динамической конфигурации.Как мы уже говорили, его можно настроитьFilterInvocationSecurityMetadataSourceреализовать.

выполнитьFilterInvocationSecurityMetadataSourceИнтерфейса достаточно.Ядро состоит в том, чтобы получить соответствующую роль в соответствии с методом и путем запроса FilterInvocation, а затем передать ее RoleVoter, чтобы определить, есть ли у него разрешение.

Пользовательский фильтрInvocationSecurityMetadataSource

Мы создаем новый DaoSecurityMetadataSource для реализации интерфейса FilterInvocationSecurityMetadataSource, в основном рассматривая метод getAttributes:

     @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        FilterInvocation fi = (FilterInvocation) object;

        List<Role> neededRoles = this.getRequestNeededRoles(fi.getRequest().getMethod(), fi.getRequestUrl());

        if (neededRoles != null) {
            return SecurityConfig.createList(neededRoles.stream().map(role -> role.getName()).collect(Collectors.toList()).toArray(new String[]{}));
        }

        //  返回默认配置
        return superMetadataSource.getAttributes(object);
    }скопировать код

Суть в том, как реализовать getRequestNeededRoles, получить чистый RequestUrl (удалить параметры), а затем посмотреть, есть ли соответствующий бэкенд API, если нет, то возможно, что в API есть параметр пути, мы можем удалить последний путь, идем в библиотеку для нечеткого сопоставления, пока не появится.

 public List<Role> getRequestNeededRoles(String method, String path) {
        String rawPath = path;
        //  remove parameters
        if(path.indexOf("?")>-1){
            path = path.substring(0,path.indexOf("?"));
        }
        // /menus/{id}
        BackendApi api = backendApiRepository.findByPathAndMethod(path, method);
        if (api == null){
            // try fetch by remove last path
            api = loadFromSimilarApi(method, path, rawPath);
        }

        if (api != null && api.getMenus().size() > 0) {
            return api.getMenus()
                .stream()
                .flatMap(menu -> menuRepository.findOneWithRolesById(menu.getId()).getRoles().stream())
                .collect(Collectors.toList());
        }
        return null;
    }

    private BackendApi loadFromSimilarApi(String method, String path, String rawPath) {
        if(path.lastIndexOf("/")>-1){
            path = path.substring(0,path.lastIndexOf("/"));
            List<BackendApi> apis = backendApiRepository.findByPathStartsWithAndMethod(path, method);

            // 如果为空,再去掉一层path
            while(apis==null){
                if(path.lastIndexOf("/")>-1) {
                    path = path.substring(0, path.lastIndexOf("/"));
                    apis = backendApiRepository.findByPathStartsWithAndMethod(path, method);
                }else{
                    break;
                }
            }

            if(apis!=null){
                for(BackendApi backendApi : apis){
                    if (antPathMatcher.match(backendApi.getPath(), rawPath)) {
                        return backendApi;
                    }
                }
            }
        }
        return null;
    }скопировать код

Среди них BackendApiRepository:

    @EntityGraph(attributePaths = "menus")
    BackendApi findByPathAndMethod(String path,String method);

    @EntityGraph(attributePaths = "menus")
    List<BackendApi> findByPathStartsWithAndMethod(String path,String method);
    скопировать код

и MenuRepository

    @EntityGraph(attributePaths = "roles")
    Menu findOneWithRolesById(long id);скопировать код

Использовать DaoSecurityMetadataSource

Следует отметить, что в DaoSecurityMetadataSource Repository нельзя внедрить напрямую, мы можем добавить метод в DaoSecurityMetadataSource для облегчения входящего:

   public void init(MenuRepository menuRepository, BackendApiRepository backendApiRepository) {
        this.menuRepository = menuRepository;
        this.backendApiRepository = backendApiRepository;
    }скопировать код

Затем создайте контейнер для хранения экземпляра DaoSecurityMetadataSource.Мы можем создать следующий ApplicationContext в качестве контейнера объектов для доступа к объектам:

public class ApplicationContext {
    static Map<Class<?>,Object> beanMap = Maps.newConcurrentMap();

    public static <T> T getBean(Class<T> requireType){
        return (T) beanMap.get(requireType);
    }

    public static void registerBean(Object item){
        beanMap.put(item.getClass(),item);
    }
}скопировать код

Использовать в конфигурации SecurityConfigurationDaoSecurityMetadataSource, и пройтиApplicationContext.registerBeanбудетDaoSecurityMetadataSourceрегистр:

 @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
            .exceptionHandling()
            .authenticationEntryPoint(problemSupport)
            .accessDeniedHandler(problemSupport)
            ....
           // .withObjectPostProcessor()
            // 自定义accessDecisionManager
            .accessDecisionManager(accessDecisionManager())
            // 自定义FilterInvocationSecurityMetadataSource
            .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                @Override
                public <O extends FilterSecurityInterceptor> O postProcess(
                    O fsi) {
                    fsi.setSecurityMetadataSource(daoSecurityMetadataSource(fsi.getSecurityMetadataSource()));
                    return fsi;
                }
            })
        .and()
            .apply(securityConfigurerAdapter());

    }

    @Bean
    public DaoSecurityMetadataSource daoSecurityMetadataSource(FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource) {
        DaoSecurityMetadataSource securityMetadataSource = new DaoSecurityMetadataSource(filterInvocationSecurityMetadataSource);
        ApplicationContext.registerBean(securityMetadataSource);
        return securityMetadataSource;
    }скопировать код

Наконец, после запуска программы, перейдитеApplicationContext.getBeanПолучите daoSecurityMetadataSource, затем вызовите init для внедрения репозитория

 public static void postInit(){
        ApplicationContext
            .getBean(DaoSecurityMetadataSource.class)
 .init(applicationContext.getBean(MenuRepository.class),applicationContext.getBean(BackendApiRepository.class));
    }

    static ConfigurableApplicationContext applicationContext;

    public static void main(String[] args) throws UnknownHostException {
        SpringApplication app = new SpringApplication(UserCenterApp.class);
        DefaultProfileUtil.addDefaultProfile(app);
        applicationContext = app.run(args);

        // 后初始化
        postInit();
}скопировать код

Готово!

дальнейшее чтение


Добавить Автора
Источник: технический блокнот jqpeng.www.cnblogs.com/xiaoqi
Ваша поддержка — величайшее поощрение для блоггеров, спасибо за внимательное чтение.
Авторские права на эту статью принадлежат автору, и вы можете ее перепечатать, но это заявление должно быть сохранено без согласия автора, а ссылка на исходный текст дана в видном месте на странице статьи, в противном случае право преследовать юридическую ответственность зарезервировано.