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.