For projects separated from the front and back ends, the front end has menu and the backend has API. A page corresponding to menu has N API interfaces to support it. This paper introduces how to realize the synchronization permission control of the front and back ends based on Spring Security.

Implementation approach

The implementation is still based on Role. The specific idea is that a Role has multiple menus, and a Menu has multiple backendapis, among which Role and Menu, as well as Menu and backendApi are ManyToMany relations.

Authentication authorization is also simple. When a user logs in to the system, the user obtains the Menu associated with Role. When the page accesses the back-end API, the user verifies whether he/she has the permission to access the API.

Domain definition

Let’s use JPA to do this. Let’s define roles first

public class Role implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; /** * name */ @notnull @APIModelProperty (value = "name", Required = true) @column (name = "name", nullable = false) private String name; /** */ @apiModelProperty (value = "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<>(); }Copy the code

And the Menu:

public class Menu implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "parent_id") private Integer parentId; /** * text */ @apiModelProperty (value = "text") @column (name = "text") private String text; @APIModelProperty (value = "Angular route ") @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<>(); }Copy the code

BackendApi: 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<>();
    
    }Copy the code

Management page implementation

The Menu Menu is defined by business requirements, so CRUD editing can be provided. BackendAPI, can be obtained by Swagger. The front-end option is ng-algin, as described in Angular backend Front-end solution -ng Alain

Get BackendAPI by Swagger

There are many ways to obtain the Swagger API. The simplest way is to access the HTTP interface to obtain json and then parse it. This is very simple and I won’t go into details here, and the other way is to directly call the relevant API to obtain the Swagger object.

Looking at the official Web code, you can see something like this:

        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);Copy the code

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

    }Copy the code

Write an updateApi, read the Swagger object, parse it into 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) {// Load existing API Map<String,Boolean> apiMap = maps.newhashMap (); List<BackendApi> apis = backendApiRepository.findAll(); apis.stream().forEach(api->apiMap.put(api.getPath()+api.getMethod(),true)); 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); }Copy the code

CreateApiIfNeeded check whether it exists. If it does not, add:

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()); / / save this. BackendApiRepository. Save (API); }}Copy the code

Finally, make a simple page presentation:

Menu management

Add and modify the page, you can choose the higher menu, background API made according to the tag group, can choose more than:

List page

Role management

For ordinary CRUD, the most important thing is to add a menu authorization page, which can be displayed by hierarchy:

Certification implementation

Management pages can be made of thousands of hundreds of, the core or how to achieve certification.

In an article in spring security to realize dynamic configuration url permissions of two methods, we said, you can customize FilterInvocationSecurityMetadataSource to implement.

Implement FilterInvocationSecurityMetadataSource interface, the core is according to the Request of FilterInvocation method and path, get the corresponding roles, and then to RoleVoter to determine whether to have permissions.

Custom FilterInvocationSecurityMetadataSource

We create a DaoSecurityMetadataSource FilterInvocationSecurityMetadataSource interface, basically see 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 the default configuration return superMetadataSource. GetAttributes (object); }Copy the code

GetRequestNeededRoles: Get a clean RequestUrl and see if there is a corresponding backendAPI. If there is no backendAPI, 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, 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); While (apis==null){if(path.lastIndexof ("/")>-1) {path = path.lastIndexof (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; }Copy the code

BackendApiRepository:

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

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

And MenuRepository

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

Using DaoSecurityMetadataSource

It is important to note that in DaoSecurityMetadataSource, cannot be directly injected into the Repository, we can add a method to DaoSecurityMetadataSource, convenient to:

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

And then set up a container, storage instantiation DaoSecurityMetadataSource, we can build the following ApplicationContext to as object container, access the object:

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);
    }
}Copy the code

Using DaoSecurityMetadataSource in SecurityConfiguration configuration, and through the ApplicationContext. RegisterBean DaoSecurityMetadataSource to registration:

@Override protected void configure(HttpSecurity http) throws Exception { http .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() .authenticationEntryPoint(problemSupport) .accessDeniedHandler(problemSupport) .... //.withobjectPostProcessor () // Customize accessDecisionManager. AccessDecisionManager ()) // Custom 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; }Copy the code

Finally, the program starts, through the ApplicationContext. Get to daoSecurityMetadataSource getBean, then calls the init into the 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 (); }Copy the code

And you’re done!

read

  • Spring Security implements two ways to dynamically configure URL permissions
  • Spring Security architecture and source code analysis

Your support is the biggest encouragement to bloggers. Thank you for reading carefully. The copyright of this article belongs to the author, welcome to reprint, but without the consent of the author must retain this statement, and give the original text link in the obvious position of the article page, otherwise reserve the right to pursue legal responsibility.