Implementation of front-end and back-end separation project permission control based on spring security

The front end has a menu and the back end has an API. The page corresponding to a menu is supported by N API interfaces. This paper introduces how to realize the synchronous permission control of the front and back ends based on spring security.

Realization idea

It is also implemented based on role. The specific idea is that a role has multiple menus and a menu has multiple backendapis. Role and menu, and menu and backendapi are manytomany relationships.

It is also very simple to verify the authorization. When the user logs in to the system, obtain the menu associated with the role. When the page accesses the back-end API, verify whether the user has the permission to access the API.

Domain definition

We use JPA to implement it. Let's define role first

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

And menu:

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

Finally, backendapi distinguishes between method (HTTP request method), tag (which controller) and path (API request path):

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

Management page implementation

The menu is determined by business requirements, so crud can be provided for editing. Backend API, which can be obtained through swagger. Ng algin is selected for the front end. Please refer to the introduction of NG Alain, the front end solution in the background of angular

Get backendapi via swagger

There are many ways to obtain swagger APIs. The simplest way is to access the HTTP interface to obtain JSON and then parse it. This is very simple. I won't repeat it here. Another way is to directly call the relevant APIs to obtain swagger objects.

Looking at the official web code, you can see that the obtained data is roughly as follows:

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

Documentationcache, environment, mapper, etc. can be obtained directly from 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;

    }

Then it's easy to load automatically. Write an updateapi interface, read the swagger object, parse it into the backendapi, and store it in the database:

@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,pathInfo.getPost(),HttpMethod.POST.name());
            createApiIfNeeded(apiMap,pathInfo.getDelete(),HttpMethod.DELETE.name());
            createApiIfNeeded(apiMap,pathInfo.getPut(),HttpMethod.PUT.name());
        }
        return new ResponseEntity<Json>(HttpStatus.OK);
    }

Among them, createapiifneeded, first judge whether it exists, and add if it does not exist:

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

Finally, make a simple page display:

Menu management

For adding and modifying pages, you can select the upper menu, and the background API is grouped by tag. You can select more than one:

List page

Role management

For ordinary crud, the most important thing is to add a menu authorization page, and the menu can be displayed by level:

Authentication implementation

The management page can be made into thousands of kinds, and the core is how to realize authentication.

In the previous article, we mentioned two methods for spring security to dynamically configure URL permissions. We can customize the filterinvocationsecuritymetadatasource.

Implement the filterinvocationsecuritymetadatasource interface. The core is to obtain the corresponding role according to the method and path of the request of filterinvocation, and then hand it to rolevoter to judge whether it has permission.

Custom filterinvocationsecuritymetadatasource

Let's create a new daosecuritymetadatasource to implement the filterinvocationsecuritymetadatasource interface. We mainly look at the getattributes method:

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

The core is how to implement getrequestneededroles, get a clean requesturl (remove the parameters), and then see if there is a corresponding backendapi. If not, it is possible that the API has a path parameter. We can remove the last path and go to the library for fuzzy matching until we find it.

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

Among them, backendapirepository:

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

    @EntityGraph(attributePaths = "menus")
    List<BackendApi> findByPathStartsWithAndMethod(String path,String method);
	

And menurepository

    @EntityGraph(attributePaths = "roles")
    Menu findOneWithRolesById(long id);

Using daosecuritymetadatasource

It should be noted that the repository cannot be directly injected into the daosecuritymetadatasource. We can add a method to the daosecuritymetadatasource to facilitate the incoming:

   public void init(MenuRepository menuRepository,BackendApiRepository backendApiRepository) {
        this.menuRepository = menuRepository;
        this.backendApiRepository = backendApiRepository;
    }

Then create a container to store the instantiated daosecuritymetadatasource. We can create the following ApplicationContext as an object container to access objects:

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

Use daosecuritymetadatasource in securityconfiguration configuration and use ApplicationContext Registerbean registers 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;
    }

Finally, after the program starts, use ApplicationContext GetBean gets to daoSecurityMetadataSource, then calls init to Repository.

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

be accomplished!

Extended reading

The content of this article comes from the network collection of netizens. It is used as a learning reference. The copyright belongs to the original author.
THE END
分享
二维码
< <上一篇
下一篇>>