
图源:
Shiro是一个权限管理组件,可以用它来实现Web应用的权限控制,本篇将介绍如何在Spring Boot的Web项目中使用Shiro实现权限控制。
准备工作
在使用Shiro前,需要先构建一个示例需要的基本Web应用:
-
从头创建一个新的基于
Spring Boot的Web项目,并添加基本的依赖,可以参考。 -
创建数据库,可以使用。
-
添加数据库依赖和配置,可以参考。
-
利用
Mybatis Plus自动生成框架代码,可以参考。
模块的划分可以参考:
-
book
-
book
-
-
user
-
user
-
user_role
-
role
-
role_permission
-
permission
-
自动创建的实体类最好手动添加上@TableId注解,否则某些数据库查询可能获取不到结果:
(callSuper = false)
("book")
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
(value = "id",type = IdType.AUTO)
private Integer id;
private String name;
private String description;
private Integer userId;
}
添加依赖
<!-- Shiro整合Spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
有多种
shiro相关的starter可以添加,这里仅列举一种。
实现相关Service
权限管理中需要用到根据用户名查询用户信息,我们的这里的权限组织是一个用户包含多个身份,一个身份包含多个权限,所以需要实现最基本的用户信息查询相关的Service,这个不难,所以不一一列举,可以查看我的源码:。
配置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 {
private IUserService userService;
/**
* 授权
* @param principalCollection
* @return
*/
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;
}
/**
* 身份认证
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
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();
}
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就可以正常获取并进行后续处理。
网上也有一些做法是通过自己实现令牌,并替换
Shiro默认的令牌实现的前后端分离的令牌分发和传递机制,相比之下通过SessionManager这种方式更为简单。
ShiroConfig
最后就是添加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;
public class ShiroConfig {
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//权限管理,配置主要是Realm的管理认证
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(customRealm());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
(name = "customRealm")
public CustomRealm customRealm() {
return new CustomRealm();
}
(name = "sessionManager")
public SessionManager sessionManager() {
return new CustomSessionManager();
}
//Filter工厂,设置对应的过滤条件和跳转条件
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
//注入权限管理
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
除了在securityManager中添加Realm和SessionManager以外,还可以在shiroFilterFactoryBean方法中定义一系列路径权限和特殊路径,比如登录页、错误页等等,不过这些在前后端分离系统中似乎不是那么必要。
ExceptionHandler
使用Shiro后,如果Controller权限检查失败,就会抛出一个ShiroException异常,所以要让这个异常以客户端能看懂的方式返回,就需要添加一个异常处理器:
package cn.icexmoon.demo.books.system;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
public class GlobalExceptionHandle {
(ShiroException.class)
public String doHandleShiroException(ShiroException se, Model model) {
se.printStackTrace();
Result result = new Result();
result.setSuccess(false);
if (se instanceof UnknownAccountException) {
result.setMsg("该账户不存在");
} else if (se instanceof LockedAccountException) {
result.setMsg("该账户已锁定");
} else if (se instanceof IncorrectCredentialsException) {
result.setMsg("密码错误请重试");
} else if (se instanceof UnauthorizedException) {
result.setMsg("当前角色不能操作");
} else if (se instanceof AuthorizationException) {
result.setMsg("没有相应权限");
} else {
result.setMsg("操作失败请重试");
}
return result.toString();
}
}
这里的Result是一个用于返回标准格式的工具类,这里不做赘述。
Controller
忙了半天后,我们终于可以使用Shiro了。
首先是添加一个登录用的HTTP处理器:
package cn.icexmoon.demo.books.user.controller;
import cn.icexmoon.demo.books.system.Login;
import cn.icexmoon.demo.books.system.Result;
import cn.icexmoon.demo.books.user.entity.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
("/login")
public class LoginController {
private Login login;
("")
public String login( User user) {
Result result = login.checkAndLogin(user.getName(), user.getPassword());
return result.toString();
}
}
因为在
Shiro配置中设置了shiroFilterFactoryBean.setLoginUrl("/login"),所以你不需要担心/login会被因为没有权限而阻拦。
这里的Login是我编写的一个使用Shiro进行验证并登录的Component:
package cn.icexmoon.demo.books.system;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
public class Login {
/**
* 根据用户名和密码检查身份并登录
*
* @param name
* @param password
* @return
*/
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;
/**
* <p>
* 前端控制器
* </p>
*
* @author icexmoon
* @since 2022-05-06
*/
public class BookController {
private IBookService bookService;
private IUserService userService;
(value = {"guest", "manager"}, logical = Logical.OR)
("/book")
public String listAllBooks() {
Result result = new Result();
List<Book> books = bookService.list();
result.setData(books);
return result.toString();
}
("manager")
("/book/add")
public String addBook( 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,就需要这样设置:
(value = {"guest", "manager"}, logical = Logical.OR)
下面给数据库添加一些测试数据来验证一下。
你可以从获取我的测试数据SQL。
lalala用户仅有访客角色,而icexmoon有访客和管理员两个角色。使用lalala登录后可以访问书籍列表,但是不能添加书籍,而icexmoon可以访问书籍列表和添加书籍。

这是我的接口测试文档:
OK,就到这里了,谢谢阅读。
可以从获取最终的工程源码。

文章评论