简介

  • Shiro 是 Apache 的一个安全(权限)框架

  • Shiro 可以完成认证(是否登录)、授权(是否有权限)、加密(盐值MD5)、会话管理(记住我)、与 Web(SpringMVC) 集成、缓存等功能

  • Shiro 很简单,可以快速上手

架构

Shiro 的架构可以简单的表现为:

  • Subject: 应用程序直接交互的对象是 Subject,Subject 用于判断用户是否已登录、提供 Login 方法和获取认证的实体信息。这个实体的类型是 Object。与 Subject 的所有交互都会委托给 SecurityManager。

  • SecurityManager: 安全管理器。相当于 DispatcherServlet;Shiro 的核心,负责其它组件的交互。

  • Realm: 主要有两点作用:权限配置和组装实体(用户)信息。SecurityManager 要验证用户的密码是否正确,身份是否合法,那么就需要到 Realm 获取用户名、密码、角色和权限;

例子

环境

  • JDK 1.8
  • Spring MVC 4.1.7.RELEASE
  • myBatis 3.3
  • MySQL 5.7

引入依赖

<!-- Shiro start -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-core</artifactId>
	<version>1.4.0</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-web</artifactId>
	<version>1.4.0</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
<!-- Shiro end -->

设计表结构

经典的 RBAC 权限系统

用户和角色是1对多的关系,角色和权限也是1对多的关系,通过给用户分配角色,从而使用户拥有权限。

用户表

CREATE TABLE `admin_user` (
  `admin_user_id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL DEFAULT '',
  `password` varchar(255) NOT NULL DEFAULT '',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像',
  `create_date` datetime NOT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`admin_user_id`),
  UNIQUE KEY `uk_admin_user_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='管理员表';

角色表

CREATE TABLE `role` (
  `role_id` int(11) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(255) NOT NULL DEFAULT '',
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`role_id`),
  UNIQUE KEY `uk_role_role_name` (`role_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色表';

用户角色关系映射表

CREATE TABLE `admin_user_role_rel` (
  `admin_user_role_rel_id` int(11) NOT NULL AUTO_INCREMENT,
  `admin_user_id` int(11) NOT NULL,
  `role_id` int(11) NOT NULL,
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`admin_user_role_rel_id`),
  KEY `key_admin_user_role_rel_role_id` (`role_id`) USING BTREE,
  KEY `key_admin_user_role_rel_admin_user_id` (`admin_user_id`) USING BTREE,
  CONSTRAINT `fk_admin_user_role_rel_admin_user_id` FOREIGN KEY (`admin_user_id`) REFERENCES `admin_user` (`admin_user_id`),
  CONSTRAINT `fk_admin_user_role_rel_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色用户关系表';

权限表

CREATE TABLE `permission` (
  `permission_id` int(11) NOT NULL AUTO_INCREMENT,
  `permission_name` varchar(255) NOT NULL DEFAULT '',
  `permission_value` varchar(255) NOT NULL DEFAULT '',
  `pid` int(11) NOT NULL DEFAULT '0',
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`permission_id`),
  UNIQUE KEY `permission_name_uk` (`permission_name`),
  UNIQUE KEY `permission_value_uk` (`permission_value`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='权限表';

权限一般的定义规则是:资源标识符:操作

即对哪个资源可以做哪些操作,比如:

user:add

可以对用户的增加进行操作

user:edit

可以对用户的编辑进行操作

角色权限关系映射表

CREATE TABLE `role_permission_rel` (
  `role_permission_rel_id` int(11) NOT NULL AUTO_INCREMENT,
  `role_id` int(11) NOT NULL,
  `permission_id` int(11) NOT NULL,
  `create_date` datetime DEFAULT NULL,
  `update_date` datetime DEFAULT NULL,
  PRIMARY KEY (`role_permission_rel_id`),
  KEY `key_role_permission_rel_role_id` (`role_id`) USING BTREE,
  KEY `key_role_permission_rel_permission_id` (`permission_id`) USING BTREE,
  CONSTRAINT `fk_role_permission_rel_permission_id` FOREIGN KEY (`permission_id`) REFERENCES `permission` (`permission_id`),
  CONSTRAINT `fk_role_permission_rel_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色权限关系表';

每张表对应一个实体类

定义 Realm

public class AccountRealm extends AuthorizingRealm {

	private Logger log = LoggerFactory.getLogger(AuthorizingRealm.class);
	
	@Autowired
	private AdminUserService adminUserService;
	
	@Autowired
	private RoleService roleService;
	
	@Autowired
	private PermissionService permissionService;
	
	/**
	 * 用户权限配置
	 * principals:身份集合,因为我们可以在 Shiro 中同时配置多个 Realm,所以身份信息可能就有多个;
	 * 因此其提供了 PrincipalCollection 用于聚合这些身份信息
	 * getPrimaryPrincipal:如果只有一个Principal,那么直接返回即可。如果有多个 Principal,因为内部使用Map存储,则随机返回一个
	 * 返回的对象是在 doGetAuthenticationInfo 里设置的认证实体信息 principal
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		// 获取 principal
		AdminUser principal = (AdminUser)principals.getPrimaryPrincipal();
		// 获取用户
		AdminUser adminUser = adminUserService.getByName(principal.getUsername());
		if(adminUser != null) {
			SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
			
			List<Role> roles = roleService.getByAdminUserId(adminUser.getAdminUserId(), null, null);
			// 赋予角色
			roles.forEach(role -> info.addRole(role.getRoleName()));
			
			List<Permission> permissions = permissionService.getBatchByRoleList(roles);
			// 赋予权限
			permissions.forEach(permission -> info.addStringPermission(permission.getPermissionValue()));
			return info;
		}
		return null;
	}

	// 组装用户信息,会被 shiro 回调,用于密码校验的
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// 1. 把 AuthenticationToken 转换为 UsernamePasswordToken 
		UsernamePasswordToken upToken = (UsernamePasswordToken) token;
		
		// 2. 从 UsernamePasswordToken 中来获取 username
		String username = upToken.getUsername();
				
		log.debug("用户:{} 正在登录...", username);
		
		// 3.从数据库中查询 username 对应的用户记录
		AdminUser adminUser = adminUserService.getByName(username);
		
		// 4.如果用户不存在,则抛出未知用户的异常
		if(adminUser == null) throw new UnknownAccountException("用户不存在!");
		
		// 5.根据用户的情况, 来构建 AuthenticationInfo 对象并返回,通常使用的实现类为: SimpleAuthenticationInfo
		
		/**
		 * 5.1 principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. 
		 * 可以通过 SecurityUtils.getSubject().getPrincipal() 拿到 principal,如果有多个,则随机返回其中的一个
		 * 也可以通过 PrincipalCollection.getPrimaryPrincipal() 拿到 principal,如果有多个,则随机返回其中的一个
		 * 也可以通过 PrincipalCollection.asSet() 拿到所有的 principal,返回的是 set 集合
		 */
		// Object principal = username;
		AdminUser principal = new AdminUser();
		principal.setAdminUserId(adminUser.getAdminUserId());
		principal.setUsername(username);
		principal.setAvatar(adminUser.getAvatar());
		
		// 5.2 credentials: 密码
		Object credentials = adminUser.getPassword();
		
		// 5.3 realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
		String realmName = getName();
		
		// 5.4 盐值加密
		ByteSource credentialsSalt = ByteSource.Util.bytes(username);
		
		return new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
	}
}

定义 Shiro 的拦截规则

public class ShiroConfig {

	/**
	 * 定义Shiro拦截规则
	 * authc:所有url都必须认证通过,也就是登录后才可以访问
	 * anon:所有url都可以匿名访问
	 * @return
	 */
	public LinkedHashMap<String, String> buildFilterChainDefinitionMap(){
		
		// shiro的拦截规则是从上至下的,匹配到第一个就不会往下匹配了,所以这里使用有顺序的LinkedHashMap
		LinkedHashMap<String, String> map = new LinkedHashMap<>();
		
		// 配置静态资源,可以匿名访问
		map.put("/resources/**", "anon");
		
		// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
		map.put("/admin/logout", "logout");
		
		// 所有url都必须认证通过才可以访问
		map.put("/admin/**", "authc");
		
		return map;
	}
}

配置 spring-shiro.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- 配置 SecurityManager -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="authenticator" ref="authenticator"></property>
        
        <property name="realms">
        	<list>
    			<ref bean="accountRealm"/>
    		</list>
        </property>
        
        <property name="rememberMeManager.cookie.maxAge" value="10"></property>
    </bean>
    
    <bean id="authenticator" 
    	class="org.apache.shiro.authc.pam.ModularRealmAuthenticator">
    	<property name="authenticationStrategy">
    		<bean class="org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy"></bean>
    	</property>
    </bean>
    
    <!-- 配置 Realm -->
    <bean id="accountRealm" class="cn.roothub.bbs.config.realm.AccountRealm">
    	<property name="credentialsMatcher">
    		<bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
    			<!-- 加密算法的名称 -->
    			<property name="hashAlgorithmName" value="MD5"></property>
    			<!-- 配置加密的次数 -->
    			<property name="hashIterations" value="1024"></property>
    		</bean>
    	</property>
    </bean>
    
    <!-- 配置 LifecycleBeanPostProcessor. 可以自动的来调用配置在 Spring IOC 容器中 shiro bean 的生命周期方法 -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
    
    <!-- 启用 IOC 容器中使用 shiro 的注解. 但必须在配置了 LifecycleBeanPostProcessor 之后才可以使用 -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>
    
    <!-- 配置 ShiroFilter -->
    <!-- id 必须和 web.xml 文件中配置的 DelegatingFilterProxy 的 <filter-name> 一致,
    	   若不一致, 则会抛出: NoSuchBeanDefinitionException. 因为 Shiro 会来 IOC 容器中查找和 <filter-name> 名字对应的 filter bean -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <!-- 登录页面 ,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 -->
        <property name="loginUrl" value="/admin/login"/>
        <!-- 登录成功后要跳转的链接 -->
        <property name="successUrl" value="/admin/index"/>
        <!-- 出错页面定义 -->
        <property name="unauthorizedUrl" value="/admin/error"/>
        <!-- 定义拦截规则 -->
        <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"></property>
    </bean>
    
    <!-- 配置一个 bean, 该 bean 实际上是一个 Map. 通过实例工厂方法的方式 -->
    <bean id="filterChainDefinitionMap" 
    	  factory-bean="filterChainDefinitionMapBuilder" factory-method="buildFilterChainDefinitionMap">
    </bean>
    
    <!-- 定义拦截规则 -->
    <bean id="filterChainDefinitionMapBuilder"
    	  class="cn.roothub.bbs.config.ShiroConfig">
    </bean>
</beans>

配置 web.xml

   <!-- 配置  Shiro 的 shiroFilter,需先配置spring的ContextLoderListener监听器
    	Spring 会到 IOC 容器中查找和 <filter-name> 对应的 filter bean -->
  <filter>
  	<filter-name>shiroFilter</filter-name>
  	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  	<init-param>
    	<param-name>targetFilterLifecycle</param-name>
    	<param-value>true</param-value>
    </init-param>
  </filter>
  <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
  </filter-mapping>

登录处理

// 后台登录处理
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public String login(String username,String password,@RequestParam(defaultValue = "0") Boolean rememberMe,Model model) {
		try {
			// 添加用户认证信息
			Subject subject = SecurityUtils.getSubject();
			if(!subject.isAuthenticated()) {
				UsernamePasswordToken token = new UsernamePasswordToken(username, password, rememberMe);
				//进行验证,这里可以捕获异常,然后返回对应信息或者跳转到错误页面
				subject.login(token);
			}
		} catch (AuthenticationException e) {
			model.addAttribute("error", "用户名或密码错误");
			model.addAttribute("username", username);
	        return "/admin/login";
		}
		return "redirect:/admin/index";
	}

基于标签的权限控制

Shiro 提供了 taglib 标签,我们可以在 jsp 页面引入并使用

<shiro:hasPermission name="admin_user:add">
  <a href="/admin/admin_user/add" class="btn btn-xs btn-primary pull-right">添加</a>
</shiro:hasPermission>

当用户拥有 “admin_user:add” 权限时,才显示”添加”这个 a 标签

基于注解的权限控制

上面的标签只是判断是否显示某个 html 元素,如果知道后台的url,那么手动输入url访问还是能访问到资源的。所以还要在后台的 controller 层做权限控制。

Shiro 提供了 @RequiresPermissions 注解用于做权限控制

用法如下:

@RequiresPermissions("admin_user:add")
@RequestMapping(value = "/add", method = RequestMethod.GET)
public String add(Model model) {
	PageDataBody<Role> page = roleService.page(1, 100);
	model.addAttribute("page", page);
	return "/default/admin/admin_user/add";
}

Shiro 除了 @RequiresPermissions 注解外,还提供了很多注解

  • @RequiresRoles:表示需要的角色
  • @RequiresUser:表示已经身份验证或者通过记住我登录
  • @RequiresGuest:表示当前 Subject 没有身份验证,即是游客身份
  • @RequiresAuthentication:表示当前 Subject 已经通过 Login 进行了身份验证

一般用 @RequiresPermissions 注解就够了

SpringBoot 集成

SpringBoot 集成相对来说要简单,因为只要把 spring-shiro.xml 里配置的 bean 在 SpringBoot 中注入,然后用 @Configuration 注解表名这是一个配置类就可以了

如:

@Configuration
public class ShiroConfig {

  private Logger log = LoggerFactory.getLogger(ShiroConfig.class);

  @Autowired
  private AccountRealm accountRealm;

  // 配置 Shiro拦截规则
  @Bean
  public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
	// shiro的拦截规则是从上至下的,匹配到第一个就不会往下匹配了,所以这里使用有顺序的LinkedHashMap
	LinkedHashMap<String, String> map = new LinkedHashMap<>();
		
	// 配置静态资源,可以匿名访问
	map.put("/resources/**", "anon");
		
	// 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了
	map.put("/admin/logout", "logout");
		
	// 所有url都必须认证通过才可以访问
	map.put("/admin/**", "authc");
	shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    return shiroFilterFactoryBean;
  }

  // 安全管理器配置
  @Bean
  public SecurityManager securityManager() {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(accountRealm);
    return securityManager;
  }
}

总结

SpringMVC 集成 Shiro 到这里就结束了,Shiro 相对来说还是挺简单的。

我这篇博客只是对 Shiro 做一个总结,所以代码都是复制已经写好的,看起来可能会云里雾里的。。。自己动手先敲一个 hello world,在回过来看也许会更好。

框架这种东西如果只是停留在”会用”的层面,那确实是挺简单的。设计思想才是框架的精髓所在,所以我们在使用框架的时候,不仅仅是会用就行了,还要读一些源码,了解其设计思想,这样对于提高我们的编码水平还是有很大好处的。

原文链接:https://miansen.wang/2019/07/01/1/