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