权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源。权限管理几乎出现在任何系统里面,只要有用户和密码的系统。 很多人常将“用户身份认证”、“密码加密”、“系统管理”等概念与权限管理概念混淆。
在权限管理中使用最多的还是功能权限管理中的基于角色访问控制(RBAC,Role Based Access Control)。
当项目中需要使用权限管理的时候,我们可以选择自己去实现,也可以选择使用第三方实现好的框架去实现,他们孰优孰劣这就需要看大家在项目中具体的需求了。
实现权限管理系统必备的功能:
1.权限管理(自定义权限注解/加载权限)
2.角色管理(新增/编辑/删除/关联权限)
3.用户管理(新增/编辑/删除/关联用户)
4.登录功能(定义登录拦截器/登录逻辑实现/登出功能)
5.权限拦截(定义权限拦截器/拦截逻辑实现)
框架能帮我们解决权限管理系统中的哪些问题呢?
功能 | 权限框架能做的事情 |
权限管理 | × |
角色管理 | × |
用户管理 | × |
登录功能 | √ (密码加密、验证码、记住我) |
权限拦截 | √(内置很多的拦截器、提供标签/注解/编程方式进行权限认证) |
其他功能 | √(缓存、会话管理等) |
常用的权限管理框架
Apache Shiro
Apache Shiro 是一个强大且易用的 Java 安全框架,使用 Apache Shiro 的人越来越多,它可实现身份验证、授权、密码和会话管理等功能。
Spring Security
Spring Security 也是目前较为流行的一个安全权限管理框架,它与 Spring 紧密结合在一起。
Shiro 和 Spring Security 比较
Shiro 比 Spring Security更容易上手使用和理解,Shiro 可以不跟任何的框架或者容器绑定,可独立运行,而Spring Security 则必须要有Spring环境, Shiro 可能没有 Spring Security 做的功能强大,但是在实际工作时可能并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。对于它俩到底哪个好,这个不必纠结,能更简单的解决项目问题就好了。
Shiro
Shiro 可以帮助我们完成:认证、授权、加密、会话管理、与 Web 集成、缓存等。
Shiro 架构
Shiro 主要组件包括:
Subject,SecurityManager,Authenticator,Authorizer,SessionManager,CacheManager,Cryptography,Realms。
Subject用于封装我们传入的账号密码信息,然后委托安全管理器去认证或鉴权等等,安全管理器中的认证器或授权器去调用Realms去数据库中查询想要的数据
-
Subject(用户): 访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体; Subject 一词是一个专业术语,其基本意思是**“封装了当前在操作的用户账号密码信息”**。
在程序任意位置可使用:Subject currentUser = SecurityUtils.getSubject() 获取到subject主体对象,类似 Employee user = UserContext.getUser() (详看RequestContextHolder 工具类这篇文章)
-
SecurityManager(安全管理器):它是 shiro 功能实现的核心,负责与后边介绍的其他组件(认证器/授权器/缓存控制器)进行交互,实现 subject 委托的各种功能。有点类似于SpringMVC 中的 DispatcherServlet 前端控制器,负责进行分发调度。
-
Realms(数据源): Realm 充当了 Shiro 与应用安全数据间的“桥梁”或者“连接器”。;可以把Realm 看成 DataSource,即安全数据源。执行认证(登录)和授权(访问控制)时,Shiro 会从应用配置的 Realm 中查找相关的比对数据。以确认用户是否合法,操作是否合理。
-
Authenticator(认证器): 用于认证(登录),从 Realm 数据源取得数据之后进行执行认证流程处理。
-
Authorizer(授权器):用户访问控制授权(访问权限),决定用户是否拥有执行指定操作的权限。
-
SessionManager (会话管理器):Shiro 与生俱来就支持会话管理,这在安全类框架中都是独一无二的功能。即便不存在 web 容器环境,shiro 都可以使用自己的会话管理机制,提供相同的会话 API。
-
CacheManager (缓存管理器):用于缓存认证授权信息等。
-
Cryptography(加密组件): Shiro 提供了一个用于加密解密的工具包。
Shiro 的认证
认证的过程即为用户的身份确认过程,所实现的功能就是我们所熟悉的登录验证,用户输入账号和密码提交到后台,后台通过访问数据库执行账号密码的正确性校验。
前面我们介绍过,Shiro 不仅在 web 环境中可以使用,在 JavaSE 中一样可以完美的实现相关的功能,
环境准备
- 创建普通的Maven项目
- 添加必要的依赖
<!--日志-->
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
<scope>provided</scope>
</dependency>
快速使用体验,基于ini配置的认证
我们先基于最简单的方式来感受一下Shiro认证的过程,这种方式可以不需要集成MySQL,MyBatis,从而快速体验Shiro框架
- 编写 ini 配置文件:shiro-authc.ini
shiro默认支持的是ini配置的方式,这里只是为了方便,项目中还是会选择xml
# 用户的身份、凭据
[users]
zhangsan=555
lanxw=666
编写测试类,使用 Shiro 相关的 API 完成身份认证
@Test
public void testLoginByIni(){
//创建Shiro的安全管理器,是shiro的核心
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//加载shiro.ini配置,得到配置中的用户信息(账号+密码)
IniRealm iniRealm = new IniRealm("classpath:shiro-authc.ini");
//把配置信息放入安全管理器中
securityManager.setRealm(iniRealm);
//把安全管理器注入到当前的环境中(传入工具类中,方便后续使用)
SecurityUtils.setSecurityManager(securityManager);
//无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
Subject subject = SecurityUtils.getSubject();
System.out.println("认证状态:"+subject.isAuthenticated());//判断这个主体有没有通过认证(登录),此时还没登录成功所以是false
//创建令牌(携带登录用户的账号和密码)
UsernamePasswordToken token = new UsernamePasswordToken("cy","666");
//执行登录操作(将令牌中的账号密码 和 ini 配置中的账号密码做匹配,正确则登录成功,错误则抛出异常)
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());//此时如果账号密码是对的,结果为true
//subject.logout();//退出登录的方法
//System.out.println("认证状态:"+subject.isAuthenticated());
}
如果输入的身份和凭证和 ini 文件中配置的能够匹配,那么登录成功,登录状态为true,反之登录状态为false。
登录失败一般存在两种情况:
- 账号错误 org.apache.shiro.authc.UnknownAccountException
- 密码错误 org.apache.shiro.authc.IncorrectCredentialsException
流程分析
0.初始化IniRealm,内部会读取shiro-authc.ini文件并把用户信息封装到Map<String,SimpleAccount>集合中
0.初始化SecurityManager,把InitRealm交给SecurityManager管理
0.将SecurityManager绑定到当前的环境中
1.通过SecurityUtils工具类获取Subject主体对象
2.Subject对象将Token作为参数,调用login方法完成登录
3.Subject.login方法会将请求分发给SecurityManager中的Authenticator认证器
3.Authenticator认证器会获取配置好的Realm,然后调用里面获取用户认证信息SimpleAccount的方法.
3.如果这个过程返回的是NULL,则会抛出UnknowAccountException
4.如果SimpleAccount返回不为空,则会拿到Subject传入的token,然后获取其凭证(密码).
4.获取SimpleAccount中的凭证(密码)
4.将UsernamePasswordToken中的凭证(密码)和SimpleAccount中的凭证(密码)做比对.
4.如果一致则登录成功,否则就会抛出IncorrectCredentialsException异常
基于自定义Realm的认证
自定义 Realm 在实际开发中使用非常多,通常我们使用的账户信息来自程序或者数据库中, 而不是前面使用到的 ini 文件的配置存储。
Realm的继承体系
继承不同的Realm拥有的功能不一样,肯定是继承子类的功能更多些
自定义Realm
在继承体系中的每个类所能够实现的功能不一样,在后面的开发中,我们通常需要使用到缓存、认证、授权所有的功能,所以选择继承 AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
//提供认证信息
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token就是我们使用subject.login(token)传入的对象,也可以强转成UsernamePasswordToken类型然后getUsername()
//获得传入的账号
String userName = (String) token.getPrincipal();
//模拟从数据库中查询数据
User user = DataMapper.getUserByName(userName);
if(user==null){
//账号不存在则返回NULL
return null;
}else{
//如果存在需要封装成AuthenticationInfo对象返回,认证器会拿token中的密码和我们这里凭证比对是否密码正确
return new SimpleAuthenticationInfo(
user,//身份对象,可以理解为在Web环境中登录成功后需要放入Session中的对象
user.getPassword(),//凭证(密码),需要和传入的凭证(密码)做比对
this.getName());//当前 Realm 的名称,以前用来判断是通过什么方式登录的(多realm,如qq,微信等),现在都是通过第三方接口,用不到了,不需纠结
}
}
//提供授权信息,此次实验暂时用不到
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
}
通过Map的方式来模拟上面从数据库取数据的操作.
//定义User实体
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String username;//用户名
private String password;//密码
}
public class DataMapper {
private static Map<String, User> userData = new HashMap<String, User>();
static{
//初始化数据
User u1 = new User("cy","666");
User u2 = new User("yl","888");
userData.put(u1.getUsername(),u1);
userData.put(u2.getUsername(),u2);
}
//提供静态方法,模拟数据库返回数据
public static User getUserByName(String username){
return userData.get(username);
}
}
注册自定义的Realm,并设置到SecurityManager对象中
@Test
public void testLoginByRealm(){
//创建Shiro的安全管理器,是shiro的核心
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//注册我们自定义的Realm
securityManager.setRealm(new UserRealm());
//把安全管理器注入到当前的环境中
SecurityUtils.setSecurityManager(securityManager);
//无论有无登录都可以获取到subject主体对象,但是判断登录状态需要利用里面的属性来判断
Subject subject = SecurityUtils.getSubject();
System.out.println("认证状态:"+subject.isAuthenticated());
//创建令牌(携带登录用户的账号和密码)
UsernamePasswordToken token = new UsernamePasswordToken("cy","666");
//执行登录操作(将用户的账号密码和Realm读取到的账号密码做匹配)
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
//登出
//subject.logout();
//System.out.println("认证状态:"+subject.isAuthenticated());
}
Web环境如何使用Shiro认证
Shiro的鉴权
-
系统中的授权功能,就是为用户分配相关的权限的过程
-
系统中的鉴权功能 : 判断当前访问用户是否有某个资源的访问权限的过程
如果系统中无法管理用户的权限,那么将会出现客户信息泄露,数据被恶意篡改等问题,所以在绝大多数的应用中,我们都会有权限管理模块。
我们的权限管理系统是基于角色的权限管理,所以在系统中应该有下面三个子模块:
- 用户管理 2. 角色管理 3. 权限管理
那么目前我们所需要的就是将用户拥有的权限告知 Shiro,供其在权限校验的时候使用。
基于ini文件的鉴权体验
编写 ini 配置文件:shiro-author.ini
#用户的身份,凭据,角色
[users]
yl=888,hr,seller
lanxw=666,seller
#角色与权限信息
[roles]
hr=user:list,user:delete
seller=customer:list,customer:save
权限表达式 在 ini 文件中书写配置规则是:
权限字符串也可以使用*通配符
#用户的身份、凭据、角色
[users]
用户名=密码,角色1,角色2...
#角色与权限信息
[roles]
角色=权限1,权限2...
使用 Shiro 相关的 API完成权限校验
@Test
public void testAuthorByIni(){
/*****************************登录逻辑开始**********************************/
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro-author.ini");
securityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("cy","666");
subject.login(token);
/*****************************登录逻辑结束**********************************/
//判断用户是否有某个角色
System.out.println("是否有hr角色?===>"+subject.hasRole("hr"));
System.out.println("是否有seller角色?===>"+subject.hasRole("seller"));
//是否同时拥有多个角色
System.out.println("是否同时拥有hr和seller?===>"+subject.hasAllRoles(Arrays.asList("hr", "seller")));
//check开头的是没有返回值的,当没有权限时就会直接抛出异常
subject.checkRole("seller");
//判断用户是否有某个权限
System.out.println("是否有用户删除权限?===>"+subject.isPermitted("user:delete"));//可以多权限一起判断,返回一个boolean数组
subject.checkPermission("customer:list");
}
基于自定义Realm的鉴权
我们直接复用前面定义的UserRealm即可,需要复写doGetAuthorizationInfo
方法逻辑
模拟数据库的数据
public class DataMapper {
//用户集合
private static Map<String, User> userData = new HashMap<String, User>();
//角色集合
private static Map<String, List<String>> roleData = new HashMap<String, List<String>>();
//权限集合
private static Map<String, List<String>> permissionData = new HashMap<String, List<String>>();
static{
//初始化用户数据
User u1 = new User("la","666");
userData.put(u1.getUsername(),u1);
//记录用户1拥有的角色
roleData.put(u1.getUsername(), Arrays.asList("seller"));
//记录用户1拥有的权限
permissionData.put(u1.getUsername(),Arrays.asList("customer:list","customer:save"));
User u2 = new User("yl","888");
userData.put(u2.getUsername(),u2);
//记录用户2拥有的角色
roleData.put(u2.getUsername(), Arrays.asList("seller","hr"));
//记录用户2拥有的权限
permissionData.put(u1.getUsername(),Arrays.asList("customer:list","customer:save","user:list","user:delete"));
}
//提供静态方法,模拟数据库返回数据
//查询用户账号密码
public static User getUserByName(String username){
return userData.get(username);
}
//查询用户拥有的角色
public static List<String> getRoleByName(String username){
return roleData.get(username);
}
//查询用户拥有的权限
public static List<String> getPermissionByName(String username){
return permissionData.get(username);
}
}
自定义Realm的逻辑
public class UserRealm extends AuthorizingRealm {
//提供认证信息
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token就是我们使用subject.login(token)传入的对象,也可以强转成UsernamePasswordToken类型然后getUsername()
//获得传入的账号
String userName = (String) token.getPrincipal();
//模拟从数据库中查询数据
User user = DataMapper.getUserByName(userName);
if(user==null){
//账号不存在则返回NULL
return null;
}else{
//如果存在需要封装成AuthenticationInfo对象返回,认证器会拿token中的密码和我们这里凭证比对是否密码正确
return new SimpleAuthenticationInfo(
user,//身份对象,可以理解为在Web环境中登录成功后需要放入Session中的对象
user.getPassword(),//凭证(密码),需要和传入的凭证(密码)做比对
this.getName());//当前 Realm 的名称,以前用来判断是通过什么方式登录的(多realm,如qq,微信等),现在都是通过第三方接口,用不到了,不需纠结
}
}
//提供授权信息
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//principals.getPrimaryPrincipal()//获得主要的委托人,这个委托人其实就是在认证时放入SimpleAuthenticationInfo的第一个参数,也就是我们上面放入的user对象
User user = (User) principals.getPrimaryPrincipal();
//创建一个授权信息对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 根据登录用户的名称查询到其拥有的所有角色
List<String> roleSns = DataMapper.getRoleByName(user.getUsername());
// 将用户拥有的角色添加到授权信息对象中,供 Shiro 权限校验时使用
info.addRoles(roleSns);
// 根据登录用户的名称查询到其拥有的所有权限表达式
List<String> expressions = DataMapper.getPermissionByName(user.getUsername());
// 将用户拥有的权限添加到授权信息对象中,供 Shiro 权限校验时使用
info.addStringPermissions(expressions);
//返回授权信息对象,以后在鉴权的时候,授权器就会根据这个授权对象中的信息和需要的权限进行判断
return info;
}
}
使用 Shiro 相关的 API完成权限校验
@Test
public void testAuthorByRealm(){
/*****************************登录逻辑开始**********************************/
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new UserRealm());
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("la","666");
subject.login(token);
/*****************************登录逻辑结束**********************************/
//判断用户是否有某个角色
System.out.println("是否有hr角色?===>"+subject.hasRole("hr"));
System.out.println("是否有seller角色?===>"+subject.hasRole("seller"));
//是否同时拥有多个角色
System.out.println("是否同时拥有hr和seller?===>"+subject.hasAllRoles(Arrays.asList("hr", "seller")));
//check开头的是没有返回值的,当没有权限时就会抛出异常
subject.checkRole("seller");
//判断用户是否有某个权限
System.out.println("是否有用户删除权限?===>"+subject.isPermitted("user:delete"));
subject.checkPermission("customer:list");
}
Shiro的加密
加密的目的是从系统数据的安全考虑,如,用户的密码,如果我们不对其加密,那么所有用户的密码在数据库中都是明文,只要有权限查看数据库的人都能够得知用户的密码,或者数据库被别攻击了,这是非常不安全的。所以,只要密码被写入磁盘,任何时候都不允许是明文, 以及对用户来说非常机密的数据,我们都应该想到使用加密技术,这里我们采用的是 MD5 加密。
如何实现项目中密码加密的功能:
- 添加用户的时候,对用户的密码进行加密
- 登录时,按照相同的算法对表单提交的密码进行加密然后再和数据库中的加密过的数据进行匹配
Shiro的加密工具
在 Shiro 中实现了 MD5 的算法(不可逆加密算法,只能由明文加密成密文,不能从密文解出明文,当然一些简单的明文所对应的MD5加密出来的密文被人记录了,就像下面一样,我知道123456加密出来的密文是什么,我又知道你使用的是什么加密算法,不就知到原本的明文是什么了吗),可以使用MD5来对密码进行加密,不够还要再加点东西,继续看后面。
@Test
public void testMD5() throws Exception{
//创建加密对象,对123456加密
Md5Hash hash = new Md5Hash("123456");
System.out.println(hash);//e10adc3949ba59abbe56e057f20f883e
}
- 加盐加密
但是我们可以对数据加“盐”。同样的数据加不同的“盐”之后就是千变万化的,因为我们不同的人加的“盐”都不一样。这样得到的结果相同率也就变低了。
盐一般要求是固定长度的字符串,且每个用户的盐不同。
可以选择用户的唯一的数据来作为盐(账号名,身份证等等),注意使用这些数据作为盐要求是不能改变的,假如登录账号名改变了,则再次加密时结果就对应不上了。
@Test
public void testMD5() throws Exception{
//明文,盐
Md5Hash hash = new Md5Hash("123456","45cb27");
System.out.println(hash);//173eee408921a3509be87823f2dc307d
}
Md5Hash() 构造方法中的第二个参数就是对加密数据添加的“盐”,加密之后的结果也和之前不一样了。如果还觉得不够安全,我们还可以通过加密次数来增加 MD5 加密的安全性。
@Test
public void testMD5() throws Exception{
Md5Hash hash = new Md5Hash("123456","45cb27",3);
System.out.println(hash);//8fd16c11f8b3fde5bde2c1eaacf6fb02
}
Realm加盐加密
我们在使用Shiro进行认证时,绝大多数情况都是使用Realm方式进行认证.我们要求密码具备一定的安全性,所以我们需要看使用Realm进行加盐加密.
- 在数据库中我们的密码需要存储为密文,同时需要存储对应的盐.
@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class User {
private String username;//用户名
private String password;//密码
private String salt;//盐
}
Realm返回认证对象信息的时候需要把salt盐一并返回
public class UserRealm extends AuthorizingRealm {
//提供认证信息
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token就是我们使用subject.login(token)传入的对象,也可以强转成UsernamePasswordToken类型然后getUsername()
//获得传入的账号
String userName = (String) token.getPrincipal();
//模拟从数据库中查询数据
User user = DataMapper.getUserByName(userName);
if(user==null){
//账号不存在则返回NULL
return null;
}else{
//如果存在需要封装成AuthenticationInfo对象返回,认证器会拿token中的密码和我们这里凭证比对是否密码正确
return new SimpleAuthenticationInfo(
user,//身份对象,可以理解为在Web环境中登录成功后需要放入Session中的对象
user.getPassword(),//凭证(密码),需要和传入的凭证(密码)做比对
ByteSource.Util.bytes(user.getSalt()),//只能用Shiro这个工具类的方法将盐放进去
this.getName());//当前 Realm 的名称,以前用来判断是通过什么方式登录的(多realm,如qq,微信等),现在都是通过第三方接口,用不到了,不需纠结
}
}
登录认证测试
@Test
public void testCredential(){
DefaultSecurityManager securityManager = new DefaultSecurityManager();
UserRealm userRealm = new UserRealm();
//创建哈希凭证信息匹配器,指定加密的算法
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("md5");
//设置加密的次数
matcher.setHashIterations(3);
//放入Realm中来管理和使用这个凭证匹配器
userRealm.setCredentialsMatcher(matcher);
//realm注册到安全管理器中
securityManager.setRealm(userRealm);
//把安全管理器注入到当前的环境中(传入工具类中,方便后续使用)
SecurityUtils.setSecurityManager(securityManager);
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//创建令牌
UsernamePasswordToken token = new UsernamePasswordToken("la","666");
//登录
subject.login(token);
System.out.println("认证状态:"+subject.isAuthenticated());
}
SpringBoot集成Shiro
我们需要使用Shiro框架进行项目的权限控制,所以我们把项目中之前的权限控制相关代码删除掉(如拦截器等)。
添加依赖
在pom里properties标签中添加两个版本参数
<shiro.version>1.7.1</shiro.version>
<thymeleaf.extras.shiro.version>2.0.0</thymeleaf.extras.shiro.version>
依赖
<!--Shiro核心框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Shiro使用Spring框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- Shiro使用EhCache缓存框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>${shiro.version}</version>
</dependency>
<!-- thymeleaf模板引擎和shiro框架的整合 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>${thymeleaf.extras.shiro.version}</version>
</dependency>
集成认证功能
自定义Realm
public class EmployeeRealm extends AuthorizingRealm {
//授权相关
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Employee employee = (Employee)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//根据用户的id查询该用户拥有的角色编码
List<Role> roles = roleService.queryByEmployeeId(employee.getId());
for(Role role:roles){
info.addRole(role.getSn());
}
//根据用户的id查询该用户拥有的权限表达式
List<String> permissions = permissionService.queryByEmployeeId(employee.getId());
info.addStringPermissions(permissions);
return info;
}
//认证相关
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
Employee employee = employeeService.selectByUsername(username);
if(username==null){
return null;
}
return new SimpleAuthenticationInfo(employee,employee.getPassword(),this.getName());
}
}
Web环境需要配置的对象
//配置类
@Configuration
public class ShiroConfig {
/**
* 自定义Realm配置到spring容器中
*/
@Bean
public EmployeeRealm userRealm()
{
EmployeeRealm employeeRealm = new EmployeeRealm();
return employeeRealm;
}
/**
* 配置安全管理器到容器中并注册realm
*/
@Bean
public DefaultWebSecurityManager securityManager(EmployeeRealm employeeRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(employeeRealm);
return securityManager;
}
/**
* Shiro过滤器配置
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 将安全管理器设置到工厂中
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 身份认证失败,则跳转到登录页面的配置
shiroFilterFactoryBean.setLoginUrl("/login.html");
// 权限认证失败,则跳转到指定页面
shiroFilterFactoryBean.setUnauthorizedUrl("/nopermissioin");
// Shiro连接约束配置,即过滤链的定义
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 对静态资源设置可以匿名访问
filterChainDefinitionMap.put("/favicon.ico**", "anon");
filterChainDefinitionMap.put("/logo.png**", "anon");
filterChainDefinitionMap.put("/html/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
//不需要拦截的访问
filterChainDefinitionMap.put("/login", "anon");
// 退出 logout地址,shiro去清除session
filterChainDefinitionMap.put("/logout", "logout");
// 其他所有请求需要认证
filterChainDefinitionMap.put("/**", "authc");
//将过滤规则设置到工厂中
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;//返回工厂创建好的对象放入容器中
}
}
Shiro 配置的过滤器解释
(不为什么不使用拦截器,不会与springMVC强绑定,可以使用不同的表现层框架)来完成不同的预处理操作:
过滤器的名称 | Java 类 |
---|---|
anon | org.apache.shiro.web. lter.authc.AnonymousFilter |
authc | org.apache.shiro.web. lter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web. lter.authc.BasicHttpAuthenticationFilter |
roles | org.apache.shiro.web. lter.authz.RolesAuthorizationFilter |
perms | org.apache.shiro.web. lter.authz.PermissionsAuthorizationFilter |
user | org.apache.shiro.web. lter.authc.UserFilter |
logout | org.apache.shiro.web. lter.authc.LogoutFilter |
port | org.apache.shiro.web. lter.authz.PortFilter |
rest | org.apache.shiro.web. lter.authz.HttpMethodPermissionFilter |
ssl | org.apache.shiro.web. lter.authz.SslFilter |
anon: 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon”
authc: 表示需要认证(登录)才能使用;示例“/**=authc”
authcBasic:Basic HTTP身份验证拦截器
roles: 角色授权拦截器,验证用户是否拥有资源角色;示例“/admin/**=roles[admin]”
perms: 权限授权拦截器,验证用户是否拥有资源权限;示例“/user/create=perms[“user:create”]”
user: 用户拦截器,用户已经身份验证/记住我登录的都可;示例“/index=user”
logout: 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”
port: 端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
rest: rest风格拦截器;
ssl: SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;
注:
anon,authcBasic,auchc,user是认证过滤器,
perms,roles,ssl,rest,port是授权过滤器
1.用户请求被ShiroFilter拦截到,然后会通过PathMatchingFilterChainResolver解析请求
2.会形成内部的过滤器链,请求就会依次执行这些过滤器链
修改登录方法
// 处理登录请求的方法
@RequestMapping("/login")
@ResponseBody
public JsonResult login(String username, String password) {
try{
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
subject.login(token);
//登录成功,前端需要登录的用户账号做回显,放到作用域中(视需求而定)
subject.getSession().setAttribute("EMPLOYEE_IN_SESSION",subject.getPrincipal());
return new JsonResult(true,"登录成功");
}catch(UnknownAccountException e){
return new JsonResult(false,"账号不存在");
}catch (IncorrectCredentialsException e){
return new JsonResult(false,"账号密码有误");
}catch (Exception e){
e.printStackTrace();
return new JsonResult(false,"登录异常,请联系管理员");
}
}
400错误问题解决
在Shiro进行第一次重定向时,会在url后携带jsessionid,这会导致400错误(无法找到该网页)。
原因在于ShiroHttpServletResponse配置类的doIsEncodeable当中,会将url自动拼接jsessionid。
我们需要关闭该功能
/**
* shiro安全管理器中的session管理器
*/
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
//关闭url重写功能
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**
* 将session会话管理器设置到 安全管理器中
*/
@Bean
public DefaultWebSecurityManager securityManager(
EmployeeRealm employeeRealm,
DefaultWebSessionManager sessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(employeeRealm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
集成鉴权功能
Shiro 鉴权三种方式
编程式
通过写 if/else 授权代码块完成
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("hr")) {
//有权限
} else {
//无权限
}
注解式
通过在controller的方法上放置相应的注解完成
@RequiresRoles("hr")
@RequiresPermissions("user:create")
public void addUser(User user) {
}
JSP标签(shiro自带) 、Freemarker的标签(第三方) 、ThymeLeaf的标签(第三方)在页面通过相应的标签完成
<a shiro:hasRole="administrator" href="admin.html">Administer the system</a>
<a shiro:hasPermission="user:create" href="createUser.html">Create a new User</a>
支持注解鉴权需要的配置
在RBAC中,我们采用自定义权限注解贴在需要的方法上,然后再扫描对应类的方法,获取对应的注解生成权限表达式。其中的注解是我们自定义的,很明显,Shiro 权限框架并不认识这个注解,自然也无法完成权限的校验功能,所以我们需要使用 Shiro 自身提供的一套注解来完成,内置的注解需要被识别,所以我们需要添加相关配置让应用程序能识别到这些注解.
在ShiroConfig配置类中添加如下配置
/**
* 开启Shiro注解通知器
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
/**
* 设置支持CGlib代理,因为我们shiro的过滤器虽然拦截了需要登录的路径,但不能拿到方法判断该方法需要的权限注解,配置这个对象,shiro可以生成代理类去执行我们的方法,这样就能拿到权限注解了
* 详情看DefaultAopProxyFactory#createAopProxy
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
使用注解鉴权
在 Controller 的方法上贴上 Shiro 提供的权限注解(@RequiresPermissions,@RequiresRoles)如
@RequestMapping("/list")
@RequiresPermissions("department:list")//这个注解有两个参数一个是这个方法需要的权限集合,另一个是选择需要的权限为 and 或者 or模式,and表示需要这个权限集合里的所有权限,or表示只需要拥有这个权限集合中的任意一个
public String list(Model model, QueryObject qo) {
PageInfo<Department> pageInfo = departmentService.query(qo);
model.addAttribute("pageInfo", pageInfo);
return "department/list";
}
如果用户没有该权限会报如下错误.
我们可以进行统一异常处理,跳转到没有权限的提示页面.
@ControllerAdvice
public class CommonControllerAdvice {
@ExceptionHandler(UnauthorizedException.class)
public String exceptionHandler(UnauthorizedException e){
e.printStackTrace();
return "/nopermission";
}
超级管理员权限
由于超级管理员并没有配置任务的角色和权限,所以目前我们的代码会把超级管理员拦截了.
所以我们需要在授权这块代码中做特殊的处理
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Employee employee = (Employee)principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
if(employee.isAdmin()){
//如果是超级管理员,给管理员赋所有的权限
List<Role> roles = roleService.listAll();
for(Role role:roles){
info.addRole(role.getSn());
}
//使用通配符表示拥有所有的权限
info.addStringPermission("*:*");
}else{
//根据用户的id查询该用户拥有的角色编码
List<Role> roles = roleService.queryByEmployeeId(employee.getId());
for(Role role:roles){
info.addRole(role.getSn());
}
//根据用户的id查询该用户拥有的权限表达式
List<String> permissions = permissionService.queryByEmployeeId(employee.getId());
info.addStringPermissions(permissions);
}
return info;
}
编程式鉴权
有时候我们需要在代码中根据用户的角色或者权限信息做不同的业务逻辑,这时候我们就需要用到编程式的鉴权
@RequestMapping("/list")
public String list(Model model, QueryObject qo) {
Subject subject = SecurityUtils.getSubject();
PageInfo<Department> pageInfo = null;
if(subject.isPermitted("department:list")){
//有权限查询信息
pageInfo = departmentService.query(qo);
}else{
//没权限,抛出UnauthorizedException异常,给到上面统一异常处理跳转没权限页面
}
model.addAttribute("pageInfo", pageInfo);
return "department/list";
}
标签式鉴权
需要导入shiro与ThymmeLeaf的整合依赖,上面导入依赖有写
需要在配置类中配置Shiro集成ThymeLeaf标签支持的一个对象
/**
* thymeleaf模板引擎和shiro框架的整合
*/
@Bean
public ShiroDialect shiroDialect()
{
return new ShiroDialect();
}
ThymmeLeaf模板文件页面中添加约束头
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
在标签中添加shiro属性使用
<a href="#" class="btn btn-success btn-input" style="margin: 10px" shiro:hasPermission="department:saveOrUpdate"> <!--没有这个权限的话,不显示该按钮=-->
<span class="glyphicon glyphicon-plus"></span> 添加
</a>
集成EhCache
我们通过操作发现,每当应用程序进行鉴权的时候,都会调用Realm中的doGetAuthorizationInfo来获取用户的角色信息/权限信息,这个方法是需要访问数据库的. 而用户的角色信息/权限信息基本上是不变的, 所以目前我们的程序是每次鉴权都需要访问数据库,而且返回的数据都是一样的.
我们可以集成EhCache,将角色信息/权限信息都缓存起来,只有用户第一次鉴权的时候才会查询数据库,后续的鉴权都直接从缓存中获取.
添加相关依赖
在resource
目录下新建ehcache/ehcache-shiro.xml
文件,内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<ehcache>
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="600"
timeToLiveSeconds="6000"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
配置属性说明
参数 | 说明 |
---|---|
maxElementsInMemory | 缓存对象最大个数 |
eternal | 对象是否永久有效,false表示超过最大缓存数量时,对象会按设置的淘汰策略淘汰 |
timeToIdleSeconds | 对象空闲时间,指对象在多长时间没有被访问就会失效(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。 |
timeToLiveSeconds | 对象存活时间,指对象从创建到失效所需要的时间,不管这时间内是否被访问(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,默认是 0,也就是对象存活时间无穷大。 |
memoryStoreEvictionPolicy | 当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。 |
缓存淘汰策略:
策略 | 说明 |
---|---|
LRU | 默认,最近最少使用,距离现在最久没有使用的元素将被清出缓存 |
FIFO | 先进先出, 如果一个数据最先进入缓存中,则应该最早淘汰掉 |
LFU | 较少使用,意思是一直以来最少被使用的,缓存的元素有一个hit 属性(命中率(一段时间内被访问的次数)),hit 值最小的将会被清出缓存 |
添加对应的配置
//缓存管理器
@Bean
public EhCacheManager getEhCacheManager(){
EhCacheManager em = new EhCacheManager();
em.setCacheManagerConfigFile("classpath:ehcache/ehcache-shiro.xml");
return em;
}
/**
* 自定义Realm中设置缓存缓存管理器,或者在安全管理器配置中放入这个缓存管理器
*/
@Bean
public EmployeeRealm userRealm(EhCacheManager cacheManager)
{
EmployeeRealm employeeRealm = new EmployeeRealm();
employeeRealm.setCacheManager(cacheManager);
return employeeRealm;
}
配置完后可以多访问一下权限看看控制台是否还有多次执行sql查询权限的日志,没有多次就成功了