权限管理的设计范式:RBAC(Role Based Access Control),基于角色的访问控制。用户是通过角色与权限进行关联的。因此一般会有五个表格:用户表,用户角色表,角色表,角色权限表,权限表。
为什么要用到权限?
举个通俗易懂的例子,我们生活在一个充满规则的世界里,万事万物都运行在规则之内,软件系统也是一样,它也有自己的一些规则。规则落到实处就是什么人能做什么事,什么人不能做什么事。也就是说对某个用户,或者说某类主体的抽象做一个权责限定。
为什么中间一定要有个角色表?(用户直接与权限关联不好吗?)
角色:可以理解为一堆权限的集合抽象或者载体。比如一个论坛会有管理员和版主两种角色,版主只能管理自己旗下的用户和数据,而管理员可以管理所有版主的用户和数据。这就是角色,有了角色,权限分配就会变得容易。
什么是用户组?
当用户量越来越多时,为了更好的管理权限,可以抽象出一个叫做用户组的概念,用户组可以给一组用户授予权限。这时,一个用户所拥有的权限=用户个人所拥有的权限+用户所在组所拥有的权限。用户组,用户和角色的关系如下图:
通常在应用系统中,权限的表现形式如下几种:
1.功能模块的操作(功能);2.菜单的访问(资源);3.文件、按钮、图片等的操作(资源)。
因此可以构成用户、角色、权限、资源这样一种授权模型。
如何设计出一个可维护、便捷的数据库表结构?
将功能操作、资源通过与权限表相关联进行统一管理。(表设计思路:功能、资源↔权限)
编码落地→业界的流行解决方案是(不要重复造轮子)?我们如何选择框架?
shiro
技术的发展路线:出现问题→好的解决方案。
根据请求资源,通过查表判断其权限来做响应返回。
缺点:业务处理代码和权限管理代码糅杂在一起。是否可以解耦?
将通用的代码放在过滤器中,比如权限查询,根据权限查询结果对请求资源做相应反应(放行or不放行)。如果不放行直接返回权限不足;如果放行,则请求资源有资格进入业务代码做相应业务处理。
缺点:每次请求资源都要查其权限,性能低。是否可以保存用户的权限?
服务器对每个请求都保存一个sessionId,并将这个sessionId存储在客户端的cookie中用作身份标识,在下次请求的时候客户端就会携带这个cookie,服务器就会拿到cookie中的sessionId,根据这个sessionId到相应的session域(基于内存/缓存/数据库/redis等)中查用户的数据比如身份信息来判断权限等。性能高。
缺点:我们始终在重复造轮子。能将其封装成一个好的框架?
shiro框架是对过滤器+会话的封装。它是 Apache下的一个Java安全框架,旨在简化身份验证和授权。Shiro在JavaSE和JavaEE项目中都可以使用。它主要用来处理身份认证,授权,企业会话管理和加密等。
缺点:对于没有PC客户端(浏览器)的终端(手机app、小程序等),cookie技术无法得到使用。强大的shiro框架也无力可施。
基于session+cookie的一种授权认证流程并不是适用于任意场景。比如手机app和小程序等没有客户端浏览器,它们无法使用cookie技术。JWT(Json web token)则是应对这一场景的利器,它是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
官方代码:https://github.com/apache/shiro
官方文档: http://shiro.apache.org
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
认证(Authentication):用户身份识别,常被称为用户“登录”,判断用户是否登陆,如果未登陆,则拦截其请求。优点:可以实现单点登录,多个子系统登录一个其他子系统也自动登录。(如登录了淘宝,天猫也自动登录了)
授权(Authorization):访问控制。当用户登陆后,判断其身份是否有权限访问相应的资源,如果没有权限则拦截
密码加密(Cryptography):保护或隐藏数据防止被偷窃。将MD5进行二次封装,让其更加容易使用。注意MD5不可逆运算
会话管理(Session Management):保持用户的整个会话活动的互动与计算机系统跟踪过程。
三个核心组件:Subject, SecurityManager 和 Realms
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟应用软件交互的东西”。 (Subject其实是一个门面,SecurityManager才是实际的执行者)。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade(外观)模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。 (它相当于SpringMVC的DispatchServlet的角色)。
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
shiro核心组件:
1、UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token。
2、SecurityManager,Shiro 的核心部分,负责安全认证和授权。
3、Suject,Shiro 的一个抽象概念,包含了用户信息。
4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
5、AuthenticationInfo,用户的角色信息集合,认证时使用。
6、AuthorzationInfo,角色的权限信息集合,授权时使用。
7、DefaultWebSecurityManager,安全管理器,开发者自定义的 Realm 需要注入到DefaultWebSecurityManager 进行管理才能生效。
8、ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由 ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。
Shiro 的运行机制如下图所示。
SpringBoot集成Shiro框架。由于是前后端分离后:使用Swagger对接口进行鉴权处理。
step1:创建 Spring Boot 应用,集成 Shiro 及相关组件,pom.xml。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.5.3</version>
</dependency>
<!-- 集成swagger,用于测试接口(也可以用官方的swagger) -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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>
step2:自定义Realm权限认证。
import com.example.shiro.pojo.User;
import com.example.shiro.service.UserService;
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 org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.HashSet;
import java.util.Set;
/**
* ----------------------------------------------------------------------
* @description: UserRealm 自定义Realm
* @author: Create by Liu Wen at 2020-07-14 15:41
* ----------------------------------------------------------------------
**/
public class UserRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* @Description: 授权(查询数据库进行授权) 角色的权限信息集合,授权时使用。 这里权限代码一般超级多
* 也可以根据用户自带的权限标识来进行判断
* @date 20.7.14 23:47
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权-->doGetAuthorizationInfo");
//获取由认证(getPrincipal())传来的授权标识
Subject subject = SecurityUtils.getSubject();
User currentUser = (User)subject.getPrincipal();
//设置角色
Set<String> roles = new HashSet<>();
roles.add(currentUser.getRole());
//设置角色权限
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
//都有add权限
info.addStringPermission("add");
//设置update权限
info.addStringPermission(currentUser.getPerm());
return info;
}
/**
* @Description: 认证(查询数据库进行认证) 用户的角色信息集合,认证时使用。
* @date 20.7.14 23:47
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行认证-->doGetAuthenticationInfo");
//用户传入的用户名和密码封装在Token中
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
//根据用户输入到 数据库中查询用户信息
User user = userService.queryUserByName(usernamePasswordToken.getUsername());
if(user==null){
//Controller层会抛出UnknownAccountException异常
return null;
}
//根据用户输入的用户名查到的数据库中用户信息不为null,下面认证密码
//密码认证(Shiro自动做认证)(在第一个参数中将权限标识传给授权)
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
step3:配置Shiro 过滤器。
添加shiro内置过滤器,一共分两类过滤器:
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;
/**
* ----------------------------------------------------------------------
* @description: Shiro配置类
* shiro的三个组件ShiroFilterFactoryBean、DefaultWebSecurityManager、Realm通过@Bean注解交给IOC容器管理
* 再通过@Qualifier相互注入将三个组件联合协作起来,以完成对认证与权限管理。
* @author: Create by Liu Wen at 2020-07-14 15:25
* ----------------------------------------------------------------------
**/
//取消@Configuration,shiro权限框架失效。可以访问所有url资源
@Configuration
public class ShiroConfig {
/**
* @Description: 一、ShiroFilterFactoryBean
* 过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作
* 是由 ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。
* @date 20.7.16 09:22
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全框架
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//添加shiro内置过滤器
/*一共分两类过滤器:
认证过滤器:
anon: 无需认证就可以访问
authc: 认证可以访问
authcBasic:需要通过HTTPBasic认证才可以可以访问
user: 必须被shiro记录过才可以访问,比如:记住我
授权过滤器:(权限是基于登录的,登录通过,才可能有权限)
perms: 必须拥有某个资源的权限时才可以访问(资源可是页面)
role: 必须拥有某个角色权限时才可以访问
port:请求的借口必须是指定值才可以访问
rest:请求必须基于restful:post,put,get,delete才可以访问
ssl:必须是安全的URL请求,基于HTTPS协议才可以访问
*/
//拦截+权限
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
//对登录资源请求进行授权,正常的情况下,没有授权会跳出未授权页面
//anon无需认证就可以访问的页面(首页)
filterChainDefinitionMap.put("/index","anon");
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/unauthorized","authc");
//拥有add资源权限即可访问页面
filterChainDefinitionMap.put("/user/add","perms[add]");
//拥有update资源权限即可访问页面
filterChainDefinitionMap.put("/user/update","perms[update]");
//拥有admin角色权限即可访问页面
filterChainDefinitionMap.put("/user/admin","roles[admin]");
//拥有root角色权限即可访问页面
filterChainDefinitionMap.put("/user/root","roles[root]");
//登录就能访问/user/*下所有页面 (权限越大放后面,以防止大权限泄露给后面的请求)
filterChainDefinitionMap.put("/user/*","authc");
//过滤器会拦截以上请求做权限资源判断
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
//设置未登录页面(未登录则跳到未登录页面)
shiroFilterFactoryBean.setLoginUrl("/tologin");
//设置为授权页面(未授权则跳到未授权页面)
shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
return shiroFilterFactoryBean;
}
/**
* @Description: 二、DefaultWebSecurityManager(SecurityManager是 Shiro 的核心部分,负责安全认证和授权。其子类:
* DefaultWebSecurityManager是安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。
* 通过@Qualifier注解找到IOC容器中对应的userRealm对象
* @date 20.7.16 09:22
*/
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDeafaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联自定义的Realm(可以配置多个,至少配置一个)
securityManager.setRealm(userRealm);
return securityManager;
}
/**
* @Description: 三、创建Realm对象(通过自定义类:开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。)
* 通过@Bean注解把UserRealm对象交给IOC容器管理(用name="userRealm"对其进行标识)
* @date 20.7.16 09:22
*/
@Bean(name = "userRealm")
public UserRealm getUserRealm(){
return new UserRealm();
}
}
step4:Controller层。(定义请求资源)
import io.swagger.annotations.ApiOperation;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
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.*;
/**
* ----------------------------------------------------------------------
* @description: 请求资源(即url请求页面)
* @author: Create by Liu Wen at 2020-07-14 15:15
* ----------------------------------------------------------------------
**/
@RestController
public class HelloController {
@ApiOperation(value = "首页")
@GetMapping(value="/index")
public String toIndex(){
return "This is index";
}
@ApiOperation(value = "未登录页面")
@GetMapping("/tologin")
public String login(){
return "Not login, please login";
}
@ApiOperation(value = "添加权限")
@GetMapping("/user/add")
public String add(){
return "Welcome,This is add";
}
@ApiOperation(value = "更新权限")
@GetMapping("/user/update")
public String update(){
return "Welcome,This is update";
}
@ApiOperation("管理员用户")
@GetMapping("/user/admin")
public String admin(){
return "Welcome,This is admin";
}
@ApiOperation("root用户")
@GetMapping("/user/root")
public String root(){
return "Welcome,This is root";
}
@ApiOperation(value = "未授权")
//未授权返回未授权页面
@GetMapping("/unauthorized")
@ResponseBody
public String unauthorized(){
return "Unauthorized access to this page";
}
@ApiOperation(value = "登录")
@PostMapping("/login")
public String toLogin(String username,String password){
//获取当前用户(Shiro 的一个抽象概念,包含了用户信息。)
Subject subject = SecurityUtils.getSubject();
//用来封装用户登录信息,使用用户的登录信息来创建令牌 Token。
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
try {
//把登录业务逻辑交给shiro的subject,subject就是对接的借口人。进入到UserRealm中的认证。(断点认证)
subject.login(usernamePasswordToken);
return "login_Success";
//捕获 UserRealm中可能抛出的异常
} catch (UnknownAccountException e) {
return "Username err";
} catch (IncorrectCredentialsException e){
return "Password err";
}
}
}
(Swagger组件的集成配置、数据库的查询、以及启动类等代码就不粘贴了)
step5:使用Swagger组件进行权限认证测试。
补充:数据库表结构如图:
spring security和shiro的异同
1、认证功能;2、授权功能;3、加密功能;4、会话管理;5、缓存支持 ;6、rememberMe功能;
不同点
Spring Security的优点:
1、Spring Security 基于Spring 开发,项目若使用 Spring 作为基础,配合 Spring Security 做权限更加方便,而 Shiro 需要和 Spring 进行整合开发;
2、Spring Security 功能比 Shiro 更加丰富些,例如安全维护方面;
3、Spring Security 社区资源相对比 Shiro 更加丰富;Spring Security对Oauth、OpenID也有支持,Shiro则需要自己手动实现。而且Spring Security的权限细粒度更高。spring security 接口 RequestMatcher 用于匹配路径,对路径做特殊的请求,类似于shiro的抽象类 PathMatchingFilter,但是 RequestMatcher 作用粒度更细。
shiro的优点:
4、Shiro 的配置和使用比较简单,Spring Security 上手复杂些;
5、Shiro 依赖性低,不需要任何框架和容器,可以独立运行.Spring Security 依赖Spring容器;
6、shiro 不仅仅可以使用在web中,还支持非web项目它可以工作在任何应用环境中。在集群会话时Shiro最重要的一个好处或许就是它的会话是独立于容器的。apache shiro的话,简单,易用,功能也强大,spring官网就是用的shiro,可见shiro的强大。
总结:spring security 和 shiro 对加密都提供了各种各样的支持。例如 :BCryptPasswordEncoder 采用 SHA-256 + 随机盐 + 秘钥 对密码进行加密。shrio 的 SimpleHash 提供散列算法的支持,生成数据的摘要信息。shiro 的 AuthorizingRealm 的 doGetAuthorizationInfo方法 与 doGetAuthenticationInfo 一个是定义获取 用户权限信息 的方法,一个是定义用户身份认证及获取用户身份的方法,而 spring security 也有资源角色授权器FilterInvocationSecurityMetadataSource,定义资源url 与 角色权限的关系 ,决策管理器AccessDecisionManager 定义权限满足的规则。