Передняя часть имеет меню (меню), задняя часть имеет API (backendApi), а страница, соответствующая меню, поддерживается интерфейсами API N. В этой статье рассказывается, как реализовать управление разрешениями на синхронизацию для передней и задней частей. на основе весенней безопасности.
Реализовать идеи
Он по-прежнему реализуется на основе роли.Конкретная идея заключается в том, что роль имеет несколько меню, а меню имеет несколько бэкендапи, из которых роль и меню, а также меню и бэкендапи являются отношениями ManyToMany.
Аутентификация и авторизация также очень просты.Когда пользователь входит в систему, он получает меню, связанное с ролью.Когда страница обращается к внутреннему API, затем проверяется, есть ли у пользователя разрешение на доступ к API.
Определение домена
Мы используем JPA для достижения, сначала определить роль
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<>();
}
скопировать код
и меню:
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, который различает метод (метод запроса HTTP), тег (какой контроллер) и путь (путь запроса 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<>();
}
скопировать код
Реализация страницы администратора
Меню меню определяется потребностями в бизнесе, поэтому достаточно для обеспечения редактирования CRUD.
Backendapi может быть получен с помощью чванство.
Внешний интерфейс выбирает ng-algin, см.Решение Angular для среднего и внешнего интерфейса — введение Нг Алена
Получить BackendAPI через swagger
Есть много способов получить swagger api. Самый простой способ — получить доступ к интерфейсу http, чтобы получить json, а затем проанализировать его. Это очень просто, и я не буду здесь вдаваться в подробности. Другой способ — напрямую вызвать соответствующий API для получения объекта 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);
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);
скопировать код
Документацию по кешу, окружению, мапперу и т. д. можно получить напрямую через 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, созданный с помощью группировки тегов, множественный выбор:
страница со списком
управление ролями
Обыкновенный CRUD, самое главное добавить страницу авторизации меню, причем меню можно отображать слоями:
Реализация аутентификации
Страница управления может быть разного вида, и самое главное, как реализовать аутентификацию.
в предыдущей статьеБезопасность Spring - это два способа получить права URL-адреса динамической конфигурации.Как мы уже говорили, его можно настроитьFilterInvocationSecurityMetadataSource
реализовать.
выполнитьFilterInvocationSecurityMetadataSource
Интерфейса достаточно.Ядро состоит в том, чтобы получить соответствующую роль в соответствии с методом и путем запроса FilterInvocation, а затем передать ее RoleVoter, чтобы определить, есть ли у него разрешение.
Пользовательский фильтрInvocationSecurityMetadataSource
Мы создаем новый 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 (удалить параметры), а затем посмотреть, есть ли соответствующий бэкенд API, если нет, то возможно, что в API есть параметр пути, мы можем удалить последний путь, идем в библиотеку для нечеткого сопоставления, пока не появится.
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);
}
}
скопировать код
Использовать в конфигурации SecurityConfigurationDaoSecurityMetadataSource
, и пройтиApplicationContext.registerBean
будет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;
}
скопировать код
Наконец, после запуска программы, перейдитеApplicationContext.getBean
Получите daoSecurityMetadataSource, затем вызовите init для внедрения репозитория
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();
}
скопировать код
Готово!
дальнейшее чтение
- Spring Security реализует два метода динамической настройки разрешений URL.
- Архитектура безопасности Spring и анализ исходного кода
Добавить Автора
Источник: технический блокнот jqpeng.www.cnblogs.com/xiaoqi
Ваша поддержка — величайшее поощрение для блоггеров, спасибо за внимательное чтение.
Авторские права на эту статью принадлежат автору, и вы можете ее перепечатать, но это заявление должно быть сохранено без согласия автора, а ссылка на исходный текст дана в видном месте на странице статьи, в противном случае право преследовать юридическую ответственность зарезервировано.