在构建需要用户认证和授权的 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。
  • ConfigurationShared 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 ShiroSpring 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 的 FreeMarkerConfigurerFreeMarkerConfigurationFactoryBean 来配置 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 的 FilterView 处理。
  • 因此,你不需要像 Shiro 那样手动创建 TemplateDirectiveModel 或注册共享变量。freemarker-springspring-security-taglibs 的组合会自动完成集成。
  • 在 FreeMarker 模板中,这些标签直接作为 HTML 属性使用。
  • rc (RequestContext) 是 Spring 注入的变量,用于访问请求上下文信息(如上下文路径 getContextPath())。

3. 常见错误

Apache Shiro

  1. Expression shiro is undefined:

    • 原因: ShiroTags 实例未通过 configuration.setSharedVariable("shiro", ...) 成功注册。
    • 解决: 检查 FreeMarkerConfig 是否执行了注册代码,Configuration 实例是否正确传递。
  2. TemplateModelException during registration:

    • 原因: 创建 ShiroTags 或其内部指令实例时出错(如 SecurityUtils 未初始化)。
    • 解决: 确保 Shiro 的 SecurityManager 已经在 Web 应用启动时正确配置和初始化(通常通过 ShiroFilterSpring 配置)。
  3. NullPointerException in directive:

    • 原因: SecurityUtils.getSubject() 返回 null,通常因为 Shiro 的 SecurityManager 未正确设置,或当前线程没有绑定 Subject
    • 解决: 确认 Shiro 的过滤器(如 ShiroFilter)已正确配置并拦截了请求。

Spring Security

  1. sec:authorize 标签不生效,始终显示或隐藏:

    • 原因: spring-security-taglibs 依赖未添加,或 FreeMarkerConfigurer 未正确配置(但通常自动)。
    • 解决: 确认 spring-security-taglibs 在 classpath。检查 WebSecurityConfig 是否正确。确保视图解析器返回的是 FreeMarkerView
  2. rc 变量未定义:

    • 原因: FreeMarkerViewResolver.setRequestContextAttribute("rc") 未设置,或设置的属性名不匹配。
    • 解决:FreeMarkerViewResolver 配置中设置 requestContextAttribute
  3. hasPermission 表达式不工作:

    • 原因: PermissionEvaluator 未配置,或 #articleId 等变量未在数据模型中提供。
    • 解决: 确保 PermissionEvaluator Bean 已定义(通常需要自定义实现)。确认数据模型包含 articleId 等变量。

4. 注意事项

  1. Shiro vs Spring Security: Spring Security 与 Spring 生态集成更深,功能更强大,是 Spring 项目的首选。Shiro 更轻量,可独立使用。
  2. Shiro 集成方式: Shiro 官方未提供直接的 FreeMarker 支持,自定义指令是可靠且可控的方式。避免使用过时或不维护的第三方库。
  3. Spring Security 标签机制: 理解其基于 XML 属性和后处理的机制,与 FreeMarker 原生指令不同。
  4. 性能: 安全检查(尤其是 hasPermission)可能涉及数据库查询。确保权限信息被合理缓存(如 Shiro 的 CacheManager,Spring Security 的缓存)。
  5. 安全性: UI 级别的隐藏不能替代后端的权限验证!所有关键操作必须在 Controller/Service 层再次进行权限检查。
  6. 错误处理: 在自定义 Shiro 指令中,妥善处理 TemplateExceptionIOException
  7. 线程安全: TemplateDirectiveModel 实例是共享的,确保其无状态或状态是线程安全的。
  8. 命名空间: 使用清晰的命名空间(shiro, sec)避免冲突。

5. 使用技巧

  1. Shiro 指令复用:hasAnyRoles, lacksRole, authenticated, notAuthenticated 等创建更多指令。
  2. Spring Security 表达式: 熟练掌握 SpEL 表达式,如 hasRole('ADMIN'), hasAuthority('USER_READ'), hasPermission(#id, 'Message'), isAuthenticated(), isAnonymous(), isRememberMe()
  3. 结合 rc: 在 Spring Security 模板中,充分利用 rc 访问请求信息。
  4. 调试: 在 Shiro 指令中添加日志,观察 Subject 状态。在 Spring Security 中,开启 DEBUG 日志查看权限决策过程。
  5. 文档: 为自定义的 Shiro 指令编写清晰的文档。

6. 最佳实践与性能优化

  1. 最佳实践:

    • 后端验证: 始终在服务端进行最终的权限验证。
    • 最小权限: 遵循最小权限原则设计角色和权限。
    • 清晰命名: 权限名清晰(如 user:create, order:delete)。
    • 缓存: 启用 Shiro 或 Spring Security 的权限缓存。
    • 选择合适框架: Spring 项目优先选 Spring Security;非 Spring 或需要轻量级方案可选 Shiro。
    • Shiro 指令: 将自定义指令打包成独立模块,便于复用。
  2. 性能优化:

    • 缓存权限: 这是最关键的优化。确保 RealmgetAuthorizationInfo 结果被缓存。
    • 减少昂贵检查: 避免在循环中频繁调用 hasRole/hasPermission。将结果提取到循环外。
    • 预加载数据: 在 Controller 层预加载用户权限信息,并放入数据模型,供模板直接使用(减少模板中的方法调用)。
    • 监控: 监控权限检查的耗时。

通过以上详细步骤,你可以成功地将 Apache Shiro 或 Spring Security 的安全控制集成到 FreeMarker 模板中,实现动态、安全的用户界面。记住,前端隐藏只是用户体验,真正的安全防线在后端