春季安全生成授权规则的文档
#编程 #教程 #java #documentation

这是我的previous post "Spring Security and Non-flat Roles Inheritance Architecture"的一小部分。在本文中,我告诉您:

  1. 如何直接从代码中生成弹簧安全授权规则的文档?
  2. 如何在GitHub Pages上托管结果HTML页面?

这种文档对各种专家很有用。系统和业务分析师希望了解请求处理背后的逻辑。当质量工程师检查端点时,验证了任务中所述的访问。这两个类别都将从始终相关的文档中受益。

Meme cover

您可以找到整个generator's code by this link。查看下面生成的文档的示例。

http方法 api路径 安全检查规则 控制器功能名称
post /api/community isauthenticated() createCommunity
post /api/community/{communityId}/post @roleservice.hasanyrolebybbycommunityid(#communityid, @communityRole.admin) createpost
put /api/post/{posts} @roleservice.hasanyrobybypost(#posts, @postrole.editor) UpdatePost
get /api/post/{posts} @roleservice.hasanyrobybypost(#posts, @postrole.viewer) getPost

我将布局作为标记表,以使其更易于阅读。无论如何,您可以查看the rendered HTML by this link

算法

这是文档生成的整个想法:

  1. 有一个单独的DocsTest启动了整个弹簧应用程序。
  2. 运行测试时,将结果HTML页面生成build/classes/test目录。
  3. 最后,在GitHub管道执行期间,我们在GitHub Pages上托管了HTML页面。

生成步骤

看下面的基本设置。

class DocsTest extends AbstractControllerTest {
    @Autowired
    private ApplicationContext context;

    ...
}

AbstractControllerTest启动了使用TestContainers的PostgreSQL。您可以找到其源代码by this link

我使用ApplicationContext bean解决注册的REST控制器。

我们需要从静止控制器上的注释中解析哪些信息?这是列表:

  1. 控制器的名称
  2. 有关每个端点的详细信息
    1. http方法
    2. API路径
    3. @PreAuthorize注释中解析的安全性spel表达式。
    4. 映射HTTP请求的Java方法的名称。

查看保存着所述点的Java记录:

@With
private record ControllerInfo(
    String name,
    List<MethodInfo> methods
) {}

@With
private record MethodInfo(
    String httpMethod,
    String apiPath,
    String security,
    String functionName
) {}

现在是时候穿越现有控制器并解析所需数据了。查看下面的代码段:

@Test
void generateDocs() throws Exception {
    final var controllers = new ArrayList<ControllerInfo>();
    for (String controllerName : context.getBeanNamesForAnnotation(RestController.class)) {
        final var controllerBean = context.getBean(controllerName);
        final var baseApiPath = getApiPath(AnnotationUtils.findAnnotation(controllerBean.getClass(), RequestMapping.class));
        final var controllerSecurityInfo = new ControllerInfo(
            StringUtils.capitalize(controllerName),
            new ArrayList<>()
        );
        for (Method method : controllerBean.getClass().getMethods()) {
            getMethodInfo(method)
                .map(m -> m.withPrefixedApiPath(baseApiPath))
                .ifPresent(m -> controllerSecurityInfo.methods().add(m));
        }
        controllers.add(controllerSecurityInfo);
    }
    ...
}

这是逐步发生的事情:

  1. 我检索了标有@RestController注释的所有bean名称。
  2. 然后我以其名称获得当前的控制器bean。
  3. 之后,我解析了基本API路径。
  4. 最后,我遍历控制器内部的每种方法并解析有关它的信息。

查看下面的getMethodInfo声明。

private static Optional<MethodInfo> getMethodInfo(Method method) {
        return Optional.<Annotation>ofNullable(AnnotationUtils.findAnnotation(method, GetMapping.class))
                   .or(() -> ofNullable(AnnotationUtils.findAnnotation(method, PostMapping.class)))
                   .or(() -> ofNullable(AnnotationUtils.findAnnotation(method, DeleteMapping.class)))
                   .or(() -> ofNullable(AnnotationUtils.findAnnotation(method, PutMapping.class)))
                   .map(annotation -> AnnotationUtils.getAnnotationAttributes(method, annotation))
                   .map(attributes -> new MethodInfo(
                       attributes.annotationType()
                           .getSimpleName()
                           .replace("Mapping", "")
                           .toUpperCase(),
                       getApiPath(attributes.getStringArray("value")),
                       ofNullable(AnnotationUtils.findAnnotation(method, PreAuthorize.class))
                           .map(PreAuthorize::value)
                           .orElse(""),
                       method.getName()
                   ));
    }

在这种情况下,我正在尝试从该方法中获取可能的请求映射注释:GetMappingPostMappingDeleteMappingPutMapping。然后,我通过调用AnnotationUtils.getAnnotationAttributes获得注释的属性,最后将参数传递给MethodInfo构造函数。

getApiPath方法接受String...参数并在存在的情况下返回其第一个值。

创建HTML报告

现在,我们已经获得了有关端点的信息,现在该将其格式化为HTML页面了。查看下面的模板声明:

final var html = """
    <html>
    <head>
        <meta charset="UTF8">
        <style>
            body, table {
                font-family: "JetBrains Mono";
                font-size: 20px;
            }
            table, th, td {
              border: 1px solid black;
            }
        </style>
        <link href='https://fonts.googleapis.com/css?family=JetBrains Mono' rel='stylesheet'>
    </head>
    <body>
        <div>
            <h2>Endpoints role checking</h2>
            <div>{docs}</div>
        </div>
    </body>
    </html>
    """.replace("{docs}", toHtml(controllers));

writeFileToBuildFolder("index.html", html);

controllers变量表示我们以前构建的List<ControllerInfo>。功能toHtml将其转换为HTML片段。然后,我们用内容代替{docs}的占位符。

writeFileToBuildFolder函数将结果内容写入文件build/classes/java/test/index.html。您可以找到其声明by this link

查看下面的toHtml函数定义。

private static String toHtml(List<ControllerInfo> controllers) {
    StringBuilder docs = new StringBuilder();
    for (ControllerInfo controller : controllers) {
        docs.append("<b>")
            .append(controller.name())
            .append("</b>")
            .append("<br>")
            .append("<table>");

        for (MethodInfo method : controller.methods()) {
            docs.append("<tr>")
                .append("<td>").append(method.httpMethod()).append("</td>")
                .append("<td>").append(method.apiPath()).append("</td>")
                .append("<td>").append(method.security()).append("</td>")
                .append("<td>").append(method.functionName()).append("</td>")
                .append("</tr>");
        }
        docs.append("</table>")
            .append("----------------------------------<br>");
        }
    return docs.toString();
}

您可以看到,我只是为每个现有控制器创建一个HTML表,然后将它们连接到一个字符串中。

在github页面上托管文档

整个GitHub动作管道小于40行。看看下面的yaml。

name: Java CI with Gradle

on:
  push:
    branches: [ "master" ]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build-and-deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Build with Gradle
        run: ./gradlew build
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v1
        with:
          path: build/classes/java/test/
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v1

这是发生的事情:

  1. Set up JDK 17Build with Gradle执行常规的Gradle构建操作。
  2. 然后是Upload artifact,它将包含HTML文档的目录保存到GitHub注册表中。
  3. 最后,我们将先前存储的工件部署到github页面。

基本上就是这样。您可以通过this link查看生成的HTML页面。最酷的事情是,您不必手动编写文档。因此,它总是相关的,因为您直接从代码中生成内容。

结论

我想告诉您有关记录春季安全应用程序并将HTML结果存储在GitHub页面上的所有信息。您在项目中生成任何文档吗?如果是这样,它是什么样的文档?在评论中讲述您的故事。感谢您的阅读!

资源

  1. My previous post "Spring Security and Non-flat Roles Inheritance Architecture"
  2. GitHub Pages
  3. The entire generator code
  4. The rendered HTML page hosted on GitHub Pages
  5. AbstractControllerTest with Testcontainers setup
  6. Gradle