Spring Security到底是什么
Spring Security你懂了吗
前置知识
- 掌握Spring框架
- 掌握SpringBoot框架
- 掌握JavaWeb框架
下面的内容主要是用来介绍用户认证和用户的授权
两个安全认证框架对比
SpringSecurity
- 能够和Spring进行无缝整合
- 全面的权限控制
- 专门为Web开发而设计的
- 旧版本不能脱离 Web 环境使用
- 新版本对整个框架进行了分层抽取,分成了核心模块和 Web 模块。单独
引入核心模块就可以脱离 Web 环境
Shiro
是一款轻量级的权限控制框架
- 轻量级。 Shiro 主张的理念是把复杂的事情变简单。针对对性能有更高要求
的互联网应用有更好表现 - 通用性
- 好处 不局限于 Web 环境,可以脱离 Web 环境使用。
- 坏处 在 Web 环境下一些特定的需求需要手动编写代码定制
用户认证
简单的说指定就是系统认为用户是否能登录
用户授权
指定就是系统判断用户是否具有权限去做某些事情
SpringSecurity小测试
配置环境
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency>
编写Controller
@RestController
@RequestMapping("/test")
public class TestController {@GetMapping("/hello")public String add(){return "Hello Spring Security";}
}
此时启动服务进行登录之后 会出现安全认证界面
注意 这里的用户名默认是user 密码会在控制台打印出来
SpringSecurity基本原理
SpringSecurity本质是过滤器链 本文主要讲解三个过滤器
- FilterSecurityInterceptor是一个方法级的权限过滤器, 基本位于过滤链的最底部
具体invoke方法的实现
super.beforeInvocation(fi) 表示查看之前的 filter 是否通过。
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());表示真正的调用后台的服务
- ExceptionTranslationFilter: 是一个异常过滤器,用来处理在认证授权的过程总抛出的异常
- UsernamePasswordAuthenticationFilte 实现对/login的POST请求的拦截,检验表单中用户名与密码
用户自定义开发的时候不能用户名与密码都是涉及数据库的 所以在自定义开发中设计到的两个重要地接口
- UserDetailsService 用来查询数据库的用户名和密码的过程
创建类继承UsernamePasswordAuthenticationFilte ,重写attemptAuthentication successfulAuthentication unsuccessfulAuthentication三个方法
创建类实现UserDetailsService 编写查询数据库的过程,返回User对象 这个User对象是安全框架提供的对象
- PasswordEncoder
对密码进行解密
Web权限方案
认证
第一种 通过配置类来实现
spring.security.user.name=123 spring.security.user.password=123
第二种 通过配置类来实现
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();String password = passwordEncoder.encode("123");//可以通过auth设置登录名与密码auth.inMemoryAuthentication().withUser("123").password(password).roles("admin");}@BeanPasswordEncoder password(){return new BCryptPasswordEncoder();} }
第三种 自定义编写实现类
创建配置类 设置使用哪个userDetailService实现类
编写实现类 返回User对象 User对象有用户名 买吗和操作权限
/*** SpringSecurity的配置类*/ @Configuration public class SecurityConfigTest extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//在设置的userDetail里面返回用户名 密码 权限auth.userDetailsService(userDetailsService).passwordEncoder(password());}@BeanPasswordEncoder password(){return new BCryptPasswordEncoder();} } @Service("userDetailsService") public class UserDetailService implements UserDetailsService {@Overridepublic UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {//如果涉及到数据库 根据name 查询数据库对应的数据List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");return new User("ljx",new BCryptPasswordEncoder().encode("123"),auths);} }
连接数据库 完成用户的认证
添加pom.xml文件
<dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.2.0</version></dependency><dependency><groupId>mysql</ groupId><artifactId>mysql-connector-java</ artifactId></ dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
设置实体类 (包含username和password)
创建UserMapper继承BaseMapper
在UserDetailService里面根据传入的用户名获取对应的数据库对象 进行判断
@Service("userDetailsService")
public class UserDetailService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 这里面接收的username 就是security界面传递的用户名QueryWrapper<Users> qw = new QueryWrapper<>();qw.eq("username",username);Users users = userMapper.selectOne(qw);if (users==null){//认证失败throw new UsernameNotFoundException("对不起 用户名不存在");}//如果涉及到数据库 根据name 查询数据库对应的数据List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths);}
}
自定义登录页面以及设置哪些访问不需要设置就能实现访问
在配置类里面设置过滤规则
/*** SpringSecurity的配置类*/
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {@Autowiredprivate UserDetailsService userDetailsService;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {//在设置的userDetail里面返回用户名 密码 权限auth.userDetailsService(userDetailsService).passwordEncoder(password());}@BeanPasswordEncoder password(){return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.formLogin().loginPage("/login.html") //登录页面设置.loginProcessingUrl("/user/login")//自定义的登录访问路径要与表单提交到地址一致.defaultSuccessUrl("/test/index").permitAll()//登录成功之后跳转的路径.and().authorizeRequests().antMatchers("/","/test/hello","/user/login").permitAll()//设置那些路径不需要进行访问.anyRequest().authenticated().and().csrf().disable();//关闭csrf防护}
}
设置默认的登录界面 路径resourcess/statis/login.html
启动测试:
localhost:8111/test/hello==>得到输出结果
localhost:8111/test/index==>就会跳转到登录界面 然后进行登录 就能够得到对应的返回值
基于角色或权限进行访问控制
hasAuthority
如果当前的用户具有指定的权限那么返回true 否则返回false
在配置类里面设置哪些路径需要指定权限
在UserDetailsService里面 把返回User对象设置权限
在配置类中 .antMatchers("/test/index").hasAuthority("admins")//设置当前的登录用户 只有具有了admin权限才能够访问这个路径
在UserDetailsService中 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
hasAnyAuthority
如果当前的主体有任何任何的角色的话返回true
hasAnyAuthority("admins,admin1")
hasRole
如果用户具备给定的角色就允许访问否则出现403
.antMatchers("/test/index").hasRole("sale")底层会在sale前面加ROLE_sale所以 List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale");
hasAnyRole
如果允许具备的多个角色就允许访问 否则403
.antMatchers("/test/index").hasAnyRole("sale,sale1")List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins,ROLE_sale,ROLE_sale1");
注意:在设置角色的底层会默认为其加上ROLE_ 所以在为用户赋予角色的时候也要加上ROLE_
自定义403没有权限的界面
在配置类里面
http.exceptionHandling().accessDeniedPage("/norole.html");
认证授权中注解的使用
使用注解要先开启注解的功能
@ Secured
先开启注解功能@EnableGlobalMethodSecurity(securedEnabled=true)
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“
在控制器上也可以在方法上 针对于某一个方法
@RestController
@RequestMapping("/test")
//@Secured({"ROLE_admin123"})只有在UserDetails里面为用户设置了权限才能够进行访问
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_admin123");
@PreAuthrorize
开启注解@EnableGlobalMethodSecurity(prePostEnabled = true)
作用在方法上
注解适合进入方法前的权限验证,可以将登录用户的 roles/permissions 参数传到方法中
在方法上设置
@PreAuthorize("hasAnyAuthority('admin')")
在UserDetails设置
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
@PostAuthorize
开启注解@EnableGlobalMethodSecurity(prePostEnabled = true)
在方法执行之后进行执行 基本不用 方法之后检验还有啥作用
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
在方法上设置
@PostAuthorize("hasAnyAuthority('admin')")
在UserDetails设置
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admin");
@PostFilter
权限认证之后,对返回的数据进行过滤
@GetMapping("getAll")
@PreAuthorize("hasRole('ROLE_ 管理员')")
@PostFilter("filterObject.username == 'admin1'")
public List<UserInfo> getAllUser(){ArrayList<UserInfo> list = new ArrayList<>();
list.add(new UserInfo(1l,"admin1","6666"));
list.add(new UserInfo(2l,"admin2","888"));
return list;
}
只返回username为admin1的数据
@PreFilter
对进入控制器之前的数据进行过滤
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_ 管理员')")
@PreFilter(value = "filterObject.id%2==0")
public List<UserInfo> getTestPreFilter(@RequestBody List<UserInfo>list){list.forEach(t-> {System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}
只有list集合里面Id的值是偶数的才会进入
登录之后用户的注销或者退出操作
在配置类里面添加配置退出的映射
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
此时修改一下 登录成功的跳转界面
.defaultSuccessUrl("/success.html").permitAll()//登录成功之后跳转的路径
在success.html里面
<a href="/logout">退出</a>
CSRF
跨站请求伪造,默认情况下回启动CSRF保护,以防止CSRF攻击应用,SpringSecurity会针对PATCH POST PUT DELETE方法进行防护
SpringSecurity微服务权限方案
- 基于Session 那么Spring-Security会对cookie里面的sessionId进行解析,找到服务器存储的session信息,然后判断当前的用户是否符合请求的要求
- 如果是token 那么就要解析出token然后将当前请求加入到Spring-Security管理的权限信息中
用户登录成功之后 查询对应的用户权限列表—>将用户相关信息保存在Redis里面(Key:用户名 value:用户的权限列表)—>根据用户名生成token(使用JWT)—>将token放到cookie里面 在header放token---->Spring Security 从header中获取token 那token获取用户名 那这个用户名查询对应的权限列表(Redis里面)
微服务权限管理案例的主要功能
- 登录(人证 )
- 添加角色
- 为角色分配菜单
- 添加用户
- 为用户分配角色
权限管理数据模型
菜单表 角色菜单表 角色表 用户角色表 用户表
案例涉及到的技术
Maven SpringBoot MybatisPlus SpringCloud(GetWay Nacos) Redis Swagger
创建一个父工程: acl_parent 管理依赖的版本
在父工程下面创建子模块
- common
- service_base: 工具类
- spring_security: 权限配置
- infrastructure
- api_getway: 网关
- service
- service_acl 权限管理模块
启动Redis和Nacos服务
Redis相当于一个数据库 用来存储数据
Nacos 就是一个注册中心
项目操作步骤
- 编写service_base里面的工具类的内容
- 编写spring_security认证授权的工具类
DefaultPasswordEncoder 密码处理
//密码处理工具类
public class DefaultPasswordEncoder implements PasswordEncoder {public DefaultPasswordEncoder(){this(-1);}public DefaultPasswordEncoder (int strength){}//实现加密 进行MD5加密@Overridepublic String encode(CharSequence charSequence) {MD5.encrypt(charSequence.toString());return null;}//进行比对/**** @param charSequence 传入的密码* @param encodingPassword 加密之后的密码* @return*/@Overridepublic boolean matches(CharSequence charSequence, String encodingPassword) {return encodingPassword.equals(MD5.encrypt(charSequence.toString()));}
}
TokenLogoutHandle 退出处理器
//退出处理器
public class TokenLogoutHandle implements LogoutHandler {//删除Token 根据token获取用户名在Redis里面进行删除private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenLogoutHandle(TokenManager tokenManager, RedisTemplate redisTemplate) {this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}@Overridepublic void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {//在Header里面获取token// token不为空 移除token 从Redis删除tokenString token = request.getHeader("token");if (token!=null){//删除TokentokenManager.removeToken(token);//从Token获取用户名String username = tokenManager.getUserInfoFromToken(token);redisTemplate.delete(username);}ResponseUtil.out(response, R.ok());}
}
TokenManager token操作工具类
// token操作工具类
public class TokenManager {//token有效时长private long tokenEcpiration=24*60*60*100;//编码秘钥private String tokenSignKey="123456";//根据用户名生成Tokenpublic String createToken(String username){String token = Jwts.builder().setSubject(username).setExpiration(new Date(System.currentTimeMillis()+tokenEcpiration)).signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();return token;}//根据Token得到用户信息public String getUserInfoFromToken(String token){String userInfo = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();return userInfo;}//删除Tokenpublic void removeToken(String token){}
}
UnauthorizedEntryPoint 未授权统一处理
//未授权统一处理类
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {ResponseUtil.out(response, R.error());}
}
TokenLoginFilter认证过滤器
//认证过滤 指定就是进行认证登录
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {private TokenManager tokenManager;private RedisTemplate redisTemplate;private AuthenticationManager authenticationManager;public TokenLoginFilter(TokenManager tokenManager, RedisTemplate redisTemplate, AuthenticationManager authenticationManager) {this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;this.authenticationManager = authenticationManager;this.setPostOnly(false);//设置登录的路径和提交方式this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));}//获取表单提交的用户名和密码@Overridepublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {try {User user = new ObjectMapper().readValue(request.getInputStream(), User.class);return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword(),new ArrayList<>()));} catch (IOException e) {e.printStackTrace();throw new RuntimeException();}}//认证成功调用的方法@Overrideprotected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {// 认证成功 得到认证成功之后用户信息SecurityUser user = (SecurityUser)authResult.getPrincipal();//根据用户名生成tokenString token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());//将用户名和用户权限放到权限列表里面redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(),user.getPermissionValueList());//返回tokenResponseUtil.out(response, R.ok().data("token",token));}//认证失败调用的方法@Overrideprotected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {ResponseUtil.out(response,R.error());}
}
TokenAuthenticationFilter授权过滤器
//授权过滤
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {private TokenManager tokenManager;private RedisTemplate redisTemplate;public TokenAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint, TokenManager tokenManager, RedisTemplate redisTemplate) {super(authenticationManager, authenticationEntryPoint);this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {//获取当前认证乘公共用户的授权信息//获取当前认证成功用户权限信息UsernamePasswordAuthenticationToken authRequest = getAuthentication(request);//判断如果有权限信息,放到权限上下文中if(authRequest != null) {SecurityContextHolder.getContext().setAuthentication(authRequest);}chain.doFilter(request,response);}private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request){//从Header中获取TokenString token = request.getHeader("token");if (token!=null){//从Token中获取用户名String username = tokenManager.getUserInfoFromToken(token);//从redis获取对应的权限列表List<String> permissionValueList = (List<String>)redisTemplate.opsForValue().get(username);Collection<GrantedAuthority> authority = new ArrayList<>();for(String permissionValue : permissionValueList) {SimpleGrantedAuthority auth = new SimpleGrantedAuthority(permissionValue);authority.add(auth);}return new UsernamePasswordAuthenticationToken(username,token,authority);}return null;}
}
TokenWebSecurityConfig核心配置类
//核心配置类
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {private TokenManager tokenManager;private RedisTemplate redisTemplate;private DefaultPasswordEncoder defaultPasswordEncoder;private UserDetailsService userDetailsService;@Autowiredpublic TokenWebSecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,TokenManager tokenManager, RedisTemplate redisTemplate) {this.userDetailsService = userDetailsService;this.defaultPasswordEncoder = defaultPasswordEncoder;this.tokenManager = tokenManager;this.redisTemplate = redisTemplate;}/*** 配置设置* @param http* @throws Exception*///设置退出的地址和token,redis操作地址@Overrideprotected void configure(HttpSecurity http) throws Exception {http.exceptionHandling().authenticationEntryPoint(new UnauthorizedEntryPoint())//没有权限访问.and().csrf().disable().authorizeRequests().anyRequest().authenticated().and().logout().logoutUrl("/admin/acl/index/logout")//退出路径.addLogoutHandler(new TokenLogoutHandle(tokenManager,redisTemplate)).and().addFilter(new TokenLoginFilter( tokenManager, redisTemplate,authenticationManager())).addFilter(new TokenAuthenticationFilter(authenticationManager(),tokenManager, redisTemplate)).httpBasic();}//调用userDetailsService和密码处理@Overridepublic void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);}//不进行认证的路径,可以直接访问@Overridepublic void configure(WebSecurity web) throws Exception {web.ignoring().antMatchers("/api/**");}
}
编写UserDetailServiceImpl
@Service("userDetailsService") 这个名字要与配合类里面定义的名字一致
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserService userService;@Autowiredprivate PermissionService permissionService; 关于权限的@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//根据用户名查询数据User user = userService.selectByUsername(username);//判断if(user == null) {throw new UsernameNotFoundException("用户不存在");}User curUser = new User();BeanUtils.copyProperties(user,curUser);//根据用户查询用户权限列表List<String> permissionValueList = permissionService.selectPermissionValueByUserId(user.getId());SecurityUser securityUser = new SecurityUser();securityUser.setCurrentUserInfo(curUser);securityUser.setPermissionValueList(permissionValueList);return securityUser;}
}
整体流程是
先进行认证: attemptAuthentication–>成功successfulAuthentication/失败unsuccessfulAuthentication–>在进行授权doFilterInternal
自我理解
在将前台的登录界面设置为不拦截的请求 前台界面登录执行登录请求之后 就会与Security的核心配置类里面设置的登录路径包含登录方法相匹配 如果匹配合格就会进行认证和授权的检测 检测是否能够进行登录并对其进行授权
其他具体相关代码在这里不在讲述 想要了解的请点击我的项目地址,如有不对的地方请指出公共学习
项目地址
Spring Security到底是什么相关推荐
- Spring Security到底在哪里进行密码方式认证
一 Spring Security比较好的教程 http://www.spring4all.com/article/428 二 基于数据库的密码认证 http://www.spring4all.com ...
- Spring Security实现RememberMe功能以及原理探究
在大多数网站中,都会实现RememberMe这个功能,方便用户在下一次登录时直接登录,避免再次输入用户名以及密码去登录,下面,主要讲解如何使用Spring Security实现记住我这个功能以及深入源 ...
- .netcore 如何获取系统中所有session_集群化部署,Spring Security 要如何处理 session 共享?
前面和大家聊了 Spring Security 如何像 QQ 一样,自动踢掉已登录用户(Spring Boot + Vue 前后端分离项目,如何踢掉已登录用户?),但是前面我们是基于单体应用的,如果我 ...
- oauth2_带有Spring Security的OAuth 2.0快速指南
oauth2 "我喜欢编写身份验证和授权代码." 〜从来没有Java开发人员. 厌倦了一次又一次地建立相同的登录屏幕? 尝试使用Okta API进行托管身份验证,授权和多因素身份验 ...
- Spring Security太复杂?试试这个轻量、强大、优雅的权限认证框架!
各位程序猿小伙伴们,中秋快乐~在节日欢快的气氛中大家是不是还在奋笔疾书.沉浸在学习的海洋中呢? 小编这两天休息在家一直在想一个问题,那就是我们在开发SpringBoot项目的时候,该怎么做好权限认证呢 ...
- Spring Security 实战干货:路径Uri中的 Ant 风格
点击上方蓝色"程序猿DD",选择"设为星标" 回复"资源"获取独家整理的学习资料! 来源 | juejin.im/post/5c6b6b12 ...
- spring security源码分析之web包分析
Spring 是一个非常流行和成功的 Java 应用开发框架.Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案.一般来说,Web 应用的安全性包括 ...
- [转载]spring security 的 logout 功能
原文地址:spring security 的 logout 功能作者:sumnny 转载自:http://lengyun3566.iteye.com/blog/1114464 理解退出功能 术语退出( ...
- Spring boot+Spring Security 4配置整合实例
本例所覆盖的内容: 1. 使用Spring Security管理用户身份认证.登录退出 2. 用户密码加密及验证 3. 采用数据库的方式实现Spring Security的remember-me功能 ...
最新文章
- 服务注册发现与kit实践
- mysql load data on duplicate_带有ON DUPLICATE KEY UPDATE的MySQL LOAD DATA INFILE
- 自画菜单中如何触发MeasureItem事件的问题及解决办法
- Flutter开发之PageView指示器(31)
- 虚拟机:Centos 7 安装JDK8(亲测)
- cat查看tomcat日志 linux_方法篇:tomcat日志切割和定期删除
- SpringBoot单元测试运行时报错:Failed to load ApplicationContext
- 人人网 校内- 日志分享
- ICH E2B | ICSR 电子传输网关对接解决方案
- Docker 容器学习完整笔记
- java2048移动算法_2048游戏通关算法
- java多态的练习 ,定义三个类,父类GeometricObject代表几何形状,子类Circle代表圆形,MyRectangle代表矩形。 定义一个测试类GeometricTest,编写equals
- 国内多家视频下载网站关闭:或为暂避风头
- win10下安装matlab r2018a破解版
- 【性能测试】轻商城-项目实战3
- 兔子数列规律怎么讲_神奇兔子数列
- SpringBoot图片上传失败
- 买家用投影仪应该关注哪些数据?
- 网络接口层协议:ATM
- MT7687芯片资料MT7687原理图资料
热门文章
- Android RecyclerView实现图片瀑布流
- ☀️机器学习实战☀️基于 YOLO网络 的人脸识别 |(文末送机器学习书籍~)
- 人工神经网络反向传播,神经网络后向传播
- 基于SSM的高校助学贷款申报审批系统
- PS打开PSD文档服务器未响应,优化你的 PSD 文件防止 Photoshop 崩溃卡死 - 文章教程...
- cad地图转成shp方法
- 【ansys workbench】11.圣维南原理和模型简化
- 用java编写输出直角三角型、倒直角三角形
- 搞的谁还不会爬福利美女跳舞视频一样,用我这个方法非常简单。
- 力扣(SQL)595. 大的国家