【权限管理框架】一文看懂Shiro权限管理框架!
文章目录
- 1.JavaWeb中的权限控制
- 2.权限框架核心知识ACL和RBAC
- 2.1.ACL和RBAC简介
- 2.2主流权限框架介绍
- 3.Shiro架构和基本概念
- 3.1.Shiro的4大核心模块
- 3.2.Shiro权限控制运行流程
- 4.Shiro简单API案例
- 4.1.项目搭建所需依赖
- 4.2.Shiro认证简单实操
- 4.3.Shiro授权简单实操
- 5.安全数据来源Realm
- 5.1.Realm简介和继承关系
- 5.2.Shiro内置IniRealm权限验证
- 5.3.Shiro内置JdbcRealm权限验证
- 5.4.Shiro自定义Realm权限配置
- 5.5.Shiro源码认证授权流程
- 6.Shiro权限认证Web案例
- 6.1.Shiro内置的过滤器
- 6.2.Shiro的Filter配置路径
- 6.3.Shiro数据安全之数据加解密
- 6.4.Shiro权限控制注解
- 6.5.Shiro缓存模块讲解
- 6.6.Shiro Session模块讲解
- 7.SpringBoot2.x整合Shiro
- 7.1.数据库设计
- 7.2.Maven项目搭建
- 7.3.编写查询用户全部信息接口
- 7.4.开发自定义CustomRealm
- 7.5.ShiroFilterFactoryBean配置
- 7.6.自定义SessionManager验证
- 7.7.API拦截验证案例
- 7.8.Shiro密码加密处理
- 8.权限控制性能提升
- 8.1.自定义Shiro Filter过滤器
- 8.2.Redis整合CacheManager
- 8.3.Redis整合SessionManager
- 8.4.ShiroConfig常用的Bean配置
- 9.分布式应用鉴权方式
- 9.1.自定义SessionId
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存储到数据库。
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{//声明SecurityManagerDefaultSecurityManager securityManager = new DefaultSecurityManager();//声明RealmSimpleAccountRealm accountRealm = new SimpleAccountRealm();@BeforeTestpublic void init(){accountRealm.addAccount("lixiang","123456");accountRealm.addAccount("lisi","123456");//构建环境securityManager.setRealm(accountRealm);}@Testpublic 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)编码实操
@Testvoid 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();//创建登录TokenUsernameAndPasswordToken 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.ComboPooledDataSourcedataSource=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=falsedataSource.username=testdataSource.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)
@Testvoid 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 {//userprivate final static Map<String,String> userMaps = new HashMap<>();{userMaps.put("lixiang","123");userMaps.put("lisi","123");}//roles - > permissionprivate 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 -> roleprivate 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*/@Overrideprotected 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*/@Overrideprotected 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)自定义验证规则
一般会自定义验证规则@Beanpublic 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 {@Autowiredprivate RoleMapper roleMapper;@Autowiredprivate UserMapper userMapper;@Autowiredprivate PermissionMapper permissionMapper;@Overridepublic 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;}@Overridepublic User findSimpleUserInfoById(int userId) {return userMapper.findById(userId);}@Overridepublic User findSimpleUserInfoByUsername(String username) {return userMapper.findByUsername(username);}
}
(5)controller编写
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate 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 {@Autowiredprivate UserService userService;/*** 用户鉴权的时候会调用* @param principals* @return*/@Overrideprotected 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*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {System.out.println("认证 doGetAuthenticationInfo");//从token中拿到usernameString username = (String)token.getPrincipal();User userInfo = userService.findAllUserInfoByUsername(username);//获取密码String password = userInfo.getPassword();if(password == null || "".equals(password)){return null;}//密码不为空,说明认证成功,封装SimpleAuthenticationInfo对象,参数:用户名,密码,class.getNamereturn 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*/@Beanpublic ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){System.out.println("执行 ShiroFilterFactoryBean.shiroFilter()");ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();//设置securityManagershiroFilterFactoryBean.setSecurityManager(securityManager);//设置登录成功后访问的路径shiroFilterFactoryBean.setLoginUrl("/pub/need_login");//设置登录成功后访问的urlshiroFilterFactoryBean.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*/@Beanpublic SecurityManager securityManager(){DefaultSecurityManager securityManager = new DefaultSecurityManager();securityManager.setRealm(customRealm());//注意如果不是前后端分离的项目就不需要设置sessionManagersecurityManager.setSessionManager(customSessionManager());return securityManager;}/*** 设置散列算法* @return*/@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher(){HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();//设设置散列算法,md5hashedCredentialsMatcher.setHashAlgorithmName("md5");//设置散列次数hashedCredentialsMatcher.setHashIterations(2);return hashedCredentialsMatcher;}/*** 注入自定义的Realm* @return*/@Beanpublic CustomRealm customRealm(){CustomRealm customRealm = new CustomRealm();customRealm.setCredentialsMatcher(hashedCredentialsMatcher());return customRealm;}/*** 注入自定义的SessionManager* @return*/@Beanpublic 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();}@Overrideprotected 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 {@Testvoid 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 {@Overridepublic 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();//设置RedisManagersessionDAO.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)
@Beanpublic 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;
}
【权限管理框架】一文看懂Shiro权限管理框架!相关推荐
- 一文看懂Android APK安装的原理
一文看懂Android APK安装的原理 前言 APK包的构成 安装APK 总结 前言 大家有没有想过一个应用的APK是怎么被安装到安卓手机上的,安装的本质是什么?我们知道,Windows应用程序的安 ...
- 干货|一文看懂什么是“非标资产”
干货|一文看懂什么是"非标资产" 2017-05-19 18:56 监管/信托/资管 本文作者:中诚信托 一.非标资产的界定和范围 非标资产全称为非标准债权资产,是相对于标准化金融 ...
- php-fpm进程的用户组,一文看懂PHP进程管理器php-fpm
php-fpm是什么 php-fpm是PHP的一个进程管理器.php下面的众多work进程皆有php-fpm进程管理器管理. php-fpm的工作原理 php-fpm全名是PHP FastCGI进程管 ...
- 一文读懂 Shiro 登录认证全流程
一文读懂 Shiro 登录认证全流程 登录入口 执行登录 UserRealm Apache Shiro 是 Java 的一个安全框架.Shiro 可以帮助我们完成:认证.授权.加密.会话管理.与 We ...
- 一文看懂大数据生态圈完整知识体系【大数据技术及架构图解实战派】
一文看懂大数据生态圈完整知识体系 徐葳 随着大数据行业的发展,大数据生态圈中相关的技术也在一直迭代进步,作者有幸亲身经历了国内大数据行业从零到一的发展历程,通过本文希望能够帮助大家快速构建大数据生态圈 ...
- 【燃】是时候展现真正的实力了!一文看懂2022华为开发者大赛技术亮点
摘要:2022华为开发者大赛备赛攻略,超详细的开发技术要点介绍来了. 本文分享自华为云社区<[燃]是时候展现真正的实力了!一文看懂2022华为开发者大赛技术亮点>,作者:华为云社区精选 . ...
- 一文看懂 AI 训练集、验证集、测试集(附:分割方法+交叉验证)
2019-12-20 20:01:00 数据在人工智能技术里是非常重要的!本篇文章将详细给大家介绍3种数据集:训练集.验证集.测试集. 同时还会介绍如何更合理的讲数据划分为3种数据集.最后给大家介绍一 ...
- 一文看懂计算机视觉-CV(基本原理+2大挑战+8大任务+4个应用)
2020-03-06 20:00:00 计算机视觉(Computer Vision)是人工智能领域的一个重要分支.它的目的是:看懂图片里的内容. 本文将介绍计算机视觉的基本概念.实现原理.8 个任务和 ...
- 一文看懂人脸识别(4个特点+4个实现步骤+5个难点+算法发展轨迹)
2020-03-09 20:01:00 人脸识别是身份识别的一种方式,目的就是要判断图片和视频中人脸的身份时什么. 本文将详细介绍人脸识别的4个特点.4个步骤.5个难点及算法的发展轨迹. 什么是人脸识 ...
最新文章
- 软件测试黑盒测试实验心得_软件测试的基础知识
- 抽象工厂模式(abstract factory)
- TinyOS下TOSSIM仿真
- 简单三步-实现dede站内搜索功能
- 面试必过之Mongdodb数据库面试题总结大全!
- JavaScript错误:Maximum call stack size exceeded错误
- hive指定多个字符作为列分隔符的问题说明
- python mmap对象
- 【AWSL】之Linux引导过程及服务控制(MBR、GRUB、runlevel、systemcl、init、ntsysv、chkconfig)
- int 为什么是2147483647_现在的C语言编辑器里的int范围为什么是-2147483648~2147483647...
- FOSRestBundle功能包:视图层
- js获取baseurl
- 存储过程从入门到精通(转载)
- python session过期_设置session过期时间
- Win10右键新建中没有新建文件夹,电脑右键新建文件夹不见了
- Centos中jdk安装及环境变量配置
- 李运华《从零开始学架构》——架构设计三原则
- elasticsearch 版本区别
- 又见猛犸象:基因剪刀重新定制生命
- ubuntu16.04校园网(使用mentohust替代锐捷)
热门文章
- 继电器——电磁式继电器
- 在系统里放一只“猴子”,阿里疯了吗?
- js中根据特定字符截取字符串
- 加密文件的识别和破解工具,电子数据勘察取证实验室建设项目-掘密
- 【数据结构】387. 字符串中的第一个唯一字符
- Java中的多线程安全问题
- Python迭代器、生成器、map以及reduce
- 推荐系统[九]项目技术细节讲解z4:向量检索技术工程上实践,曝光去重实践以及检索引擎该如何选择:支撑亿级索引、5毫秒级的检索[elasticsearch、milvus]
- Java NIO与IO比较总结
- Java中表格背景设置透明