在互联网软件开发过程中,我们项目中必不可少的是安全框架,如何做好项目的安全是一个app最基本的一步。
我们现阶段使用最多的安全框架一个是Spring Security框架,另一种是Apache Shiro,今天我们来学习一下shiro的使用。
一、简介
什么是权限管理
权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
权限管理包括用户身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。
Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。Shiro可以帮助我们完成:认证、授权、加密、会话管理、与Web集成、缓存等。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可
二、Shiro基础介绍
1.Shiro框架核心组件
Shiro框架主要包含三个核心组件:Subject、SecurityManager、Realm
1.1. Subject
Subject:即“当前操作用发户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数目的和用途,你可以把它认为是Shiro的“用户”概念。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
1.2 SecurityManager
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
1.3 Realm
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
2. Shiro相关类介绍
(1)Authentication 认证 ---- 用户登录
(2)Authorization 授权 — 用户具有哪些权限
(3)Cryptography 安全数据加密
(4)Session Management 会话管理
(5)Web Integration web系统集成
(6)Interations 集成其它应用,spring、缓存框架
3. Shiro 特点
(1)易于理解的 Java Security API;
(2)简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
(3)对角色的简单的签权(访问控制),支持细粒度的签权;
(4)支持一级缓存,以提升应用程序的性能;
(5)内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
(6)异构客户端会话访问;
(7)非常简单的加密 API;
(8)不跟任何的框架或者容器捆绑,可以独立运行
三、Shiro实战
1.创建springboot项目
首先我们创建一个springboot maven项目。引入apache.shiro依赖以及web和thymeleaf依赖,以及mybatis数据库相关依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.cpown</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--webjar 引入jquery-->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.42</version>
</dependency>
<!--druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.3.7</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.57</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
<configuration>
<configurationFile>src/main/resources/generator/generatorConfig.xml</configurationFile>
<verbose>true</verbose>
<overwrite>true</overwrite>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.42</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
2.数据库准备
这里我们准备四张表:
- 部门表 department
- 用户表 sys_user
- 角色表 t_role
- 权限表 t_permission
CREATE TABLE `t_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`rolename` varchar(20) DEFAULT NULL COMMENT '角色名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='角色';
CREATE TABLE `t_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`permissionname` varchar(50) NOT NULL COMMENT '权限名',
`role_id` int(11) DEFAULT NULL COMMENT '外键关联role',
PRIMARY KEY (`id`),
KEY `role_id` (`role_id`),
CONSTRAINT `t_permission_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `t_role` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='权限';
CREATE TABLE `sys_user` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(255) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`sex` tinyint(3) DEFAULT NULL COMMENT '性别',
`email` varchar(255) DEFAULT NULL COMMENT '邮箱',
`department_id` int(11) DEFAULT NULL COMMENT '部门id',
`role_id` int(11) DEFAULT NULL COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
CREATE TABLE `department` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'id',
`department_name` varchar(255) DEFAULT NULL COMMENT '部门名称',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8 COMMENT='部门';
3.编写基本业务代码
这里详细代码不再展示,详情可以参考之前的文章:springboot + bootstrap + thymeleaf +mybatis 编写员工管理系统
项目代码git地址:https://github.com/chenpeng111/springboot
代码完成后项目如下:
-
登录页
-
部门管理页面
-
用户管理页面
-
角色管理页面
-
权限管理页
4.整合shiro
从上图可以看到:
- 我们建立了两个角色:管理、专员
- 给角色管理赋予了create权限,给专员赋予了delete权限。
- 创建了两个用户admin和张大胖子,分别给与了管理以及专员的角色。
准备工作做完了,我们来看下shiro的核心代码:
4.1 自定义Realm
自定义 Realm 需要继承 AuthorizingRealm 类,该类封装了很多方法,且继承自 Realm 类。
继承 AuthorizingRealm 类后,我们需要重写以下两个方法。
doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
这里的UsernamePasswordToken是在登录接口里面设置的用户信息(下面会展示登录代码),通过authentication.getPrincipal()可以获取到用户登录名。这里我们直接查询数据库SysUser信息:查询判断当前用户是否存在
- 不存在则返回null ===》 登录端会直接抛出UnknownAccountException 异常。
- 如果用户存在 则将用户密码 交给认证管理器 处理,如果错误的话 =》登录端会直接抛出IncorrectCredentialsException异常
doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
通过principalCollection.getPrimaryPrincipal();获取用户登录名,查询数据库用户SysUser,获取用户角色Role以及授权信息Permissions
将
当前用户角色信息,交给授权管理器处理 authorizationInfo.setRoles(stringSet);它会帮我们校验是否已经授权角色(授权代码会在下面展示)。
权限信息交给 授权管理器处理
authorizationInfo.setStringPermissions(tPermissions.stream().map(TPermission::getPermissionname).collect(Collectors.toSet()));
package com.cpown.demo.config;
import com.cpown.demo.pojo.SysUser;
import com.cpown.demo.pojo.TPermission;
import com.cpown.demo.pojo.TRole;
import com.cpown.demo.service.SysUserService;
import com.cpown.demo.service.TPermissionService;
import com.cpown.demo.service.TRoleService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
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 javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 自定义Realm文件 需要继承 AuthorizingRealm 并重写认证,授权方法
* create by c-pown on 2020-07-28
*/
@Slf4j
public class MyRealm extends AuthorizingRealm {
@Resource
private SysUserService sysUserService;
/**
* 角色service
*/
@Resource
TRoleService roleService;
/**
* 权限service
*/
@Resource
TPermissionService permissionService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
log.info("进入授权");
// 获取用户名
String username = (String) principalCollection.getPrimaryPrincipal();
//查询当前用户
SysUser sysUser = sysUserService.selectSysUserByName(username);
//获取当前用户角色
TRole tRole = roleService.selectByPrimaryKey(sysUser.getRoleId());
//将角色信息放进Set
Set<String> stringSet = new HashSet<>();
stringSet.add(tRole.getRolename());
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
//将当前用户角色信息,交给授权管理器处理
authorizationInfo.setRoles(stringSet);
//通过当前角色信息 获取权限信息
List<TPermission> tPermissions = permissionService.selectByRoleId(tRole.getId());
//权限信息交给 授权管理器处理
authorizationInfo.setStringPermissions(tPermissions.stream().map(TPermission::getPermissionname).collect(Collectors.toSet()));
return authorizationInfo;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
log.info("进入认证");
UsernamePasswordToken authentication = (UsernamePasswordToken)authenticationToken;
//查询判断当前用户是否存在 不存在则返回null ===》 登录端会直接抛出UnknownAccountException 异常
SysUser user = sysUserService.selectSysUserByName((String)authentication.getPrincipal());
if(user != null ){
SecurityUtils.getSubject().getSession().setAttribute("user",user);
//如果用户存在 则将用户密码 交给认证管理器 处理,如果错误的话====》登录端会直接抛出IncorrectCredentialsException异常
return new SimpleAuthenticationInfo(user.getName(),user.getPassword(),"myRealm");
}
return null;
}
}
4.2 创建Shiro配置中心ShiroConfig
自定义 Realm 写好了,接下来需要配置 Shiro。我们主要配置三个东西:自定义 Realm、安全管理器 SecurityManager 和 Shiro 过滤器。
这里核心的代码逻辑在 Shiro 过滤器里面:
- 配置 Shiro 过滤器时,我们引入了安全管理器。
至此,我们可以看出,Shiro 配置一环套一环,遵循从 Reaml 到 SecurityManager 再到 Filter 的过程。在过滤器中,我们需要定义一个 shiroFactoryBean,然后将 SecurityManager 引入其中,需要配置的内容主要有以下几项。
- 默认登录的 URL:身份认证失败会访问该 URL。
- 认证成功之后要跳转的 URL。
- 权限认证失败后要跳转的 URL。
- 需要拦截或者放行的 URL:这些都放在一个 Map 中。
shiro将所有的配置信息都放在一个map中,通过 bean.setFilterChainDefinitionMap(map);放进过滤器。
Map<String, String> map = new LinkedHashMap<>();
//anon标识可以直接放行 首页登录index直接放行
map.put("/index","anon");
map.put("/sys/toLogin","anon");
/**
* shiro内置过滤器
* anon 无需认证可以访问
* authc 必须认证才能访问
* user 必须拥有 记住我 功能 才可以用
* perms 拥有对某个资源的权限才能访问
* role 拥有某个角色才能访问
*/
map.put("/user/**", "authc");
map.put("/department/**", "authc");
map.put("/role/**", "authc");
map.put("/perm/**", "authc");
// “/role/**” 开头的用户需要角色认证,是“管理”才允许
map.put("/role/**", "roles[管理]");
// “/perm/**” 开头的用户需要权限认证,是create才允许
map.put("/perm/**", "perms[create]");
bean.setFilterChainDefinitionMap(map);
package com.cpown.demo.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* ShiroConfig 配置类
* create by c-pown on 2020-07-28
*/
@Configuration
@Slf4j
public class ShiroConfig {
/**
* Shiro:三大组件:Subject,SecurityManager,Realm
*
*/
@Bean
public MyRealm myRealm(){
log.info("注册Realm");
return new MyRealm();
}
/**
* 创建SecurityManager 安全管理器组件
* @param realm
* @return
*/
@Bean
public SecurityManager securityManager(@Qualifier("myRealm")MyRealm realm){
log.info("创建SecurityManager");
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")SecurityManager securityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
bean.setSecurityManager(securityManager);
Map<String, String> map = new LinkedHashMap<>();
//anon标识可以直接放行 首页登录index直接放行
map.put("/index","anon");
map.put("/sys/toLogin","anon");
/**
* shiro内置过滤器
* anon 无需认证可以访问
* authc 必须认证才能访问
* user 必须拥有 记住我 功能 才可以用
* perms 拥有对某个资源的权限才能访问
* role 拥有某个角色才能访问
*/
map.put("/user/**", "authc");
map.put("/department/**", "authc");
map.put("/role/**", "authc");
map.put("/perm/**", "authc");
// “/role/**” 开头的用户需要角色认证,是“管理”才允许
map.put("/role/**", "roles[管理]");
// “/perm/**” 开头的用户需要权限认证,是create才允许
map.put("/perm/**", "perms[create]");
bean.setFilterChainDefinitionMap(map);
//登录页
bean.setLoginUrl("/sys/toLogin");
//未授权跳转页
bean.setUnauthorizedUrl("/sys/toUnAuthorized");
return bean;
}
}
4.3 登录接口
我们重点分析下用户登录接口。整个处理过程是这样的。
首先,根据前端传来的用户名和密码,创建一个 Token。
然后,使用 SecurityUtils 创建认证主体。
紧接着,调用 subject.login(token) 进行身份认证——注意,这里传入了刚刚创建的 Token,如注释所述,这一步会跳转入自定义的 Realm,访问 doGetAuthenticationInfo 方法,开始身份认证。
- Subject subject = SecurityUtils.getSubject();相当于Shiro的回话总线。
- 这里的设置 UsernamePasswordToken 在MyRealm认证里面会用到。
- 这里定义的跳转登录页面,以及认证失败页面都是我们之前在,shiro管理器里面配置的路径。
package com.cpown.demo.controller;
import com.cpown.demo.pojo.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
/**
* 用户登录controller
*/
@Controller
@RequestMapping("/sys")
@Slf4j
public class LoginController {
/**
* 登录用户
* @param username
* @param password
* @param model
* @return
*/
@RequestMapping("/login")
public String login(@RequestParam("userName")String username, @RequestParam("password") String password, Model model){
log.info("用户登录:username={}",username);
//获取到 Subject 对象
Subject subject = SecurityUtils.getSubject();
//将当前 登录用户 放进Subject
//这里的 UsernamePasswordToken 在MyRealm认证里面会用到
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.getSession().setAttribute("user",new SysUser(username,password));
subject.login(token);
} catch (UnknownAccountException e) {//用户不存在
model.addAttribute("msg","账号不存在");
return "index";
}catch (IncorrectCredentialsException e1){
model.addAttribute("msg","密码错误");
return "index";
}
return "dashboard";
}
/**
* 跳转登录页面
* @param model
* @return
*/
@RequestMapping("/toLogin")
public String toLogin(Model model){
return "index";
}
/**
* 退出当前用户
* @param model
* @return
*/
@RequestMapping("/loginout")
public String loginout(Model model){
SecurityUtils.getSubject().logout();
return "index";
}
/**
* 认证失败页面
* @param model
* @return
*/
@RequestMapping("/toUnAuthorized")
public String toUnAuthorized(Model model){
return "error/unauthorized";
}
/**
* 首页
* @return
*/
@RequestMapping("/dashboard")
public ModelAndView toDashBoard(){
return new ModelAndView("dashboard");
}
}
4.整体效果
首先我们登录用户:
张小胖子(没有这个用户),提示账号不存在。
使用 张大胖子,1234,提示密码错误。
使用 张大胖子,123456,登录成功。角色是专员:可以查看用户,部门,但是没有权限查看,角色以及授权页面。
使用admin,123456登录,角色为管理,拥有create权限,可以查看所有页面。
没有问题。欢迎各位指正。
5.没有权限直接隐藏目录
虽然上述已经实现了权限控制,但是我们在实际项目中,没有权限的按钮或者目录一般都是直接隐藏不展示。下面来高一下子:
- maven引入:这个是shiro和thymeleaf的一个整合包。
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
- 添加配置:在ShiroConfig配置类中添加下面方法。
@Bean(name = "shiroDialect")
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
项
- 添加页面命名空间:
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
- 修改按钮标签:shiro:hasPermission是否有权限,shiro:hasRole是否是该角色。
重启登陆下:完美隐藏