前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于spring security实现前后端的同步权限控制。

实现思路

还是基于Role来实现,具体的思路是,一个Role拥有多个Menu,一个menu有多个backendApi,其中Role和menu,以及menu和backendApi都是ManyToMany关系。

验证授权也很简单,用户登陆系统时,获取Role关联的Menu,页面访问后端API时,再验证下用户是否有访问API的权限。

domain定义

我们用JPA来实现,先来定义Role

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

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

最后是BackendApi,区分method(HTTP请求方法)、tag(哪一个Controller)和path(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<>(); }

管理页面实现

Menu菜单是业务需求确定的,因此提供CRUD编辑即可。

BackendAPI,可以通过swagger来获取。

前端选择ng-algin,参见Angular 中后台前端解决方案 - Ng Alain 介绍

通过swagger获取BackendAPI

获取swagger api有多种方法,最简单的就是访问http接口获取json,然后解析,这很简单,这里不赘述,还有一种就是直接调用相关API获取Swagger对象。

查看官方的web代码,可以看到获取数据大概是这样的:

        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等可以直接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);
}
}

最后,做一个简单页面展示即可:

菜单管理

新增和修改页面,可以选择上级菜单,后台API做成按tag分组,可多选即可:

列表页面

角色管理

普通的CRUD,最主要的增加一个菜单授权页面,菜单按层级显示即可:

认证实现

管理页面可以做成千奇百样,最核心的还是如何实现认证。

在上一篇文章spring security实现动态配置url权限的两种方法里我们说了,可以自定义FilterInvocationSecurityMetadataSource来实现。

实现FilterInvocationSecurityMetadataSource接口即可,核心是根据FilterInvocation的Request的method和path,获取对应的Role,然后交给RoleVoter去判断是否有权限。

自定义FilterInvocationSecurityMetadataSource

我们新建一个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(去掉参数),然后看是否有对应的backendAPI,如果没有,则有可能该API有path参数,我们可以去掉最后的path,去库里模糊匹配,直到找到。

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

在SecurityConfiguration配置中使用DaoSecurityMetadataSource,并通过 ApplicationContext.registerBeanDaoSecurityMetadataSource注册:

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

大功告成!

延伸阅读


作者:Jadepeng

出处:jqpeng的技术记事本--http://www.cnblogs.com/xiaoqi

您的支持是对博主最大的鼓励,感谢您的认真阅读。

本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

基于spring security 实现前后端分离项目权限控制的更多相关文章

  1. 基于 Spring Security 的前后端分离的权限控制系统

    话不多说,入正题.一个简单的权限控制系统需要考虑的问题如下: 权限如何加载 权限匹配规则 登录 1.  引入maven依赖 1 <?xml version="1.0" enc ...

  2. 喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了

    折腾了一周的域名备案昨天终于搞定了. 松哥第一时间想到赶紧把微人事和 V 部落部署上去,我知道很多小伙伴已经等不及了. 1. 也曾经上过线 其实这两个项目当时刚做好的时候,我就把它们部署到服务器上了, ...

  3. 两个开源的 Spring Boot + Vue 前后端分离项目

    折腾了一周的域名备案昨天终于搞定了. 松哥第一时间想到赶紧把微人事和 V 部落部署上去,我知道很多小伙伴已经等不及了. 1. 也曾经上过线 其实这两个项目当时刚做好的时候,我就把它们部署到服务器上了, ...

  4. 如何使用Spring Securiry实现前后端分离项目的登录功能

    如果不是前后端分离项目,使用SpringSecurity做登录功能会很省心,只要简单的几项配置,便可以轻松完成登录成功失败的处理,当访问需要认证的页面时,可以自动重定向到登录页面.但是前后端分离的项目 ...

  5. Spring Boot + Vue + Shiro 实现前后端分离、权限控制

    本文总结自实习中对项目的重构.原先项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Freemarker模版的ModelA ...

  6. Springboot + Vue + shiro 实现前后端分离、权限控制

    本文总结自实习中对项目对重构.原先项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Freemarker模版的ModelA ...

  7. 使用 Nginx 部署前后端分离项目,解决跨域问题

    前后端分离这个问题其实松哥和大家聊过很多了,上周松哥把自己的两个开源项目部署在服务器上以帮助大家可以快速在线预览(喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了 ...

  8. 七个开源的 Spring Boot 前后端分离项目,一定要收藏!

    前后端分离已经在慢慢走进各公司的技术栈,根据松哥了解到的消息,不少公司都已经切换到这个技术栈上面了.即使贵司目前没有切换到这个技术栈上面,松哥也非常建议大家学习一下前后端分离开发,以免在公司干了两三年 ...

  9. 基于Vue的前后端分离项目实践

    一.为什么需要前后端分离 1.1什么是前后端分离  前后端分离这个词刚在毕业(15年)那会就听说过,但是直到17年前都没有接触过前后端分离的项目.怎么理解前后端分离?直观的感觉就是前后端分开去做,即功 ...

随机推荐

  1. CodeForces 931C Laboratory Work 水题,构造

    *这种题好像不用写题解... 题意: 一个人要改动别人的实验记录,实验记录记录是一个集合 实验记录本身满足:$max(X)-min(X)<=2$ 改动结果要求: 1.新的集合平均值和之前的一样 ...

  2. <第一站>人生的第一个博客

    在畅畅的疯狂暗示下(“最好”建个博客),我决定在博客园开通我的博客,在此记入我从3月23起的所学所想.在他的提醒之前,我曾经断断续续的在日记本上,手机备忘录里记录过我的学习情况和心路历程,总的来说,自 ...

  3. mina使用总结

    1.在会话中获得远程IP和端口 @Override public void messageReceived(IoSession session, Object message) throws Exce ...

  4. linux /proc目录

    1. /proc目录Linux 内核提供了一种通过 /proc 文件系统,在运行时访问内核内部数据结构.改变内核设置的机制.proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间.它以文 ...

  5. Light OJ 1012

    经典搜索水题...... #include<bits/stdc++.h> using namespace std; const int maxn = 20 + 13; const int ...

  6. [转]ASCII码表及扩展ASCII码表,方便查阅

    ASCII码表可以看成由三部分组成: 第一部分:由00H到1FH共32个,一般用来通讯或作为控制之用.有些可以显示在屏幕上,有些则不能显示,但能看到其效果(如换行.退格).如下表: 第二部分:是由20 ...

  7. 【进阶1-5期】JavaScript深入之4类常见内存泄漏及如何避免(转)

    这是我在公众号(高级前端进阶)看到的文章,现在做笔记 https://mp.weixin.qq.com/s/RZ8Lpkyk8lz6z5H8Q8SiEQ 垃圾回收算法 常用垃圾回收算法叫做**标记清除 ...

  8. 一个完整Java Web项目背后的密码

    前言 最近自己做了几个Java Web项目,有公司的商业项目,也有个人做着玩的小项目,写篇文章记录总结一下收获,列举出在做项目的整个过程中,所需要用到的技能和知识点,带给还没有真正接触过完整Java ...

  9. STM32L476应用开发之八:便携式气体分析仪项目总结

    在本次项目中,我们实现的实际上是2套设备:便携式氧气分析仪以及便携式甲烷分析仪.但这两台仪器实际使用的主控板我们是设计了一套,所以主控板是适合于这两个设备的. 1.硬件设计 便携式气体分析仪的功能比较 ...

  10. Confluence 6 审查日志

    日志审查能够允许管理查看你 Confluence 站点所做的修改.这个在你希望对你的 Confluence 进行问题查看或者是你希望对你 Confluence 保留重要的修改事件,例如修改了全局权限. ...