1. 概述
在企业级 Web 和移动应用开发中,安全(Security)是绕不开的核心问题。选择一个合适的安全框架,往往直接影响系统的可维护性与扩展性。
本文将对两个主流的 Java 安全框架 —— Apache Shiro 与 Spring Security 进行横向对比,从配置方式、使用习惯到生态支持,帮你快速判断哪个更适合你的项目。
✅ 适用读者:已有 Spring 或安全框架使用经验的开发者
❌ 不适合:第一次接触认证授权概念的初学者(建议先补基础)
2. 背景简介
Apache Shiro
- 起源于 2004 年,原名 JSecurity,2008 年进入 Apache 基金会
- 当前稳定版本为 1.5.3(截至本文写作时)
- 设计目标是简单、通用、可嵌入任意 Java 应用(Web、非 Web、Spring、非 Spring)
Spring Security
- 前身为 2003 年的 Acegi Security,2008 年正式成为 Spring 项目的一部分
- 当前 GA 版本为 5.3.2
- 深度集成 Spring 生态,强调“安全即切面”(security as cross-cutting concern)
共同能力
两者均提供:
- ✅ 认证(Authentication)
- ✅ 授权(Authorization)
- ✅ 加密(Cryptography)
- ✅ 会话管理(Session Management)
但 Spring Security 更进一步,原生支持 CSRF 防护、会话固定攻击防御、OAuth2、OpenID Connect 等企业级特性,这是 Shiro 目前难以匹敌的。
⚠️ 注意:本文使用 Spring Boot + FreeMarker 模板引擎构建示例,便于统一比较。
3. Apache Shiro 配置实战
3.1 Maven 依赖
在 Spring Boot 项目中使用 Shiro,需引入以下依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.3</version>
</dependency>
最新版本可参考 Maven Central
3.2 自定义 Realm
Shiro 的权限数据通过 Realm
提供。我们创建一个内存版的 CustomRealm
,模拟两个用户:
- Tom(角色:USER,权限:READ)
- Jerry(角色:ADMIN,权限:READ + WRITE)
public class CustomRealm extends JdbcRealm {
private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> permissions = new HashMap<>();
{
credentials.put("Tom", "password");
credentials.put("Jerry", "password");
roles.put("Jerry", new HashSet<>(Arrays.asList("ADMIN")));
roles.put("Tom", new HashSet<>(Arrays.asList("USER")));
permissions.put("ADMIN", new HashSet<>(Arrays.asList("READ", "WRITE")));
permissions.put("USER", new HashSet<>(Arrays.asList("READ")));
}
}
接下来重写认证与授权方法:
认证逻辑(doGetAuthenticationInfo
)
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
String username = userToken.getUsername();
if (username == null || !credentials.containsKey(username)) {
throw new UnknownAccountException("User doesn't exist");
}
return new SimpleAuthenticationInfo(
username,
credentials.get(username),
getName()
);
}
授权逻辑(doGetAuthorizationInfo
)
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Set<String> roles = new HashSet<>();
Set<String> perms = new HashSet<>();
for (Object user : principals) {
try {
roles.addAll(getRoleNamesForUser(null, (String) user));
perms.addAll(getPermissions(null, null, roles));
} catch (SQLException e) {
logger.error(e.getMessage());
}
}
SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo(roles);
authInfo.setStringPermissions(perms);
return authInfo;
}
辅助方法(获取角色与权限):
@Override
protected Set<String> getRoleNamesForUser(Connection conn, String username) throws SQLException {
if (!roles.containsKey(username)) {
throw new SQLException("User doesn't exist");
}
return roles.get(username);
}
@Override
protected Set<String> getPermissions(Connection conn, String username, Collection<String> roles)
throws SQLException {
Set<String> userPermissions = new HashSet<>();
for (String role : roles) {
if (!permissions.containsKey(role)) {
throw new SQLException("Role doesn't exist");
}
userPermissions.addAll(permissions.get(role));
}
return userPermissions;
}
3.3 注册 Bean
将 CustomRealm
注册为 Spring Bean:
@Bean
public Realm customRealm() {
return new CustomRealm();
}
配置拦截链(哪些接口需要登录):
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition filter = new DefaultShiroFilterChainDefinition();
filter.addPathDefinition("/home", "authc"); // 需认证
filter.addPathDefinition("/**", "anon"); // 其他放行
return filter;
}
✅ 至此,Shiro 配置完成。框架会自动处理登录、会话、权限校验。
4. Spring Security 配置实战
4.1 Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
最新版本见 Maven Central
4.2 安全配置类
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests(authorize -> authorize
.antMatchers("/index", "/login").permitAll()
.antMatchers("/home", "/logout").authenticated()
.antMatchers("/admin/**").hasRole("ADMIN")
)
.formLogin(formLogin -> formLogin
.loginPage("/login")
.failureUrl("/login-error")
);
return http.build();
}
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails jerry = User.withUsername("Jerry")
.password(passwordEncoder().encode("password"))
.authorities("READ", "WRITE")
.roles("ADMIN")
.build();
UserDetails tom = User.withUsername("Tom")
.password(passwordEncoder().encode("password"))
.authorities("READ")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(jerry, tom);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
关键点说明:
- ✅
HttpSecurity
配置访问规则,清晰声明每个路径的权限 - ✅
InMemoryUserDetailsManager
提供内存用户 - ✅ 密码使用
BCryptPasswordEncoder
加密存储 - ✅ 登录失败跳转
/login-error
⚠️ 注意:Spring Security 默认开启 CSRF,测试时建议关闭(如上所示)
5. 控制器与接口实现
5.1 页面渲染接口
两者共用以下接口:
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/login")
public String showLoginPage() {
return "login";
}
@GetMapping("/home")
public String getMeHome(Model model) {
addUserAttributes(model);
return "home";
}
模板文件:index.ftl
, login.ftl
, home.ftl
Shiro 获取用户信息
private void addUserAttributes(Model model) {
Subject currentUser = SecurityUtils.getSubject();
String permission = "";
if (currentUser.hasRole("ADMIN")) {
model.addAttribute("role", "ADMIN");
} else if (currentUser.hasRole("USER")) {
model.addAttribute("role", "USER");
}
if (currentUser.isPermitted("READ")) permission += " READ";
if (currentUser.isPermitted("WRITE")) permission += " WRITE";
model.addAttribute("username", currentUser.getPrincipal());
model.addAttribute("permission", permission.trim());
}
Spring Security 获取用户信息
private void addUserAttributes(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && !(auth instanceof AnonymousAuthenticationToken)) {
User user = (User) auth.getPrincipal();
model.addAttribute("username", user.getUsername());
for (GrantedAuthority authority : user.getAuthorities()) {
String authStr = authority.getAuthority();
if (authStr.contains("USER")) {
model.addAttribute("role", "USER");
model.addAttribute("permissions", "READ");
} else if (authStr.contains("ADMIN")) {
model.addAttribute("role", "ADMIN");
model.addAttribute("permissions", "READ WRITE");
}
}
}
}
✅ 小贴士:Spring 的
Authentication
对象更规范,适合做统一上下文处理
5.2 登录接口(POST)
Shiro 手动处理登录
@PostMapping("/login")
public String doLogin(HttpServletRequest req, UserCredentials credentials, RedirectAttributes attr) {
Subject subject = SecurityUtils.getSubject();
if (!subject.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken(
credentials.getUsername(),
credentials.getPassword()
);
try {
subject.login(token);
} catch (AuthenticationException ae) {
logger.error(ae.getMessage());
attr.addFlashAttribute("error", "Invalid Credentials");
return "redirect:/login";
}
}
return "redirect:/home";
}
Spring Security 自动处理
@PostMapping("/login")
public String doLogin(HttpServletRequest req) {
// 登录由 UsernamePasswordAuthenticationFilter 自动处理
return "redirect:/home";
}
✅ 赢家:Spring Security —— 登录流程完全透明,业务代码零侵入
5.3 管理员专用接口
Shiro:手动校验角色
@GetMapping("/admin")
public String adminOnly(ModelMap modelMap) {
addUserAttributes(modelMap);
Subject currentUser = SecurityUtils.getSubject();
if (currentUser.hasRole("ADMIN")) {
modelMap.addAttribute("adminContent", "only admin can view this");
}
return "home";
}
Spring Security:配置即生效
@GetMapping("/admin")
public String adminOnly(HttpServletRequest req, Model model) {
addUserAttributes(model);
model.addAttribute("adminContent", "only admin can view this");
return "home";
}
✅ 春风拂面:权限控制写在配置里,业务逻辑干净清爽
5.4 注销接口
Shiro 手动调用 logout
@PostMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/";
}
Spring Security 自动处理
无需编写 Controller 方法。只要配置了 SecurityFilterChain
,默认的 /logout
接口已自动启用。
✅ 再次体现:Spring Security 的“约定优于配置”哲学
6. 框架对比总结
维度 | Apache Shiro | Spring Security |
---|---|---|
✅ 学习曲线 | 简单直观,上手快 | 较陡,概念多(Filter、Provider、Token 等) |
✅ 集成难度 | 轻量,可独立使用 | 强依赖 Spring,非 Spring 项目难用 |
✅ 功能丰富度 | 基础功能完备 | 支持 OAuth2、SAML、LDAP、CAS 等企业级特性 |
✅ 社区生态 | 小而美 | 巨大,文档全,Stack Overflow 回答多 |
✅ 权限控制方式 | 代码中手动判断(易混乱) | 配置驱动,AOP 式切面控制(推荐) |
✅ CSRF 防护 | 无原生支持 | 开箱即用 |
一句话总结
- Shiro 适合轻量级项目、非 Spring 环境、快速接入
- Spring Security 适合中大型项目、Spring Boot 生态、需要高级安全特性
⚠️ 踩坑提醒:不要在业务代码里写
if (hasRole("ADMIN"))
,这迟早会变成技术债。用配置驱动才是正道。
7. 结论
本文通过真实代码对比,展示了 Shiro 与 Spring Security 在配置、认证、授权、登出等环节的差异。
虽然 Shiro 简单易懂,但 Spring Security 凭借其强大的生态、声明式安全控制和企业级特性支持,已成为当前 Java 安全框架的事实标准。
对于新项目,尤其是基于 Spring Boot 的应用,强烈推荐直接上 Spring Security,避免后期迁移成本。
🔗 示例源码已托管至 GitHub:https://github.com/yourname/tutorials/tree/master/security-modules/apache-shiro