这是我的previous post "Spring Security and Non-flat Roles Inheritance Architecture"的一小部分。在本文中,我告诉您:
- 如何直接从代码中生成弹簧安全授权规则的文档?
- 如何在GitHub Pages上托管结果HTML页面?
这种文档对各种专家很有用。系统和业务分析师希望了解请求处理背后的逻辑。当质量工程师检查端点时,验证了任务中所述的访问。这两个类别都将从始终相关的文档中受益。
您可以找到整个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。
算法
这是文档生成的整个想法:
- 有一个单独的
DocsTest
启动了整个弹簧应用程序。 - 运行测试时,将结果HTML页面生成
build/classes/test
目录。 - 最后,在GitHub管道执行期间,我们在GitHub Pages上托管了HTML页面。
生成步骤
看下面的基本设置。
class DocsTest extends AbstractControllerTest {
@Autowired
private ApplicationContext context;
...
}
AbstractControllerTest
启动了使用TestContainers的PostgreSQL。您可以找到其源代码by this link。
我使用ApplicationContext
bean解决注册的REST控制器。
我们需要从静止控制器上的注释中解析哪些信息?这是列表:
- 控制器的名称
- 有关每个端点的详细信息
- http方法
- API路径
- 从
@PreAuthorize
注释中解析的安全性spel表达式。 - 映射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);
}
...
}
这是逐步发生的事情:
- 我检索了标有
@RestController
注释的所有bean名称。 - 然后我以其名称获得当前的控制器bean。
- 之后,我解析了基本API路径。
- 最后,我遍历控制器内部的每种方法并解析有关它的信息。
查看下面的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()
));
}
在这种情况下,我正在尝试从该方法中获取可能的请求映射注释:GetMapping
,PostMapping
,DeleteMapping
或PutMapping
。然后,我通过调用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
这是发生的事情:
-
Set up JDK 17
和Build with Gradle
执行常规的Gradle构建操作。 - 然后是
Upload artifact
,它将包含HTML文档的目录保存到GitHub注册表中。 - 最后,我们将先前存储的工件部署到github页面。
基本上就是这样。您可以通过this link查看生成的HTML页面。最酷的事情是,您不必手动编写文档。因此,它总是相关的,因为您直接从代码中生成内容。
结论
我想告诉您有关记录春季安全应用程序并将HTML结果存储在GitHub页面上的所有信息。您在项目中生成任何文档吗?如果是这样,它是什么样的文档?在评论中讲述您的故事。感谢您的阅读!