图源:简书 (jianshu.com)
Shiro是一个权限管理组件,可以用它来实现Web应用的权限控制,本篇将介绍如何在Spring Boot的Web项目中使用Shiro实现权限控制。
准备工作在使用Shiro前,需要先构建一个示例需要的基本Web应用:
- 从头创建一个新的基于Spring Boot的Web项目,并添加基本的依赖,可以参考从零开始Spring Boot 1:快速构建 - 魔芋红茶’s blog (icexmoon.cn)。
- 创建数据库,可以使用learn_spring_boot/books.sql (github.com)。
- 添加数据库依赖和配置,可以参考从零开始 Spring Boot 4:Mybatis Plus - 魔芋红茶’s blog (icexmoon.cn)。
- 利用Mybatis Plus自动生成框架代码,可以参考从零开始 Spring Boot 7:生成框架代码 - 魔芋红茶’s blog (icexmoon.cn)。
模块的划分可以参考:
- book
- book
- user
- user
- user_role
- role
- role_permission
- permission
自动创建的实体类最好手动添加上@TableId注解,否则某些数据库查询可能获取不到结果:
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("book")
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id",type = IdType.AUTO)
private Integer id;
private String name;
private String description;
private Integer userId;
}
添加依赖
org.apache.shiro
shiro-spring
1.5.3
实现相关Service有多种shiro相关的starter可以添加,这里仅列举一种。
权限管理中需要用到根据用户名查询用户信息,我们的这里的权限组织是一个用户包含多个身份,一个身份包含多个权限,所以需要实现最基本的用户信息查询相关的Service,这个不难,所以不一一列举,可以查看我的源码:learn_spring_boot (github.com)。
配置Shiro Realm要让Shiro能够正常的鉴权和赋权,就需要实现一个Realm,具体可以继承AuthorizingRealm并实现两个抽象方法:
package cn.icexmoon.demo.books.system.shiro;
import cn.icexmoon.demo.books.user.entity.Permission;
import cn.icexmoon.demo.books.user.entity.Role;
import cn.icexmoon.demo.books.user.entity.User;
import cn.icexmoon.demo.books.user.service.IUserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
public class CustomRealm extends AuthorizingRealm {
@Autowired
private IUserService userService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//获取登录用户名
String name = (String) principalCollection.getPrimaryPrincipal();
//查询用户名称
User user = userService.getUserByName(name);
//添加角色和权限
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (Role role : user.getRoles()) {
//添加角色
simpleAuthorizationInfo.addRole(role.getName());
//添加权限
for (Permission permission : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permission.getName());
}
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (ObjectUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String name = authenticationToken.getPrincipal().toString();
User user = userService.getUserByName(name);
if (user == null) {
//这里返回后会报出对应异常
return null;
} else {
//这里验证authenticationToken和simpleAuthenticationInfo的信息
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name, user.getPassword(), getName());
return simpleAuthenticationInfo;
}
}
}
doGetAuthenticationInfo方法用于登录时检查用户密码是否正确,既进行身份验证。doGetAuthorizationInfo方法用于给已登录的用户添加相关的角色和权限,及赋权。有了这个权限和角色关联后,就可以在Controller中使用Shiro的相关注解来进行权限控制。
这里主要工作是要在doGetAuthorizationInfo中根据我们的数据库和Service来添加权限和角色。
SessionManager因为这里的示例应用是一个纯后台的应用,通过Restfull接口与客户端通信,也就是所谓的前后分离的系统,没有页面。而默认情况下Shiro是通过Cookie来存储和传递客户端令牌的,所以我们需要为Shiro添加一个自定义的SessionManager来通过HTTP请求的特定报文头来传递令牌。
package cn.icexmoon.demo.books.system.shiro;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
public class CustomSessionManager extends DefaultWebSessionManager {
private static final String HEADER_TOKEN = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public CustomSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN);
if (!ObjectUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
return super.getSessionId(request, response);
}
}
}
getSessionId方法中尝试从指定报文头(token)获取令牌,如果获取到了,就写入ServletRequest的相应属性,Shiro就可以正常获取并进行后续处理。
ShiroConfig网上也有一些做法是通过自己实现令牌,并替换Shiro默认的令牌实现的前后端分离的令牌分发和传递机制,相比之下通过SessionManager这种方式更为简单。
最后就是添加Shiro配置,以将我们设置好的Realm和SessionManager添加到Shiro中:
package cn.icexmoon.demo.books.system.shiro;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//权限管理,配置主要是Realm的管理认证
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
@Bean(name = "customRealm")
public CustomRealm customRealm() {
return new CustomRealm();
}
@Bean(name = "sessionManager")
public SessionManager sessionManager() {
return new CustomSessionManager();
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("
public Result checkAndLogin(String name, String password) {
Result result = new Result();
if (ObjectUtils.isEmpty(name) || ObjectUtils.isEmpty(password)) {
result.setSuccess(false);
result.setMsg("用户名或密码为空。");
return result;
}
//用户认证信息
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(
name,
password
);
try{
subject.login(usernamePasswordToken);
}
catch (UnknownAccountException e){
result.setSuccess(false);
result.setMsg("账户不存在");
return result;
}
catch (AuthenticationException e){
result.setSuccess(false);
result.setMsg("账号或密码错误");
return result;
}
catch (AuthorizationException e){
result.setSuccess(false);
result.setMsg("没有权限");
return result;
}
result.setData(subject.getSession().getId());
return result;
}
}
然后编写两个简单的功能用于验证:
- /book,展示所有书籍。
- /book/add,添加图书。
package cn.icexmoon.demo.books.book.controller;
import cn.icexmoon.demo.books.book.entity.Book;
import cn.icexmoon.demo.books.book.service.IBookService;
import cn.icexmoon.demo.books.system.Result;
import cn.icexmoon.demo.books.user.entity.User;
import cn.icexmoon.demo.books.user.service.IUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
public class BookController {
@Autowired
private IBookService bookService;
@Autowired
private IUserService userService;
@RequiresRoles(value = {"guest", "manager"}, logical = Logical.OR)
@GetMapping("/book")
public String listAllBooks() {
Result result = new Result();
List books = bookService.list();
result.setData(books);
return result.toString();
}
@RequiresRoles("manager")
@PostMapping("/book/add")
public String addBook(@RequestBody Book book) {
//添加图书
Subject subject = SecurityUtils.getSubject();
String name = (String) subject.getPrincipal();
User user = userService.getUserByName(name);
book.setUserId(user.getId());
bookService.save(book);
Result result = new Result();
result.setData(book.getId());
result.setMsg("添加成功");
return result.toString();
}
}
展示所有图书管理员和访客都有权限,而添加图书就只能管理员。
需要注意的是,默认的@RequiresRoles中如果添加多个角色,就要求当前用户同时具备多个角色才可以访问,这是AND的关系,如果要使用OR,就需要这样设置:
@RequiresRoles(value = {"guest", "manager"}, logical = Logical.OR)
下面给数据库添加一些测试数据来验证一下。
你可以从learn_spring_boot (github.com)获取我的测试数据SQL。
lalala用户仅有访客角色,而icexmoon有访客和管理员两个角色。使用lalala登录后可以访问书籍列表,但是不能添加书籍,而icexmoon可以访问书籍列表和添加书籍。
这是我的接口测试文档:
- https://docs.apipost.cn/preview/2475843295275e3d/c84f0c92cb6cef2a
OK,就到这里了,谢谢阅读。
参考资料可以从learn_spring_boot (github.com)获取最终的工程源码。
- MyBatis-Plus
- Springboot集成Shiro(前后端分离) - 云+社区 - 腾讯云 (tencent.com)
- 超详细 Spring Boot 整合 Shiro 教程! - 云+社区 - 腾讯云 (tencent.com)
- springboot整合shiro(完整版) - 简书 (jianshu.com)
- shiro注解@RequiresPermissions多权限任选一参数用法_Powerful_Current的博客-CSDN博客_requirespermissions的使用



