在构建需要用户认证和授权的 Web 应用时,FreeMarker 作为模板引擎,需要与安全框架(如 Apache Shiro 或 Spring Security)深度集成,以实现基于角色/权限的 UI 控制。例如,根据用户是否登录、拥有特定角色或权限来显示或隐藏菜单项、按钮或敏感信息。本指南将详细介绍如何将 Apache Shiro 和 Spring Security 的标签集成到 FreeMarker 模板中,涵盖核心概念、详细操作步骤、常见错误、注意事项、使用技巧及最佳实践。
1. 核心概念
- 安全标签库 (Security Tag Library): 提供一组自定义 FreeMarker 指令(
<@shiro.xxx>
,<@sec.xxx>
),允许在模板中执行安全相关的逻辑判断。 - Apache Shiro: 一个强大且易用的 Java 安全框架,提供认证(Authentication)、授权(Authorization)、加密(Cryptography)和会话管理(Session Management)功能。
- Spring Security: Spring 生态中的标准安全框架,功能极其丰富,深度集成 Spring,提供全面的安全解决方案。
Shiro-Taglib-Freemarker
: 官方或社区提供的库,将 Shiro 的shiro-tags
转换为 FreeMarker 可用的指令。Spring Security Tag Library
+Freemarker-Spring
: 利用 Spring 的freemarker-spring
模块,将 Spring Security 的 JSP 标签(sec:
)桥接到 FreeMarker。Configuration
与Shared Variables
: FreeMarker 的Configuration
对象通过setSharedVariable
方法注册安全标签指令,使其在所有模板中全局可用。- 数据模型 (Data Model) 注入: 安全标签需要访问当前用户(Subject/Principal)和其权限信息,这通常由 Web 框架(如 Spring MVC)在请求处理过程中自动注入到数据模型或通过
RequestContext
获取。 - 指令 (Directive): FreeMarker 中的自定义标签,语法为
<@directiveName param=value />
或<@directiveName param=value>...</@directiveName>
。安全标签库提供如guest
,user
,principal
,hasRole
,hasPermission
等指令。
2. 操作步骤 (非常详细)
我们将分别介绍 Apache Shiro 和 Spring Security 的集成。
Part A: 集成 Apache Shiro 标签
步骤 1: 添加 Maven 依赖
在 pom.xml
中添加 Shiro 核心、Web 模块和 FreeMarker 集成库。
<!-- pom.xml -->
<dependencies>
<!-- Apache Shiro Core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.13.0</version>
</dependency>
<!-- Apache Shiro Web (用于 Web 应用) -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.13.0</version>
</dependency>
<!-- FreeMarker 核心 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
<!-- 【关键】Shiro FreeMarker 标签库 -->
<!-- 注意:Shiro 官方没有单独的 "shiro-freemarker" 模块,但社区有成熟方案 -->
<!-- 方案1:使用 shiro-all (包含所有模块,较重) -->
<!-- <dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.13.0</version>
<type>pom</type>
</dependency> -->
<!-- 方案2:推荐 - 使用专门的社区库或自行实现 -->
<!-- 这里我们使用一个广泛使用的社区实现思路:创建自定义指令 -->
<!-- 或者,可以查找如 'shiro-freemarker-tags' 等第三方库 (需自行搜索确认可用性) -->
<!-- 本指南将演示如何创建自定义 Shiro 指令 -->
<!-- 假设我们使用一个名为 'shiro-freemarker' 的第三方库 (示例坐标) -->
<!-- <dependency>
<groupId>com.github.theatrus</groupId>
<artifactId>shiro-freemarker</artifactId>
<version>1.0.0</version>
</dependency> -->
<!-- 方案3:最佳实践 - 自行实现 (更可控) -->
<!-- 我们将采用此方案,创建自己的 Shiro 指令 -->
</dependencies>
步骤 2: 实现自定义 Shiro 指令 (推荐方式)
创建 FreeMarker 指令来封装 Shiro 的 API 调用。
// security/shiro/GuestDirective.java
package security.shiro;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import java.io.IOException;
import java.util.Map;
/**
* FreeMarker 指令: <@shiro.guest>...</@shiro.guest>
* 如果当前用户是游客(未认证),则执行 body。
*/
public class GuestDirective implements TemplateDirectiveModel {
public static final String DIRECTIVE_NAME = "guest";
@Override
public void execute(Environment env,
Map params,
TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
// 1. 获取当前 Subject
Subject subject = SecurityUtils.getSubject();
// 2. 检查是否为游客 (未认证)
boolean isGuest = subject == null || !subject.isAuthenticated();
// 3. 如果是游客,则执行模板体
if (isGuest && body != null) {
body.render(env.getOut());
}
}
}
// security/shiro/UserDirective.java
package security.shiro;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import java.io.IOException;
import java.util.Map;
/**
* FreeMarker 指令: <@shiro.user>...</@shiro.user>
* 如果当前用户是已认证用户(非游客),则执行 body。
*/
public class UserDirective implements TemplateDirectiveModel {
public static final String DIRECTIVE_NAME = "user";
@Override
public void execute(Environment env,
Map params,
TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
Subject subject = SecurityUtils.getSubject();
boolean isUser = subject != null && subject.isAuthenticated();
if (isUser && body != null) {
body.render(env.getOut());
}
}
}
// security/shiro/PrincipalDirective.java
package security.shiro;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.utility.DeepUnwrap;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import java.io.IOException;
import java.util.Map;
/**
* FreeMarker 指令: <@shiro.principal property="username"/>
* 获取当前用户的 Principal 信息。
* 支持 property 参数获取特定属性。
*/
public class PrincipalDirective implements TemplateDirectiveModel, TemplateScalarModel {
public static final String DIRECTIVE_NAME = "principal";
public static final String PARAM_PROPERTY = "property";
@Override
public void execute(Environment env,
Map params,
TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
Subject subject = SecurityUtils.getSubject();
if (subject == null) {
if (body != null) body.render(env.getOut());
return;
}
PrincipalCollection principals = subject.getPrincipals();
if (principals == null) {
if (body != null) body.render(env.getOut());
return;
}
String property = (String) params.get(PARAM_PROPERTY);
String value = null;
if (property != null) {
// 尝试获取特定属性 (需要 Realm 支持)
value = principals.getPrimaryPrincipal().toString(); // 简化处理,实际需根据 Realm 实现
// 更好的方式是通过 principals.oneByType(...) 或自定义逻辑
} else {
// 获取主 Principal
value = principals.getPrimaryPrincipal().toString();
}
if (value != null && body != null) {
// 将值作为字符串输出
env.getOut().write(value);
}
}
// 实现 TemplateScalarModel 以便在 ${} 中使用
@Override
public String getAsString() throws TemplateModelException {
Subject subject = SecurityUtils.getSubject();
if (subject == null) return "";
PrincipalCollection principals = subject.getPrincipals();
if (principals == null) return "";
return principals.getPrimaryPrincipal().toString();
}
}
// security/shiro/HasRoleDirective.java
package security.shiro;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.utility.DeepUnwrap;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import java.io.IOException;
import java.util.Map;
/**
* FreeMarker 指令: <@shiro.hasRole name="admin">...</@shiro.hasRole>
* 如果当前用户拥有指定角色,则执行 body。
*/
public class HasRoleDirective implements TemplateDirectiveModel {
public static final String DIRECTIVE_NAME = "hasRole";
public static final String PARAM_NAME = "name";
@Override
public void execute(Environment env,
Map params,
TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
Subject subject = SecurityUtils.getSubject();
if (subject == null || !subject.isAuthenticated()) {
if (body != null) body.render(env.getOut());
return;
}
String roleName = (String) DeepUnwrap.unwrap(params.get(PARAM_NAME));
if (roleName == null || roleName.trim().isEmpty()) {
throw new TemplateException("The 'name' parameter is required for shiro.hasRole", env);
}
boolean hasRole = subject.hasRole(roleName.trim());
if (hasRole && body != null) {
body.render(env.getOut());
}
}
}
// security/shiro/HasPermissionDirective.java
package security.shiro;
import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.utility.DeepUnwrap;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import java.io.IOException;
import java.util.Map;
/**
* FreeMarker 指令: <@shiro.hasPermission name="user:create">...</@shiro.hasPermission>
* 如果当前用户拥有指定权限,则执行 body。
*/
public class HasPermissionDirective implements TemplateDirectiveModel {
public static final String DIRECTIVE_NAME = "hasPermission";
public static final String PARAM_NAME = "name";
@Override
public void execute(Environment env,
Map params,
TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException {
Subject subject = SecurityUtils.getSubject();
if (subject == null || !subject.isAuthenticated()) {
if (body != null) body.render(env.getOut());
return;
}
String permissionName = (String) DeepUnwrap.unwrap(params.get(PARAM_NAME));
if (permissionName == null || permissionName.trim().isEmpty()) {
throw new TemplateException("The 'name' parameter is required for shiro.hasPermission", env);
}
boolean isPermitted = subject.isPermitted(permissionName.trim());
if (isPermitted && body != null) {
body.render(env.getOut());
}
}
}
步骤 3: 创建 Shiro 标签命名空间
将所有指令组织在一个 TemplateHashModel
中,形成 shiro
命名空间。
// security/shiro/ShiroTags.java
package security.shiro;
import freemarker.template.SimpleHash;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import java.util.HashMap;
import java.util.Map;
/**
* Shiro 标签命名空间,提供 <@shiro.xxx> 指令。
*/
public class ShiroTags extends SimpleHash implements TemplateHashModel {
private static final Map<String, Class<? extends TemplateDirectiveModel>> DIRECTIVES;
static {
DIRECTIVES = new HashMap<>();
DIRECTIVES.put(GuestDirective.DIRECTIVE_NAME, GuestDirective.class);
DIRECTIVES.put(UserDirective.DIRECTIVE_NAME, UserDirective.class);
DIRECTIVES.put(PrincipalDirective.DIRECTIVE_NAME, PrincipalDirective.class);
DIRECTIVES.put(HasRoleDirective.DIRECTIVE_NAME, HasRoleDirective.class);
DIRECTIVES.put(HasPermissionDirective.DIRECTIVE_NAME, HasPermissionDirective.class);
// 可以添加更多,如 hasAnyRoles, lacksRole, authenticated, notAuthenticated 等
}
public ShiroTags() throws TemplateModelException {
super();
// 动态创建指令实例并放入命名空间
for (Map.Entry<String, Class<? extends TemplateDirectiveModel>> entry : DIRECTIVES.entrySet()) {
try {
TemplateDirectiveModel directive = entry.getValue().newInstance();
put(entry.getKey(), directive);
} catch (InstantiationException | IllegalAccessException e) {
throw new TemplateModelException("Could not instantiate directive: " + entry.getKey(), e);
}
}
}
}
步骤 4: 配置 FreeMarker 注入 Shiro 标签
在 FreeMarker 配置中,将 ShiroTags
实例注册为共享变量。
// FreeMarkerConfig.java (Shiro 版本)
import freemarker.template.Configuration;
import freemarker.template.TemplateExceptionHandler;
import security.shiro.ShiroTags; // 引入自定义命名空间
import java.io.IOException;
public class FreeMarkerConfig {
private Configuration configuration;
public FreeMarkerConfig() throws IOException {
configuration = new Configuration(Configuration.VERSION_2_3_32);
configuration.setClassForTemplateLoading(getClass(), "/templates/");
configuration.setDefaultEncoding("UTF-8");
configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
configuration.setLogTemplateExceptions(false);
configuration.setWrapUncheckedExceptions(true);
// 【关键】注入 Shiro 标签命名空间
try {
ShiroTags shiroTags = new ShiroTags();
configuration.setSharedVariable("shiro", shiroTags); // 模板中使用 <@shiro.xxx>
System.out.println("Successfully registered 'shiro' tag namespace.");
} catch (Exception e) {
throw new RuntimeException("Failed to register Shiro tags", e);
}
}
public Configuration getConfiguration() {
return configuration;
}
}
步骤 5: 在模板中使用 Shiro 标签
<!-- templates/dashboard.ftl -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
</head>
<body>
<!-- 1. 区分游客和用户 -->
<@shiro.guest>
<p>Welcome, Guest! <a href="/login">Login</a> or <a href="/register">Register</a>.</p>
</@shiro.guest>
<@shiro.user>
<p>Hello, <@shiro.principal />! <a href="/logout">Logout</a>.</p>
<!-- 或者使用 ${shiro.principal} 获取主 Principal -->
<!-- <p>Hello, ${shiro.principal}!</p> -->
</@shiro.user>
<!-- 2. 基于角色显示菜单 -->
<@shiro.hasRole name="admin">
<nav>
<ul>
<li><a href="/admin/users">Manage Users</a></li>
<li><a href="/admin/settings">System Settings</a></li>
</ul>
</nav>
</@shiro.hasRole>
<@shiro.hasRole name="editor">
<nav>
<ul>
<li><a href="/editor/articles">Manage Articles</a></li>
<li><a href="/editor/publish">Publish Content</a></li>
</ul>
</nav>
</@shiro.hasRole>
<!-- 3. 基于权限显示按钮 -->
<div class="article-actions">
<@shiro.hasPermission name="article:edit">
<button onclick="editArticle()">Edit</button>
</@shiro.hasPermission>
<@shiro.hasPermission name="article:delete">
<button onclick="deleteArticle()">Delete</button>
</@shiro.hasPermission>
</div>
<!-- 4. 显示用户信息 (仅对已认证用户) -->
<@shiro.user>
<div class="user-profile">
<p>Username: <@shiro.principal property="username"/></p>
<!-- 注意:property 需要 Realm 正确实现 PrincipalCollection -->
<p>Email: <@shiro.principal property="email"/></p>
</div>
</@shiro.user>
</body>
</html>
Part B: 集成 Spring Security 标签
Spring Security 的标签库原生为 JSP 设计,但可以通过 freemarker-spring
模块在 FreeMarker 中使用。
步骤 1: 添加 Maven 依赖
<!-- pom.xml -->
<dependencies>
<!-- Spring Framework Core (通常由 spring-boot-starter-web 提供) -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.31</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.31</version>
</dependency>
<!-- Spring Security Core -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>5.8.12</version>
</dependency>
<!-- Spring Security Web (用于 Web 集成) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.8.12</version>
</dependency>
<!-- Spring Security Config (用于配置) -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.8.12</version>
</dependency>
<!-- FreeMarker 核心 -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
<!-- 【关键】Spring Framework 集成 FreeMarker -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.31</version>
</dependency>
<!-- 或者使用 Spring Boot Starter -->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency> -->
<!-- 【关键】Spring Security Tag Library -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>5.8.12</version>
</dependency>
</dependencies>
步骤 2: 配置 Spring Security (Java Config)
// SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout.permitAll());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user, admin);
}
}
步骤 3: 配置 FreeMarker 与 Spring 集成
使用 Spring 的 FreeMarkerConfigurer
或 FreeMarkerConfigurationFactoryBean
来配置 FreeMarker,并自动集成 Spring Security 标签。
// WebConfig.java (Spring MVC Configuration)
import freemarker.template.TemplateException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import java.io.IOException;
import java.util.Properties;
@Configuration
public class WebConfig {
// 配置 FreeMarker
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
// 设置模板位置
configurer.setTemplateLoaderPath("/templates/");
// 设置默认编码
configurer.setDefaultEncoding("UTF-8");
// 【关键】配置 FreeMarker Settings
Properties settings = new Properties();
settings.setProperty("template_exception_handler", "rethrow");
settings.setProperty("log_template_exceptions", "false");
settings.setProperty("wrap_unchecked_exceptions", "true");
configurer.setFreemarkerSettings(settings);
// 【关键】注入 Spring Security 标签
// Spring 会自动检测 spring-security-taglibs 并注册 sec 命名空间
// 无需手动 setSharedVariable,由 spring-context-support 内部处理
return configurer;
}
// 配置视图解析器
@Bean
public FreeMarkerViewResolver viewResolver() {
FreeMarkerViewResolver resolver = new FreeMarkerViewResolver();
resolver.setPrefix("");
resolver.setSuffix(".ftl");
resolver.setContentType("text/html;charset=UTF-8");
resolver.setRequestContextAttribute("rc"); // 允许在模板中使用 rc (RequestContext)
return resolver;
}
}
步骤 4: 在模板中使用 Spring Security 标签
<!-- templates/sec-dashboard.ftl -->
<!DOCTYPE html>
<html lang="en"
xmlns:sec="http://www.springframework.org/security/tags"> <!-- 声明命名空间 (可选,但推荐) -->
<head>
<meta charset="UTF-8">
<title>Spring Security Dashboard</title>
</head>
<body>
<!-- 1. 获取认证信息 -->
<div sec:authorize="isAuthenticated()">
<p>Hello, <span sec:authentication="name"></span>!
<a href="#" th:href="@{/logout}">Logout</a></p>
<!-- 注意:th:href 是 Thymeleaf 语法,FreeMarker 中用 ${rc.getContextPath()}/logout -->
<p>Roles: <span sec:authentication="principal.authorities"></span></p>
</div>
<div sec:authorize="isAnonymous()">
<p>Welcome, Guest! <a href="${rc.getContextPath()}/login">Login</a>.</p>
</div>
<!-- 2. 基于权限/角色显示内容 -->
<div sec:authorize="hasRole('ADMIN')">
<nav>
<ul>
<li><a href="${rc.getContextPath()}/admin/users">Manage Users (Admin)</a></li>
<li><a href="${rc.getContextPath()}/admin/settings">System Settings</a></li>
</ul>
</nav>
</div>
<div sec:authorize="hasAnyRole('USER', 'ADMIN')">
<nav>
<ul>
<li><a href="${rc.getContextPath()}/user/profile">My Profile</a></li>
<li><a href="${rc.getContextPath()}/user/articles">My Articles</a></li>
</ul>
</nav>
</div>
<!-- 3. 基于权限显示按钮 -->
<div class="article-actions">
<button sec:authorize="hasPermission(#articleId, 'article', 'edit')"
onclick="editArticle(${articleId})">Edit</button>
<button sec:authorize="hasPermission(#articleId, 'article', 'delete')"
onclick="deleteArticle(${articleId})">Delete</button>
<!-- 注意:#articleId 需要在数据模型中提供 -->
</div>
<!-- 4. 使用表达式 -->
<div sec:authorize="hasRole('PREMIUM_USER') and authentication.principal.accountNonExpired">
<p>Welcome Premium User! Your account is active.</p>
</div>
<!-- 5. 显示用户名 (另一种方式) -->
<p>User: <sec:authentication property="principal.username" /></p>
</body>
</html>
重要说明 (Spring Security Tags in FreeMarker):
- Spring Security 的标签(
sec:authorize
,sec:authentication
)是 XML 属性,不是 FreeMarker 指令。 - 它们在模板解析后、渲染前由 Spring Security 的
Filter
或View
处理。 - 因此,你不需要像 Shiro 那样手动创建
TemplateDirectiveModel
或注册共享变量。freemarker-spring
和spring-security-taglibs
的组合会自动完成集成。 - 在 FreeMarker 模板中,这些标签直接作为 HTML 属性使用。
rc
(RequestContext) 是 Spring 注入的变量,用于访问请求上下文信息(如上下文路径getContextPath()
)。
3. 常见错误
Apache Shiro
Expression shiro is undefined
:- 原因:
ShiroTags
实例未通过configuration.setSharedVariable("shiro", ...)
成功注册。 - 解决: 检查
FreeMarkerConfig
是否执行了注册代码,Configuration
实例是否正确传递。
- 原因:
TemplateModelException
during registration:- 原因: 创建
ShiroTags
或其内部指令实例时出错(如SecurityUtils
未初始化)。 - 解决: 确保 Shiro 的
SecurityManager
已经在 Web 应用启动时正确配置和初始化(通常通过ShiroFilter
或Spring
配置)。
- 原因: 创建
NullPointerException
in directive:- 原因:
SecurityUtils.getSubject()
返回null
,通常因为 Shiro 的SecurityManager
未正确设置,或当前线程没有绑定Subject
。 - 解决: 确认 Shiro 的过滤器(如
ShiroFilter
)已正确配置并拦截了请求。
- 原因:
Spring Security
sec:authorize
标签不生效,始终显示或隐藏:- 原因:
spring-security-taglibs
依赖未添加,或FreeMarkerConfigurer
未正确配置(但通常自动)。 - 解决: 确认
spring-security-taglibs
在 classpath。检查WebSecurityConfig
是否正确。确保视图解析器返回的是FreeMarkerView
。
- 原因:
rc
变量未定义:- 原因:
FreeMarkerViewResolver.setRequestContextAttribute("rc")
未设置,或设置的属性名不匹配。 - 解决: 在
FreeMarkerViewResolver
配置中设置requestContextAttribute
。
- 原因:
hasPermission
表达式不工作:- 原因:
PermissionEvaluator
未配置,或#articleId
等变量未在数据模型中提供。 - 解决: 确保
PermissionEvaluator
Bean 已定义(通常需要自定义实现)。确认数据模型包含articleId
等变量。
- 原因:
4. 注意事项
- Shiro vs Spring Security: Spring Security 与 Spring 生态集成更深,功能更强大,是 Spring 项目的首选。Shiro 更轻量,可独立使用。
- Shiro 集成方式: Shiro 官方未提供直接的 FreeMarker 支持,自定义指令是可靠且可控的方式。避免使用过时或不维护的第三方库。
- Spring Security 标签机制: 理解其基于 XML 属性和后处理的机制,与 FreeMarker 原生指令不同。
- 性能: 安全检查(尤其是
hasPermission
)可能涉及数据库查询。确保权限信息被合理缓存(如 Shiro 的CacheManager
,Spring Security 的缓存)。 - 安全性: UI 级别的隐藏不能替代后端的权限验证!所有关键操作必须在 Controller/Service 层再次进行权限检查。
- 错误处理: 在自定义 Shiro 指令中,妥善处理
TemplateException
和IOException
。 - 线程安全:
TemplateDirectiveModel
实例是共享的,确保其无状态或状态是线程安全的。 - 命名空间: 使用清晰的命名空间(
shiro
,sec
)避免冲突。
5. 使用技巧
- Shiro 指令复用: 为
hasAnyRoles
,lacksRole
,authenticated
,notAuthenticated
等创建更多指令。 - Spring Security 表达式: 熟练掌握 SpEL 表达式,如
hasRole('ADMIN')
,hasAuthority('USER_READ')
,hasPermission(#id, 'Message')
,isAuthenticated()
,isAnonymous()
,isRememberMe()
。 - 结合
rc
: 在 Spring Security 模板中,充分利用rc
访问请求信息。 - 调试: 在 Shiro 指令中添加日志,观察
Subject
状态。在 Spring Security 中,开启 DEBUG 日志查看权限决策过程。 - 文档: 为自定义的 Shiro 指令编写清晰的文档。
6. 最佳实践与性能优化
最佳实践:
- 后端验证: 始终在服务端进行最终的权限验证。
- 最小权限: 遵循最小权限原则设计角色和权限。
- 清晰命名: 权限名清晰(如
user:create
,order:delete
)。 - 缓存: 启用 Shiro 或 Spring Security 的权限缓存。
- 选择合适框架: Spring 项目优先选 Spring Security;非 Spring 或需要轻量级方案可选 Shiro。
- Shiro 指令: 将自定义指令打包成独立模块,便于复用。
性能优化:
- 缓存权限: 这是最关键的优化。确保
Realm
的getAuthorizationInfo
结果被缓存。 - 减少昂贵检查: 避免在循环中频繁调用
hasRole
/hasPermission
。将结果提取到循环外。 - 预加载数据: 在 Controller 层预加载用户权限信息,并放入数据模型,供模板直接使用(减少模板中的方法调用)。
- 监控: 监控权限检查的耗时。
- 缓存权限: 这是最关键的优化。确保
通过以上详细步骤,你可以成功地将 Apache Shiro 或 Spring Security 的安全控制集成到 FreeMarker 模板中,实现动态、安全的用户界面。记住,前端隐藏只是用户体验,真正的安全防线在后端。