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!