目录
一、前置内容
1.什么是权限
权限管理,一般指根据系统设置的安全策略或者安全规则,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。
权限管理在系统中一般分为:访问权限和数权限。
访问权限:一般表示你能做什么样的操作,或者能够访问那些资源。例如:给张三赋予“店铺主管”角色,“店铺主管”具有“查询员工”、“添加员工”、“修改员工”和“删除员工”权限。此时张三能够进入系统,则可以进行这些操作。
数据权限:一般表示某些数据你是否属于你,或者属于你可以操作范围。例如:张三是"店铺主管"角色,他可以看他手下客服人员所有的服务的买家订单信息,他的手下只能看自己负责的订单信息。
2.认证概念
身份认证,就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和密码,看其是否与系统中存储的该用户的用户名和密码一致,来判断用户身份是否正确。例如:密码登录,手机短信验证、三方授权等。
认证流程:
关键对象:
Subject:主体:访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
Principal:身份信息是主体(subject)进行身份认证的标识,标识必须具有唯一性,如用户名、手机号、邮箱地址等,一个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
credential:凭证信息:是只有主体自己知道的安全信息,如密码、证书等。
3.授权概念
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后,系统会为其分配对应的权限,当访问资源时,会校验其是否有访问此资源的权限。
用户对象user:当前操作的用户、程序。
资源对象resource:当前被访问的对象
角色对象role :一组 "权限操作许可权" 的集合。
权限对象permission:权限操作许可权
授权流程:
who + what + how.
Who:主体(Subject),可以是一个用户、也可以是一个程序
What:资源(Resource),如系统菜单、页面、按钮、方法、系统商品信息等。
访问类型:商品菜单,订单菜单、分销商菜单
数据类型:我的商品,我的订单,我的评价
How:权限/许可(Permission)
我的商品(资源)===>访问我的商品(权限许可)
分销商菜单(资源)===》访问分销商列表(权限许可)
二、shiro入门
1.shiro简介
【1】什么是shiro?
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
【2】shiro特点
· 易于理解的 Java Security API;
· 简单的身份认证(登录),支持多种数据源(LDAP,JDBC 等);
· 对角色的简单的签权(访问控制),也支持细粒度的鉴权;
· 支持一级缓存,以提升应用程序的性能;
· 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
· 异构客户端会话访问;
· 非常简单的加密 API;
· 不跟任何的框架或者容器捆绑,可以独立运行。
【3】核心组件
Subject主体,外部应用与subject进行交互,subject将用户作为当前操作的主体,这个主体:可以是一个通过浏览器请求的用户,也可能是一个运行的程序。Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过subject进行认证授,而subject是通过SecurityManager安全管理器进行认证授权
SecurityManager权限管理器,它是shiro的核心,负责对所有的subject进行安全管理。通过SecurityManager可以完成subject的认证、授权等,SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等。SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口
Authenticator即认证器,对用户登录时进行身份认证
Authorizer授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
Realm领域,相当于datasource数据源,securityManager进行安全认证需要通过Realm获取用户权限数据
比如:
如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息。
注意:
不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
SessionManager会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
SessionDAO即会话dao,是对session会话操作的一套接口
CacheManager缓存管理,将用户权限数据存储在缓存,这样可以提高性能
Cryptography密码管理,shiro提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能
2.身份认证
【1】基本流程
1、Shiro把用户的数据封装成标识token,token一般封装着用户名,密码等信息
2、使用Subject门面获取到封装着用户的数据的标识token
3、Subject把标识token交给SecurityManager,在SecurityManager安全中心中,SecurityManager把标识token委托给认证器Authenticator进行身份验证。认证器的作用一般是用来指定如何验证,它规定本次认证用到哪些Realm
4、认证器Authenticator将传入的标识token,与数据源Realm对比,验证token是否合法
【2】演示
①导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
②编写shiro.ini
#声明用户账号
[users]
jay=123
③编写测试类
public class HelloShiro {
@Test
public void shiroLogin() {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("jay", "123");
//登录操作
subject.login(usernamePasswordToken);
System.out.println("是否登录成功:" + subject.isAuthenticated());
}
}
3.Realm
一般在真实的项目中,我们不会直接实现Realm接口,我们一般的情况就是直接继承AuthorizingRealm,能够继承到认证与授权功能。它需要强制重写两个方法:
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 认证
* @param authcToken token对象
* @return
*/
public abstract AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
return null;
}
/**
* @Description 鉴权
* @param principals 令牌
* @return
*/
public abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){
return null;
}
}
下面自定义一个Realm:
public class DefinitionRealm extends AuthorizingRealm {
/**
* @Description 认证接口
* @param token 传递登录token
* @return
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
String password = securityService.findPasswordByLoginName(loginName);
if ("".equals(password)||password==null){
throw new UnknownAccountException("账户不存在");
}
//传递账号和密码
return new SimpleAuthenticationInfo(loginName,password,getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
4.编码、散列算法
【1】编码与解码
hiro提供了base64和16进制字符串编码/解码的API支持,方便一些编码解码操作。
Shiro内部的一些数据的【存储/表示】都使用了base64和16进制字符串
/**
* @Description:封装base64和16进制编码解码工具类
*/
public class EncodesUtil {
/**
* @Description HEX-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeHex(byte[] input){
return Hex.encodeToString(input);
}
/**
* @Description HEX-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeHex(String input){
return Hex.decode(input);
}
/**
* @Description Base64-byte[]--String转换
* @param input 输入数组
* @return String
*/
public static String encodeBase64(byte[] input){
return Base64.encodeToString(input);
}
/**
* @Description Base64-String--byte[]转换
* @param input 输入字符串
* @return byte数组
*/
public static byte[] decodeBase64(String input){
return Base64.decode(input);
}
}
【2】散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”,产生的散列值是“21232f297a57a5a743894a0e4a801fc3”,可以到一些md5解密网站很容易的通过散列值得到密码“admin”,即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据,如salt(即盐);这样散列的对象是“密码+salt”,这样生成的散列值相对来说更难破解。
public class DigestsUtil {
private static final String SHA1 = "SHA-1";
private static final Integer ITERATIONS =512;
/**
* @Description sha1方法
* @param input 需要散列字符串
* @param salt 盐字符串
* @return
*/
public static String sha1(String input, String salt) {
return new SimpleHash(SHA1, input, salt,ITERATIONS).toString();
}
/**
* @Description 随机获得salt字符串
* @return
*/
public static String generateSalt(){
SecureRandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();
return randomNumberGenerator.nextBytes().toHex();
}
/**
* @Description 生成密码字符密文和salt密文
* @param
* @return
*/
public static Map<String,String> entryptPassword(String passwordPlain) {
Map<String,String> map = new HashMap<>();
String salt = generateSalt();
String password =sha1(passwordPlain,salt);
map.put("salt", salt);
map.put("password", password);
return map;
}
}
5.在Realm中使用散列算法
为DefinitionRealm类添加构造方法如下:
/**
* @Description 构造函数
*/
public DefinitionRealm() {
//指定密码匹配方式为sha1
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(DigestsUtil.SHA1);
//指定密码迭代次数
matcher.setHashIterations(DigestsUtil.ITERATIONS);
//使用父亲方法使匹配方式生效
setCredentialsMatcher(matcher);
}
修改DefinitionRealm类的认证doGetAuthenticationInfo方法如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从AuthenticationToken中获得登录名称
String loginName = (String) token.getPrincipal();
SecurityService securityService = new SecurityServiceImpl();
Map<String, String> map = securityService.findPasswordByLoginName(loginName);
if (map.isEmpty()){
throw new UnknownAccountException("账户不存在");
}
String salt = map.get("salt");
String password = map.get("password");
//传递账号和密码:参数1:缓存对象,参数2:明文密码,参数三:字节salt,参数4:当前DefinitionRealm名称
return new SimpleAuthenticationInfo(loginName,password, ByteSource.Util.bytes(salt),getName());
}
6.身份授权
进行授权操作的前提:用户必须通过认证。
【1】基本流程
1、首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager。
2、SecurityManager接着会委托给内部组件Authorizer;
3、Authorizer再将其请求委托给我们的Realm去做;Realm才是真正干活的;
4、Realm将用户请求的参数封装成权限对象。再从我们重写的doGetAuthorizationInfo方法中获取从数据库中查询到的权限集合。
5、Realm将用户传入的权限对象,与从数据库中查出来的权限对象,进行一一对比。如果用户传入的权限对象在从数据库中查出来的权限对象中,则返回true,否则返回false。
【2】演示
①在DefinitionRealm中修改doGetAuthorizationInfo方法如下:
/**
* @Description 授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//拿到用户认证凭证信息
String loginName = (String) principals.getPrimaryPrincipal();
//从数据库中查询对应的角色和资源
SecurityService securityService = new SecurityServiceImpl();
List<String> roles = securityService.findRoleByloginName(loginName);
List<String> permissions = securityService.findPermissionByloginName(loginName);
//构建资源校验
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
authorizationInfo.addRoles(roles);
authorizationInfo.addStringPermissions(permissions);
return authorizationInfo;
}
②编写测试类
public class HelloShiro {
@Test
public void testPermissionRealm() {
Subject subject = shiroLogin("jay", "123");
//判断用户是否已经登录
System.out.println("是否登录成功:" + subject.isAuthenticated());
//---------检查当前用户的角色信息------------
System.out.println("是否有管理员角色:"+subject.hasRole("admin"));
//---------如果当前用户有此角色,无返回值。若没有此权限,则抛 UnauthorizedException------------
try {
subject.checkRole("coder");
System.out.println("有coder角色");
}catch (Exception e){
System.out.println("没有coder角色");
}
//---------检查当前用户的权限信息------------
System.out.println("是否有查看订单列表资源:"+subject.isPermitted("order:list"));
//---------如果当前用户有此权限,无返回值。若没有此权限,则抛 UnauthorizedException------------
try {
subject.checkPermissions("order:add", "order:del");
System.out.println("有添加和删除订单资源");
}catch (Exception e){
System.out.println("没有有添加和删除订单资源");
}
}
/**
* @Description 登录方法
*/
private Subject shiroLogin(String loginName,String password) {
//导入权限ini文件构建权限工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//工厂构建安全管理器
SecurityManager securityManager = factory.getInstance();
//使用SecurityUtils工具生效安全管理器
SecurityUtils.setSecurityManager(securityManager);
//使用SecurityUtils工具获得主体
Subject subject = SecurityUtils.getSubject();
//构建账号token
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginName, password);
//登录操作
subject.login(usernamePasswordToken);
return subject;
}
}
三、Springboot继承Shiro