文章目录
1.JavaWeb中的权限控制
(1)什么是权限控制
- 忽略特别细的概念,比如权限能细分很多种,功能权限,数据权限,管理权限等
- 理解两个概念:用户和资源,让指定的用户,只能操作指定的资源(CRUD)
(2)javaweb中怎么处理
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws Exception {
HttpServletRequest httpRequest=(HttpServletRequest)request;
HttpServletResponse httpResponse=(HttpServletResponse)response;
HttpSession session=httpRequest.getSession();
if(session.getAttribute("username")!=null){
chain.doFilter(request, response);
} else {
httpResponse.sendRedirect(httpRequest.getContextPath()+"/login.jsp");
}
}
2.权限框架核心知识ACL和RBAC
2.1.ACL和RBAC简介
- ACL:Access Control List 访问控制列表
- 以前盛行的一种权限设计,它的核心在于用户直接和权限挂钩
- 优点:简单易用、开发便捷
- 缺点:用户和权限直接挂钩,导致在授权时的复杂性,比较分散,不便于管理
- 案例:常见的文件系统权限设计,直接给用户加权,类似Linux系统中的chmod
- RBAC:Role Based Access Control
- 基于角色的访问控制系统。权限与角色相关联,用户通过适当的角色的成员而获得角色的权限
- 优点:简化了用户与权限的管理,通过对用户进行分类,使得角色与权限关联起来
- 缺点:开发相比ACL复杂
- 案例:基于RBAC模型的权限验证框架有Apache Shiro、Spring Security
- 总结:权限设计不能太过于复杂,否则性能会下降
2.2主流权限框架介绍
(1)什么是Spring Security
- 官网:https://spring.io/projects/spring-security
Spring Security是一个能够基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了企业系统安全控制编写大量重复代码的工作。
(2)什么是Apache Shiro
- 官网:https://github.com/apache/shiro
Apache Shiro是一个强大且易用的java安全框架,执行身份验证、授权、密码和会话管理。使用shiro的易于理解的API,可以快速、轻松的执行任何应用程序。
- 两个的优缺点
- Apache Shiro比Spring Security使用更简单
- Shiro功能强大、简洁、灵活,不跟任何的框架或者容器绑定,可以独立运行
- SpringSecurity对Spring体系支持比较友好,脱离Spring体系开发很难
- SpringSecuity支持Oauth鉴权,Shiro需要自己实现
3.Shiro架构和基本概念
3.1.Shiro的4大核心模块
(1)Shiro的四大核心模块分为身份认证、授权、会话管理和加密
- 身份认证
- Authentication,身份认证,一般就是登录
- 授权
- Authorization,给用户分配角色或者访问某些资源的权限
- 会话管理
- Session Management,用户的会话管理员,多数情况下是web Session
- 加密
- Cryptogarphy,数据加解密,你如密码加解密等
(2)Shiro架构图
3.2.Shiro权限控制运行流程
(1)Shiro常见名称
- Subject
- 我们把用户或者程序称为主体,主体去访问资源或者系统
- SecurityManager
- 安全管理器,Subject的认证和授权都在安全管理器下进行
- Authenticator
- 认证器,主要负责Subject的认证
- Realm
- 数据域,Shiro和安全数据的连接器,好比jdbc连接数据库;通过realm获取认证授权的相关信息
- Authorizer
- 授权器,主要负责Subject的授权,控制subject拥有的角色或者权限
- Cryptography
- 加解密,Shiro包含易于使用和理解的数据加密方法,简化了很多复杂的API
- CacheManager
- 缓存管理器,比如认证或者授权信息,通过缓存进行管理,提高性能
- SessionManager
- 会话管理器,大多数是web session
- SessionDAO
- SessionDAO即会话,是对session会话的一套接口,比如要将session存储到数据库。
![](images/3.1%282%29.jpg)
4.Shiro简单API案例
4.1.项目搭建所需依赖
- 环境准备:maven3.5+jdk8+springboot+idea
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mysql starter 注意一定要把runtime去掉-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--测试模块starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--阿里巴巴数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<!--shiro相关依赖包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
4.2.Shiro认证简单实操
(1)Shiro的认证流程
- 创建Security Manager:Security Manager是用来提供安全服务的,所以在做shiro认证的时候要先创建此对象
- 主题Subject提交请求给Security Manager
- Security Manager调用Authenticator组件做认证
- Authenticator通过Realm来从数据源中获取认证数据
(2)编码测试
@SpringBootTest
public class Test{
//声明SecurityManager
DefaultSecurityManager securityManager = new DefaultSecurityManager();
//声明Realm
SimpleAccountRealm accountRealm = new SimpleAccountRealm();
@BeforeTest
public void init(){
accountRealm.addAccount("lixiang","123456");
accountRealm.addAccount("lisi","123456");
//构建环境
securityManager.setRealm(accountRealm);
}
@Test
public void test(){
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernameAndPasswordToken token = new UsernameAndPasswordToken("lixiang","123456");
subject.login(token);
System.out.println("认证结果:"+subject.isAuthenticated());
}
}
(3)测试结果
4.3.Shiro授权简单实操
(1)常用API
//是否有对应角色
subject.hasRole("root")
//获取subject名
subject.getPrincipal()
//检查是否有对应的角色,无返回值,直接在SecurityManager里面进行判断
subject.checkRole("admin")
//检查是否有对应的角色
subject.hasRole("admin")
//退出登录
subject.logout();
(2)编码实操
@Test
void contextLoads() {
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("lixiang","123456");
subject.login(usernamePasswordToken);
System.out.println("认证结果:"+subject.isAuthenticated());
System.out.println("获取subject主体的唯一标识:"+subject.getPrincipal());
//检查是否有对应角色,无返回值,直接在SecurityManager里面进行判断
subject.checkRole("admin");
//检查是否有对应的角色
System.out.println("是否有对应角色:"+subject.hasRole("admin"));
//退出登录
subject.logout();
System.out.println("认证结果:"+subject.isAuthenticated());
}
5.安全数据来源Realm
5.1.Realm简介和继承关系
- Realm的作用:Shiro从Realm中获取安全的数据
- Realm中的两个概念:
- principal:主体的标识,可以有多个,但是必须要有一个唯一性的,常见的用户名、手机号、邮箱
- credential:访问凭证,一般就是密码
- 如果要自定义Realm,继承AuthorizingRealm
- Realm:顶级接口,所有类的父接口
- CachingRelam:带有缓存功能的Realm抽象类
- AuthenticatingRealm:带有认证功能的Realm抽象类
- AuthorizingRealm:带有授权功能的Realm抽象类
- SimpleAccountRealm:提供一些简单的Realm认证
- TextConfigurationRealm:提供文本形式的Realm认证
- IniRealm和PropertiesRealm:TextConfigurationRealm的子类,细化文本验证方式
- JdbcRealm:与数据库交互的Realm认证
- DefaultLdapRealm:根据LDAP进行身份验证
5.2.Shiro内置IniRealm权限验证
(1)新建shiro.ini文本文件,编写规则
#用户模块,对应用户名、密码、角色,多个角色之间用逗号隔开
[users]
lixiang = 123456,user
zhangsan = 123456,admin,root
#权限模块,对应角色名称、对应权限,多个权限用,分隔
[roles]
user = video:find,video:buy
admin = video:*
root = *
(2)测试编码
@Test
public void test(){
//创建IniSecurityManagerFactory工厂实例,注意这块一定要是shiro下的包
//IniSecurityManagerFactory这个类已经废弃了,这里只做验证
Factory<SecurityManager> factory = new IniSecurityManagerFactory();
//获取工厂实例
SecurityManager securityManager = factory.getInstance();
//将securityManager设置到当前运行环境当中
SecurityUtils.setSecurityManager(securityManager);
//获取Subject对象
Subject subject = SecurityUtils.getSubject();
//创建登录Token
UsernameAndPasswordToken token = new UsernameAndPasswordToken("lixiang","123456");
//验证
subject.login(token);
//判断是否有对应角色
System.out.print("判断是否有对应角色:"+subject.hasRole("admin"));
//判断是否有对应的权限
System.out.print("判断是否有对应权限:"+subject.isPermitted("video:find"));
//判断是否有对应的权限,无返回值,如果检验不通过则抛出异常
//checkPermission("find:video")
}
5.3.Shiro内置JdbcRealm权限验证
(1)配置jdbcrealm.ini文件,注意这块一定要是ANSI格式否则运行会抛错
#注意 文件格式必须为ini,编码为ANSI
#声明Realm,指定realm类型
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
#配置数据源
#dataSource=com.mchange.v2.c3p0.ComboPooledDataSource
dataSource=com.alibaba.druid.pool.DruidDataSource
# mysql-connector-java 5 用的驱动url是com.mysql.jdbc.Driver,mysql-connector-java6以后用的是com.mysql.cj.jdbc.Driver
dataSource.driverClassName=com.mysql.cj.jdbc.Driver
#避免安全警告
dataSource.url=jdbc:mysql://120.76.62.13:3606/xdclass_shiro?characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
dataSource.username=test
dataSource.password=Xdclasstest
#指定数据源
jdbcRealm.dataSource=$dataSource
#开启查找权限, 默认是false,不会去查找角色对应的权限,坑!!!!!
jdbcRealm.permissionsLookupEnabled=true
#指定SecurityManager的Realms实现,设置realms,可以有多个,用逗号隔开
securityManager.realms=$jdbcRealm
- 如果编码不是ANSI格式
(2)验证
配置文件中 jdbcRealm.permissionsLookupEnabled=true 一定要设置成true,默认是false不会去校验角色
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdIHQpbi-1667452035146)(images/5.2(3)].jpg)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TjSb8TpI-1667452035146)(images/5.2(4)].jpg)
@Test
void contextLoads() {
//创建SecurityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:jdbcrealm.ini");
//拿到工厂
SecurityManager securityManager = factory.getInstance();
//将securityManager设置到当前运行环境当中
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("jack","123");
subject.login(token);
System.out.println("认证结果:"+subject.isAuthenticated());
System.out.println("是否有对应的角色:"+subject.hasRole("user"));
//查询是否有权限,无返回值,没有则抛异常
//subject.checkPermission("video:delete");
//查询是否有权限,有返回值
System.out.println(subject.isPermitted("video:delete"));
}
5.4.Shiro自定义Realm权限配置
(1)自定义Realm步骤
(1)创建一个类,继承AuthorizingRealm->AuthenticatingRealm->CachingRealm->Realm
(2)重写授权方法:doGetAuthorizationInfo(进行权限校验的时候会调用)
(3)重写认证方法:doGetAuthenticationInfo(当用户登陆的时候会调用)
(2)对象介绍
- UsernamePasswordToken : 对应就是 shiro的token中有Principal和Credential
- UsernameAndPasswordToken->HostAuthenticationToken->AuthenticationToken
- SimpleAuthorizationInfo:代表用户角色权限信息
- SimpleAuthenticationInfo:代表该用户的认证信息
(3)编写自定义的Realm类
public class CustomRealm extends AuthorizingRealm {
//user
private final static Map<String,String> userMaps = new HashMap<>();
{
userMaps.put("lixiang","123");
userMaps.put("lisi","123");
}
//roles - > permission
private final static Map<String,Set<String>> permissionMaps = new HashMap<>();
{
Set<String> set1 = new HashSet<>();
Set<String> set2 = new HashSet<>();
set1.add("video:find");
set1.add("video:buy");
set2.add("video:add");
set2.add("video:delete");
permissionMaps.put("lixiang",set1);
permissionMaps.put("lisi",set2);
}
//user -> role
private final Map<String,Set<String>> roleMap = new HashMap<>();
{
Set<String> set1 = new HashSet<>();
Set<String> set2 = new HashSet<>();
set1.add("role1");
set1.add("role2");
set2.add("root");
roleMap.put("jack",set1);
roleMap.put("xdclass",set2);
}
/**
* 进行权限验证的时候调用
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("进行权限验证doGetAuthorizationInfo");
String username = principals.getPrimaryPrincipal().toString();
Set<String> permissions = getPermissionsfromDB(username);
Set<String> roles = getRolesfromDB(username);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
return simpleAuthorizationInfo;
}
/**
* 通过用户名查找角色
* @param username
* @return
*/
private Set<String> getRolesfromDB(String username) {
return roleMap.get(username);
}
/**
* 通过用户名查找权限
* @param username
* @return
*/
private Set<String> getPermissionsfromDB(String username) {
return permissionMaps.get(username);
}
/**
* 进行身份验证的时候调用
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println(" 进行身份验证doGetAuthenticationInfo");
String username = token.getPrincipal().toString();
String pwd = getPwdfromDB(username);
if("".equals(pwd) || pwd == null){
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username,pwd,this.getName());
return simpleAuthenticationInfo;
}
private String getPwdfromDB(String username) {
return userMaps.get(username).toString();
}
}
(4)测试
SecurityUtils.setSecurityManager(securityManager);
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("lixiang","123");
//登录
subject.login(token);
//唯一标识
System.out.println("用户名:"+subject.getPrincipal());
System.out.println("是否有对应的角色:"+subject.hasRole("role1"));
System.out.println("是否有对应的权限:"+subject.isPermitted("video:find"));
5.5.Shiro源码认证授权流程
认证流程:
- subject.login(token)
- DelegatingSubject.login(token)
- AuthenticatingSecurityManager.authenticate(token)
- AbstractAuthenticator.authenticate(token)
- ModulearRealmAuthenticator.doAuthenticate(token)
- ModulearRealmAuthenticator.doSingleRealmAuthentication(token)
- AuthenticatingRealm.getAuthenticationInfo(token)
鉴权流程:
- subject.checkRole(“admin”)
- DelegatingSubject.checkRole()
- AuthorizingSecurityManager.checkRole()
- ModulatRealmAuthorizer.checkRole()
- AuthorizingReaim,hasRole()
- AuthorizingRealm.doGetAuthorizationInfo()
6.Shiro权限认证Web案例
6.1.Shiro内置的过滤器
- 核心过滤器类:DefaultFilter,配置那个路径对应那个拦截器进行处理
authc:org.apache.shiro.web.filter.authc.FromAuthenticationFilter
- 需要认证登录才能访问
user:org.apache.shiro.web.filter.authc.UserFilter
- 用户拦截器,表示必须存在用户
anon:org.apache.shiro.web.filter.authc.AnonymousFilter
- 匿名拦截器,不需要登录即可访问的资源,匿名用户或者游客,一般用于过滤静态资源
roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
- 角色授权拦截器,验证用户是否拥有角色。
- 参数可以写多个,多个参数时写roles["admin","user"],当多个参数时必须每个参数都通过才算通过。
perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
- 权限授权拦截器,验证用户是否拥有权限
- 参数可写多个,和角色多个是一致的
authcBasic:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
- httpBasic身份验证拦截器
logout:org.apache.shiro.web.filter.authc.LogoutFilter
- 退出拦截器,执行后会执行跳转到shiroFilterFactoryBean.setLoginUrl()设置的url
port:org.apache.shiro.web.filter.authz.PortFilter
- 端口拦截器,可通过的端口
ssl:org.apache.shiro.web.filter.authz.SslFilter
- ssl拦截器,只有请求协议是https才能通过
6.2.Shiro的Filter配置路径
- 路径支持通配符,完整匹配,注意匹配符不包括分隔符"/"
- 路径通配符支持?、*、**,注意通配符匹配不包括目录分隔符"/"
- * 可以匹配所有。不加 * 可以进行前缀匹配,但多个冒号就需要多个 * 来匹配
URL权限采取第一次匹配优先的方式,优先匹配靠前的规则
?:匹配一个字符,如/user? 匹配/user3,但不匹配/user/
*:匹配0个或多个字符串,如/add*,匹配/addtest,但不匹配/add/1
** : 匹配路径中的零个或多个路径,如 /user/** 将匹 配 /user/xxx 或 /user/xxx/yyy
例子
/user/**=filter1
/user/add=filter2
请求 /user/add 命中的是filter1拦截器
- 性能问题:通配符比字符串匹配会复杂点,所以性能也会稍弱,推荐使用字符匹配方式
6.3.Shiro数据安全之数据加解密
(1)为啥要加解密
- 明文数据容易泄露,比如密码铭文存储,万一泄露则会造成严重的后果
(2)什么是散列算法
- 一般叫hash,简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数,适合存储密码,比如MD5
(3)什么是salt(盐)
- 如果直接通过散列函数得到的加密数据,容易被对应解密网站暴力破解,一般会在应用层序里面加特殊的自动进行处理,比如用户id等等,唯一标识的东西。
(4)Shiro里面CredentialsMatcher,用来验证密码是否正确
源码:AuthenticatingRealm -> assertCredentialsMatch()
(5)自定义验证规则
一般会自定义验证规则
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//散列算法,使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次数,比如散列两次,相当于 md5(md5("xxx"));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
6.4.Shiro权限控制注解
@RequiresRoles(value={"admin","editor"},logical=Logical.AND)
需要角色admin和editor两个角色同时满足
@RequiresRoles(value={"admin","editor"},logical=Logical.OR)
需要角色admin或editor两个角色其中一个满足
@RequiresAuthentication
已经授过权,调用Subject.isAuthenticated()返回true
@RequiresUser
身份验证或者通过记 住我登录的
查用API
subject.hasRole("xxx")
subject.isPermitted("xxx")
subject.isPermittedAll("xxx","xxx")
subject.checkRole("xxx")
6.5.Shiro缓存模块讲解
- AuthenticatingRealm 及 AuthorizingRealm 分别提供了对AuthenticationInfo 和 AuthorizationInfo 信息的缓存.
6.6.Shiro Session模块讲解
(1)什么是session会话
用户和程序直接的链接,程序可以根据session识别到哪个用户,和javaweb中的session类似
(2)什么是会话管理器SessionManager
- 会话管理器管理所有subject的所有操作,是shiro的核心组件
- 核心方法
//开启一个session
Session start(SessionContext context)
//指定key获取session
Session getSession(SessionKey key)
(3)SessionDao会话存储/持久化
-
SessionDAO
- AbstractSessionDAO
- CachingSessionDAO
- EnterpeiseCacheSessionDAO
- MemorySessionDAO
- CachingSessionDAO
- AbstractSessionDAO
-
核心方法
//创建
Serializable create(Session session)
//获取
Session readSession(Serializable sessionId) throws UnknownSessionException
//更新
void update(Session session)
//删除,会话过期时调用
void delete(Session session)
//获取活跃的session
Collection<Session> getActiveSessions()
RememberMe
1.Cookie写到客户端并保存
2.通过调用subject.login()前,设置 token.setRememberMe(true)
- subject.isAuthenticated() 表示用户进行了身份验证登录的,即Subject.login 进行了登录
- subject.isRemembered() 表示用户是通过RememberMe登录的
- subject.isAuthenticated()==true,则 subject.isRemembered()==false, 两个互斥
- 总结:特殊页面或者API调用才需要authc进行验证拦截,该拦截器会判断用户是否是通过
7.SpringBoot2.x整合Shiro
7.1.数据库设计
- user表
CREATE TABLE `user` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(128) DEFAULT NULL COMMENT '用户名',
`password` varchar(256) DEFAULT NULL COMMENT '密码',
`create_time` datetime DEFAULT NULL,
`salt` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- role表
CREATE TABLE `role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL COMMENT '名称',
`description` varchar(64) DEFAULT NULL COMMENT '描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
- user_role表
CREATE TABLE `user_role` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL,
`user_id` int(11) DEFAULT NULL,
`remarks` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
- permission表
CREATE TABLE `permission` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL COMMENT '名称',
`url` varchar(128) DEFAULT NULL COMMENT '接口路径',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
- role_permission表
CREATE TABLE `role_permission` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`role_id` int(11) DEFAULT NULL,
`permission_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
7.2.Maven项目搭建
创建SpringBoot项目,引入依赖,配置数据库
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--阿里巴巴druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<!--spring整合shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</pluginRepository>
</pluginRepositories>
#==============================数据库相关配置========================================
spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.10.88:3306/rbac_shiro?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username =root
spring.datasource.password =123456
#使用阿里巴巴druid数据源,默认使用自带的
#spring.datasource.type =com.alibaba.druid.pool.DruidDataSource
#开启控制台打印sql
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# mybatis 下划线转驼峰配置,两者都可以
#mybatis.configuration.mapUnderscoreToCamelCase=true
mybatis.configuration.map-underscore-to-camel-case=true
7.3.编写查询用户全部信息接口
(1)实体类编写
- User
/**
* 用户表
*/
public class User {
private int id;
private String username;
private Date createTime;
private String salt;
private List<Role> roleList;
}
- Role
/**
* 角色表
*/
public class Role {
private int id;
private String name;
private String description;
private List<Permission> permissionList;
}
- UserRole
/**
* 用户角色中间表
*/
public class UserRole {
private int id;
private int userId;
private int roleId;
}
- Permission
/**
* 权限表
*/
public class Permission {
private int id;
private String name;
private String url;
}
- RolePermission
/**
* 权限角色中间表
*/
public class RolePermission {
private int id;
private int roleId;
private int permissionId;
}
(2)Mapper编写
- UserMapper
public interface UserMapper {
@Select("select * from user where username = #{username}")
User findByUsername(@Param("username") String username);
@Select("select * from user where id = #{id}")
User findById(@Param("id") int id);
@Select("select * from user where username = #{username} and password = #{pwd}")
User findByUsernameAndPwd(@Param("username") String username,@Param("pwd") String pwd);
}
- RoleMapper
public interface RoleMapper {
@Select("select * from user_role where user_id = #{userId}")
List<UserRole> findRolesByUserId(@Param("userId") int userId);
@Select("select * from role where id = #{roleId}")
List<Role> findRolesByRoleId(@Param("roleId") int roleId);
}
- PermissionMapper
public interface PermissionMapper {
@Select("select * from permission where id = #{roleId}")
List<Permission> findPermissionsByRoleId(@Param("roleId") int roleId);
}
(3)UserService编写
- UserService
public interface UserService {
/**
* 获取用户全部信息,包括角色权限
* @param username
* @return
*/
User findAllUserInfoByUsername(String username);
/**
* 获取用户基本信息
* @param userId
* @return
*/
User findSimpleUserInfoById(int userId);
/**
* 获取用户基本信息
* @param username
* @return
*/
User findSimpleUserInfoByUsername(String username);
}
- UserServiceImpl
@Service
public class UserServiceImpl implements UserService {
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
public User findAllUserInfoByUsername(String username) {
User user = userMapper.findByUsername(username);
List<UserRole> userRoles = roleMapper.findRolesByUserId(user.getId());
List<Role> roles = new ArrayList<>();
for (UserRole role : userRoles) {
roles = roleMapper.findRolesByRoleId(role.getRoleId());
for (Role x : roles) {
x.setPermissionList(permissionMapper.findPermissionsByRoleId(x.getId()));
}
}
user.setRoleList(roles);
return user;
}
@Override
public User findSimpleUserInfoById(int userId) {
return userMapper.findById(userId);
}
@Override
public User findSimpleUserInfoByUsername(String username) {
return userMapper.findByUsername(username);
}
}
(5)controller编写
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/find_user")
public Object findUserInfo(@RequestParam("username") String username){
return userService.findAllUserInfoByUsername(username);
}
}
(6)测试
7.4.开发自定义CustomRealm
- 继承 AuthorizingRealm
- 重写 doGetAuthorizationInfo
- 重写 doGetAuthenticationInfo
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 用户鉴权的时候会调用
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("鉴权 doGetAuthorizationInfo");
String username = (String)principals.getPrimaryPrincipal();
User userInfo = userService.findAllUserInfoByUsername(username);
//将角色,权限放到对应的两个String类型集合中
List<String> stringRoleList = new ArrayList<>();
List<String> stringPermissionList = new ArrayList<>();
List<Role> roleList = userInfo.getRoleList();
for (Role role : roleList) {
stringRoleList.add(role.getName());
List<Permission> permissionList = role.getPermissionList();
for (Permission permission : permissionList) {
if(permission!=null){
stringPermissionList.add(permission.getName());
}
}
}
//拿到对应的角色集合,权限集合,封装SimpleAuthorizationInfo对象添加
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addRoles(stringRoleList);
simpleAuthorizationInfo.addStringPermissions(stringPermissionList);
return simpleAuthorizationInfo;
}
/**
* 用户认证登录的时候会调用
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("认证 doGetAuthenticationInfo");
//从token中拿到username
String username = (String)token.getPrincipal();
User userInfo = userService.findAllUserInfoByUsername(username);
//获取密码
String password = userInfo.getPassword();
if(password == null || "".equals(password)){
return null;
}
//密码不为空,说明认证成功,封装SimpleAuthenticationInfo对象,参数:用户名,密码,class.getName
return new SimpleAuthenticationInfo(username,userInfo.getUsername(),this.getClass().getName());
}
}
7.5.ShiroFilterFactoryBean配置
- shiroFilterFactoryBean-》
- SecurityManager-》
- CustomSessionManager
- CustomRealm-》hashedCredentialsMatcher
- SecurityManager-》
- SessionManager
- DefaultSessionManager: 默认实现,常用于javase
- ServletContainerSessionManager: web环境
- DefaultWebSessionManager:常用于自定义实现
@Configuration
public class ShiroConfig {
/**
* 设置shiroFilter
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//设置登录成功后访问的路径
shiroFilterFactoryBean.setLoginUrl("/pub/need_login");
//设置登录成功后访问的url
shiroFilterFactoryBean.setSuccessUrl("/");
//设置无权限访问的接口,未授权无法访问接口
shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit");
//拦截器路径配置,注意这里一定要用LinkedHashMap,HashMap会出现偶尔无法拦截的情况
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
//设置过滤器
//登出过滤器,用户退出时调用
filterChainDefinitionMap.put("/logout","logout");
//匿名过滤器,游客模式
filterChainDefinitionMap.put("/pub/**","anon");
//登录过滤器,用户只有登录才能访问
filterChainDefinitionMap.put("/authc/**","authc");
//管理员角色才能访问
filterChainDefinitionMap.put("/admin/**","roles[admin]");
//指定权限才能访问
filterChainDefinitionMap.put("/video/update","perms[video_update]");
filterChainDefinitionMap.put("/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 注入securityManager,设置Realm和SessionManager
* @return
*/
@Bean
public SecurityManager securityManager(){
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(customRealm());
//注意如果不是前后端分离的项目就不需要设置sessionManager
securityManager.setSessionManager(customSessionManager());
return securityManager;
}
/**
* 设置散列算法
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//设设置散列算法,md5
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//设置散列次数
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
/**
* 注入自定义的Realm
* @return
*/
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm = new CustomRealm();
customRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return customRealm;
}
/**
* 注入自定义的SessionManager
* @return
*/
@Bean
public CustomSessionManager customSessionManager(){
CustomSessionManager customSessionManager = new CustomSessionManager();
//超时时间,默认30分钟不操作就会过期,单位豪秒
customSessionManager.setGlobalSessionTimeout(20000);
return customSessionManager;
}
}
7.6.自定义SessionManager验证
public class CustomSessionManager extends DefaultWebSessionManager {
private static final String TOKEN="token";
//调用父类构造方法,以防后续有人修改构造,空构造覆盖,会出问题
public CustomSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String sessionId = WebUtils.toHttp(request).getHeader(TOKEN);
//如果sessionId不为空,就调用自定义的逻辑,如果为空就调用父类的方法
if(sessionId!=null){
//调用shiro内部的校验,检测sessionId是否存在,是否过期
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,
ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return sessionId;
}else{
return super.getSessionId(request,response);
}
}
}
7.7.API拦截验证案例
(1)AdminController
@RestController
@RequestMapping("/admin")
public class AdminController {
@RequestMapping("/order")
public JsonData findOrder(){
Map<String,String> recordMap = new HashMap<>();
recordMap.put("SpringBoot入门到高级实战","300元");
recordMap.put("Cloud微服务入门到高级实战","877元");
recordMap.put("分布式缓存Redis","990元");
return JsonData.buildSuccess(recordMap);
}
}
(2)LogoutController
@RestController
public class LogoutController {
@RequestMapping("/logout")
public JsonData logout(){
String username = (String)SecurityUtils.getSubject().getPrincipal();
if(username!=null){
SecurityUtils.getSubject().logout();
return JsonData.buildSuccess("logout成功");
}
return JsonData.buildError("logout失败");
}
}
(3)OrderController
@RestController
@RequestMapping("/authc")
public class OrderController {
@RequestMapping("/save")
public JsonData findMyPlayRecord(){
Map<String ,String> recordMap = new HashMap<>();
recordMap.put("SpringBoot入门到高级实战","第8章第1集");
recordMap.put("Cloud微服务入门到高级实战","第4章第10集");
recordMap.put("分布式缓存Redis","第10章第3集");
return JsonData.buildSuccess(recordMap);
}
}
(4)pubController
@RestController
@RequestMapping("/pub")
public class PubController {
@RequestMapping("/need_login")
public JsonData needLogin(){
return JsonData.buildSuccess("温馨提示:请使用对应的账号登录",-2);
}
@RequestMapping("not_permit")
public JsonData notPermit(){
return JsonData.buildSuccess("温馨提示:拒绝访问,没权限",-3);
}
@RequestMapping("/index")
public JsonData index(){
List<String> videoList = new ArrayList<>();
videoList.add("Mysql零基础入门到实战 数据库教程");
videoList.add("Redis高并发高可用集群百万级秒杀实战");
videoList.add("Zookeeper+Dubbo视频教程 微服务教程分布式教程");
videoList.add("2019年新版本RocketMQ4.X教程消息队列教程");
videoList.add("微服务SpringCloud+Docker入门到高级实战");
return JsonData.buildSuccess(videoList);
}
@PostMapping("/login")
public JsonData login(@RequestBody UserQuery userQuery, HttpServletRequest request, HttpServletResponse response){
Subject subject = SecurityUtils.getSubject();
System.out.println("userQuery:"+userQuery);
Map<String,Object> info = new HashMap<>();
try {
UsernamePasswordToken token = new UsernamePasswordToken(userQuery.getUsername(),userQuery.getPwd());
subject.login(token);
info.put("session_id",subject.getSession().getId());
return JsonData.buildSuccess(info,"登录成功");
}catch (Exception e){
info.put("session_id",subject.getSession().getId());
return JsonData.buildError("账号或密码错误");
}
}
}
(5)VideoController
@RestController
@RequestMapping("/video")
public class VideoController {
@RequestMapping("/update")
public JsonData updateVideo(){
return JsonData.buildSuccess("video 更新成功");
}
}
(6)用户角色权限分配图
(7)测试
7.8.Shiro密码加密处理
@SpringBootTest
class RbacShiroApplicationTests {
@Test
void contextLoads() {
//加密算法
String hashName = "md5";
//密码明文
String pwd = "123456";
//加密函数,使用shiro自带的
SimpleHash hash = new SimpleHash(hashName, pwd, null, 2);
System.out.println(hash);
}
}
8.权限控制性能提升
8.1.自定义Shiro Filter过滤器
(1)shiro默认的roles过滤器存在的问题
(2)自定义过滤器类,继承AuthorizationFilter
public class CustomRolesAuthorizationFilter extends AuthorizationFilter {
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
Subject subject = getSubject(request, response);
String[] rolesArray = (String[]) mappedValue;
if (rolesArray == null || rolesArray.length == 0) {
//no roles specified, so nothing to check - allow access.
return true;
}
Set<String> roles = CollectionUtils.asSet(rolesArray);
//filterChainDefinitionMap.put("/admin/**","roles[admin,root]")
// shiro配置角色默认是与的关系,需要都满足,这里改成或的关系,只要有其中一个即可
for (String role : roles) {
if (subject.hasRole(role)){
return true;
}
}
return false;
}
}
(3)ShiroConfig中配置自定义过滤器
//设置自定义过滤器
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put("customRolesFilter",new CustomRolesAuthorizationFilter());
shiroFilterFactoryBean.setFilters(filterMap);
8.2.Redis整合CacheManager
- Redis整合CacheManager为了提高性能,避免每次都去库查
(1)加入shiro-redis依赖(shiro和redis整合的jar包)
<!--shiro整合redis-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
(2)ShiroConfig中配置,RedisManager,RedisCacheManager,SecruityManager
/**
* 加入RedisManager
*/
public RedisManager getRedisManager(){
RedisManager redisManager = new RedisManager();
redisManager.setHost("192.168.10.88");
redisManager.setPort(6379);
return redisManager;
}
/**
* 配置RedisCacheManager
*/
public RedisCacheManager cacheManager(){
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(getRedisManager());
//设置过期时间,单位秒
redisCacheManager.setExpire(60);
return redisCacheManager;
}
(3)改造现有逻辑自定义的Realm
doGetAuthorizationInfo 方法
原有:
String username = (String)principals.getPrimaryPrincipal();
User user = userService.findAllUserInfoByUsername(username);
改为
User newUser = (User)principals.getPrimaryPrincipal();
User user = userService.findAllUserInfoByUsername(newUser.getUsername());
doGetAuthenticationInfo方法
原有:
return new SimpleAuthenticationInfo(username, user.getPassword(), this.getClass().getName());
改为
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName());
8.3.Redis整合SessionManager
(1)加入SessionDAO的配置
/**
* 配置SessionDAO
*/
public RedisSessionDAO sessionDAO(){
RedisSessionDAO sessionDAO = new RedisSessionDAO();
//设置RedisManager
sessionDAO.setRedisManager(getRedisManager());
return sessionDAO;
}
(2)自定义的sessionManager中设置sessionDAO
//设置Session持久化,RedisSessionManager
//注意:如果不设置过期时间,redis中存储也和shiro中session的默认过期时间保持一致
customSessionManager.setSessionDAO(sessionDAO());
(3)注意传输的实体类都要实现Serializable接口,否则会报错
8.4.ShiroConfig常用的Bean配置
(1)LifecycleBeanPostProcessor:管理shiro一些bean的生命周期,即bean初始化与销毁
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
(2)AuthorizationAttributeSourceAdvisor:加入注解的使用,不加入这个AOP注解不生效(@RequiresGuest)
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
(3)DefaultAdvisorAutoProxyCreator:用来扫描上下文寻找的所有Advistor(通知器),将符合条件的Advisor应用到切入点的Bean中,需要在LifecycleBeanPostProcessor创建后才可以创建
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setUsePrefix(true);
return defaultAdvisorAutoProxyCreator;
}
9.分布式应用鉴权方式
9.1.自定义SessionId
- Shiro 默认的sessionid生成 类名 SessionIdGenerator
- 创建CustomSessionIdGenerator类,实现 SessionIdGenerator 接口的方法
/**
* 自定义session持久化
* @return
*/
public RedisSessionDAO redisSessionDAO(){
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setRedisManager(getRedisManager()); //设置sessionid生成器
redisSessionDAO.setSessionIdGenerator(new CustomSessionIdGenerator()); //设置自定义的sessionIdGenerator
return redisSessionDAO;
}