spring security 原理+实战
疯狂创客圈 Java 高并发【 亿级流量聊天室实战】实战系列 【博客园总入口 】
架构师成长+面试必备之 高并发基础书籍 【Netty Zookeeper Redis 高并发实战 】
前言
Crazy-SpringCloud 微服务脚手架 &视频介绍:
Crazy-SpringCloud 微服务脚手架,是为 Java 微服务开发 入门者 准备的 学习和开发脚手架。并配有一系列的使用教程和视频,大致如下:
高并发 环境搭建 图文教程和演示视频,陆续上线:
中间件 | 链接地址 |
---|---|
Linux Redis 安装(带视频) | Linux Redis 安装(带视频) |
Linux Zookeeper 安装(带视频) | Linux Zookeeper 安装, 带视频 |
Windows Redis 安装(带视频) | Windows Redis 安装(带视频) |
RabbitMQ 离线安装(带视频) | RabbitMQ 离线安装(带视频) |
ElasticSearch 安装, 带视频 | ElasticSearch 安装, 带视频 |
Nacos 安装(带视频) | Nacos 安装(带视频) |
Crazy-SpringCloud 微服务脚手架 图文教程和演示视频,陆续上线:
组件 | 链接地址 |
---|---|
Eureka | Eureka 入门,带视频 |
SpringCloud Config | springcloud Config 入门,带视频 |
spring security | spring security 原理+实战 |
Spring Session | SpringSession 独立使用 |
分布式 session 基础 | RedisSession (自定义) |
重点: springcloud 开发脚手架 | springcloud 开发脚手架 |
SpingSecurity + SpringSession 死磕 (写作中) | SpingSecurity + SpringSession 死磕 |
小视频以及所需工具的百度网盘链接,请参见 疯狂创客圈 高并发社群 博客
Spring Security 的重要性
在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,特别是在spring boot项目中,spring security已经默认集成和启动了。
Spring Security 默认为自动开启的,可见其重要性。
如果要关闭,需要在启动类加上,exclude ={SecurityAutoConfiguration} 的配置
@EnableEurekaClient
@SpringBootApplication(scanBasePackages = {
"com.crazymaker.springcloud.user",
"com.crazymaker.springcloud.seckill.remote.fallback",
"com.crazymaker.springcloud.standard"
}, exclude = {SecurityAutoConfiguration.class})
或者
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
一般不建议关闭。
Spring Security 核心组件
spring security核心组件有:Userdetails 、Authentication,UserDetailsService、AuthenticationProvider、AuthenticationManager 下面分别介绍。
Authentication
authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
接口有4个get方法,分别获取
Authorities, 填充的是用户角色信息。
Credentials,直译,证书。填充的是密码。
Details ,用户信息。
Principal 直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。
因此可以推断其实现类有这4个属性。
这几个方法作用如下:
getAuthorities: 获取用户权限,一般情况下获取到的是用户的角色信息。
getCredentials: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。
getDetails: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)
getPrincipal: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。
isAuthenticated: 获取当前 Authentication 是否已认证。
setAuthenticated: 设置当前 Authentication 是否已认证(true or false)。
UserDetails
UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
方法含义如下:
getAuthorites:获取用户权限,本质上是用户的角色信息。
getPassword: 获取密码。
getUserName: 获取用户名。
isAccountNonExpired: 账户是否过期。
isAccountNonLocked: 账户是否被锁定。
isCredentialsNonExpired: 密码是否过期。
isEnabled: 账户是否可用。
UserDetailsService
提到了UserDetails就必须得提到UserDetailsService, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername,他可以用来获取UserDetails。
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);方法。我们在实现loadUserByUsername方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails,(通常是一个org.springframework.security.core.userdetails.User,它继承自UserDetails) 并返回。
在实现loadUserByUsername方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException。
AuthenticationProvider
负责真正的验证。
当我们使用 authentication-provider 元素来定义一个 AuthenticationProvider 时,如果没有指定对应关联的 AuthenticationProvider 对象,Spring Security 默认会使用 DaoAuthenticationProvider。DaoAuthenticationProvider 在进行认证的时候需要一个 UserDetailsService 来获取用户的信息 UserDetails,其中包括用户名、密码和所拥有的权限等。所以如果我们需要改变认证的方式,我们可以实现自己的 AuthenticationProvider;如果需要改变认证的用户信息来源,我们可以实现 UserDetailsService。
实现了自己的 AuthenticationProvider 之后,我们可以在配置文件中这样配置来使用我们自己的 AuthenticationProvider。其中 myAuthenticationProvider 就是我们自己的 AuthenticationProvider 实现类对应的 bean。
AuthenticationProvider 接口如下:
package org.springframework.security.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
authenticate 表示认证的动作。
supports 表示所支持的 Authentication类型。Authentication 包含很多子类,如果 AbstractAuthenticationToken 。
AbstractAuthenticationToken implements Authentication
还有,可以自定义 Authentication ,比如 本实例所使用的: JwtAuthenticationToken。
AuthenticationManager
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。
AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication,其定义如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 的作用就是校验Authentication,如果验证失败会抛出AuthenticationException异常。AuthenticationException是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能会比较常见,即密码错误的时候。
组件比较多,但是如果主要流程理顺了,也比较简单。
Spring Security 实战
搞定两个 AuthenticationProvider:
(1) 从数据库获取用户
首先通过 UserDetailsService 获取 UserDetails,然后 通过 UserDetailsService 装配 DaoAuthenticationProvider
(2) 完成用户的认证
实现一个自己的 JwtAuthenticationProvider,完成用户的认证
(3)定制一个过滤器
(4)完成所有组件的装配
实战1 : UserDetailsService 获取 UserDetails
首先通过 UserDetailsService 获取 UserDetails,然后 通过 UserDetailsService 装配 DaoAuthenticationProvider。
package com.crazymaker.springcloud.user.info.service.impl;
@Slf4j
@Service
public class UserAuthService implements UserDetailsService {
private PasswordEncoder passwordEncoder;
public UserAuthService() {
//默认使用 bcrypt, strength=10
this.passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
private UserPO loadFromDB(String username) {
if (null == userDao)
{
userDao = CustomAppContext.getBean(UserDao.class);
}
List<UserPO> list = userDao.findAllByLoginName(username);
if (null == list || list.size() <= 0) {
return null;
}
UserPO userPO = list.get(0);
return userPO;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
UserPO userPO = loadFromDB(username);
//将salt放到password字段返回
return User.builder()
.username(userPO.getLoginName())
.password(userPO.getPassword())
// .password(SessionConstants.SALT)
//BCrypt.gensalt(); 正式开发时可以调用该方法实时生成加密的salt
// .password(SessionConstants.SALT)
.authorities(SessionConstants.USER_INFO)
.roles("USER")
.build();
}
}
实战2: 装配 DaoAuthenticationProvider
在 SecurityConfiguration 配置类中加入如下内容:
@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
//这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
}
@Override
protected UserDetailsService userDetailsService() {
return new UserAuthService();
}
实战3: 实现一个自己的 JwtAuthenticationProvider
继承于 AuthenticationProvider,实现一个自己的 JwtAuthenticationProvider,完成用户的认证
package com.crazymaker.springcloud.standard.security.provider;
//...
public class JwtAuthenticationProvider implements AuthenticationProvider {
private RedisOperationsSessionRepository sessionRepository;
private CustomedSessionIdResolver httpSessionIdResolver;
public JwtAuthenticationProvider(RedisOperationsSessionRepository sessionRepository,
CustomedSessionIdResolver httpSessionIdResolver) {
this.sessionRepository = sessionRepository;
this.httpSessionIdResolver = httpSessionIdResolver;
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
DecodedJWT jwt = ((JwtAuthenticationToken) authentication).getToken();
if (jwt.getExpiresAt().before(Calendar.getInstance().getTime())) {
throw new NonceExpiredException("认证过期");
}
String sid = jwt.getSubject();
String otoken = jwt.getToken();
Session session = null;
try {
session = sessionRepository.findById(sid);
} catch (Exception e) {
e.printStackTrace();
}
if (null == session) {
throw new NonceExpiredException("认证有误,请重新登录");
}
String json = session.getAttribute(G_USER);
if (StringUtils.isBlank(json)) {
throw new NonceExpiredException("认证有误,请重新登录");
}
UserDTO userDTO = JsonUtil.jsonToPojo(json, UserDTO.class);
if (null == userDTO) {
throw new NonceExpiredException("认证有误");
}
String password = userDTO.getPassword();
String username = userDTO.getLoginName();
UserDetails user = User.builder()
.username(username)
.password(password)
.authorities(SessionConstants.USER_INFO)
.build();
String encryptSalt = password;
try {
Algorithm algorithm = Algorithm.HMAC256(encryptSalt);
JWTVerifier verifier = JWT.require(algorithm)
.withSubject(sid).build();
verifier.verify(jwt.getToken());
} catch (Exception e) {
throw new BadCredentialsException("JWT token verify fail", e);
}
JwtAuthenticationToken token =
new JwtAuthenticationToken(user, jwt, user.getAuthorities());
return token;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(JwtAuthenticationToken.class);
}
}
实战4: 装配 AuthenticationManager
认证是由 AuthenticationManager 来管理的,但是真正进行认证的是 AuthenticationManager 中定义的 AuthenticationProvider。AuthenticationManager 中可以定义有多个 AuthenticationProvider。
@EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
}
//....
}
实战5: 定制过滤器,将 AuthenticationManager 用起来
搞得再多,如果不通过过滤器,将 AuthenticationManager 用起来,也是没有用的。
package com.crazymaker.springcloud.standard.security.filter;
//.....
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private RequestMatcher requiresAuthenticationRequestMatcher;
private List<RequestMatcher> permissiveRequestMatchers;
private AuthenticationManager authenticationManager;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
//.....
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication authResult = null;
/**
* 场景: 从 zuul 过来,直接带上session 头
*/
if (StringUtils.isNotEmpty(request.getHeader(SessionConstants.SESSION_SEED))) {
request.setAttribute(SessionConstants.SESSION_SEED,
request.getHeader(SessionConstants.SESSION_SEED));
UserDetails userDetails = User.builder()
.username(request.getHeader(SessionConstants.SESSION_SEED))
.password(request.getHeader(SessionConstants.SESSION_SEED))
.authorities(SessionConstants.USER_INFO)
.build();
authResult = new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
successfulAuthentication(request, response, filterChain, authResult);
filterChain.doFilter(request, response);
return;
}
/**
* 正常场景: 单体微服务访问,或者从Zuul过来,没有带 session head
*/
if (!requiresAuthentication(request, response)) {
filterChain.doFilter(request, response);
return;
}
AuthenticationException failed = null;
try {
String token = getJwtToken(request);
if (StringUtils.isNotBlank(token)) {
JwtAuthenticationToken authToken = new JwtAuthenticationToken(JWT.decode(token));
DecodedJWT jwt = authToken.getToken();
//将 AuthenticationManager 用起来
authResult = this.getAuthenticationManager().authenticate(authToken);
UserDetails user = (UserDetails) authResult.getPrincipal();
request.setAttribute(SessionConstants.SESSION_SEED, jwt.getSubject());
} else {
failed = new InsufficientAuthenticationException("请求头认证消息为空");
}
} catch (JWTDecodeException e) {
logger.error("JWT format error", e);
failed = new InsufficientAuthenticationException("请求头认证消息格式错误", failed);
} catch (InternalAuthenticationServiceException e) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
failed = e;
} catch (AuthenticationException e) {
// Authentication failed
failed = e;
}
if (authResult != null) {
successfulAuthentication(request, response, filterChain, authResult);
} else if (!permissiveRequest(request)) {
unsuccessfulAuthentication(request, response, failed);
return;
}
filterChain.doFilter(request, response);
}
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
}
//....
}
实战6: 配置 HttpSecurity 的过滤机制
还是在 UserWebSecurityConfig 配置文件,将 HttpSecurity 的过滤机制配置起来,完成所有组件的装配。
代码如下:
package com.crazymaker.springcloud.user.info.config;
//...
@EnableWebSecurity()
public class UserWebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserAuthService userAuthService;
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers(
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
"/swagger-ui.html",
"/api/user/login/v1",
// "/api/user/add/v1",
// "/api/user/speed/test/v1",
// "/api/user/say/hello/v1",
// "/api/user/*/detail/v1",
"/api/crazymaker/duty/info/user/login")
.permitAll()
.anyRequest().authenticated()
// .antMatchers("/image/**").permitAll()
// .antMatchers("/admin/**").hasAnyRole("ADMIN")
.and()
.formLogin().disable()
.sessionManagement().disable()
.cors()
.and()
.addFilterAfter(new OptionsRequestFilter(), CorsFilter.class)
.apply(new JsonLoginConfigurer<>()).loginSuccessHandler(jsonLoginSuccessHandler())
.and()
.apply(new JwtAuthConfigurer<>()).tokenValidSuccessHandler(jwtRefreshSuccessHandler()).permissiveRequestUrls("/logout")
.and()
.logout()
// .logoutUrl("/logout") //默认就是"/logout"
.addLogoutHandler(tokenClearLogoutHandler())
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.and()
.addFilterBefore(springSessionRepositoryFilter(), SessionManagementFilter.class)
.sessionManagement().disable()
;
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/api/user/login/v1",
"/v2/api-docs",
"/swagger-resources/configuration/ui",
"/swagger-resources",
"/swagger-resources/configuration/security",
// "/api/user/say/hello/v1",
// "/api/user/add/v1",
// "/api/user/speed/test/v1",
// "/api/user/*/detail/v1",
"/images/**",
"/swagger-ui.html",
"/webjars/**",
"**/favicon.ico",
"/css/**",
"/js/**",
"/api/crazymaker/info/user/login"
);
}
@Resource
RedisOperationsSessionRepository sessionRepository;
@Resource
public CustomedSessionIdResolver httpSessionIdResolver;
@DependsOn({"sessionRepository", "httpSessionIdResolver"})
@Bean("jwtAuthenticationProvider")
protected AuthenticationProvider jwtAuthenticationProvider() {
return new JwtAuthenticationProvider(sessionRepository, httpSessionIdResolver);
}
public <S extends Session> OncePerRequestFilter springSessionRepositoryFilter() {
CustomedSessionRepositoryFilter<? extends Session> sessionRepositoryFilter = new CustomedSessionRepositoryFilter<>(
sessionRepository);
// sessionRepositoryFilter.setServletContext(this.servletContext);
sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver);
return sessionRepositoryFilter;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(daoAuthenticationProvider())
.authenticationProvider(jwtAuthenticationProvider());
}
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean("daoAuthenticationProvider")
protected AuthenticationProvider daoAuthenticationProvider() throws Exception {
//这里会默认使用BCryptPasswordEncoder比对加密后的密码,注意要跟createUser时保持一致
DaoAuthenticationProvider daoProvider = new DaoAuthenticationProvider();
daoProvider.setUserDetailsService(userDetailsService());
return daoProvider;
}
@Bean
protected JwtRefreshSuccessHandler jwtRefreshSuccessHandler() {
return new JwtRefreshSuccessHandler();
}
@Override
protected UserDetailsService userDetailsService() {
return new UserAuthService();
}
@Bean
protected JsonLoginSuccessHandler jsonLoginSuccessHandler() {
return new JsonLoginSuccessHandler(userAuthService);
}
@Bean
protected TokenClearLogoutHandler tokenClearLogoutHandler() {
return new TokenClearLogoutHandler(userAuthService);
}
@Bean
protected CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "HEAD", "OPTION"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.addExposedHeader(SessionConstants.AUTHORIZATION);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
实战小结
大概通过以上6步,一个集成jwt的springsecurity机制,完整的配置起来了。
具体,请关注 Java 高并发研习社群 【博客园 总入口 】
最后,介绍一下疯狂创客圈:疯狂创客圈,一个Java 高并发研习社群 【博客园 总入口 】
疯狂创客圈,倾力推出:面试必备 + 面试必备 + 面试必备 的基础原理+实战 书籍 《Netty Zookeeper Redis 高并发实战》
疯狂创客圈 Java 死磕系列
- Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战
- Netty 源码、原理、JAVA NIO 原理
- Java 面试题 一网打尽
- 疯狂创客圈 【 博客园 总入口 】
Java 面试题 一网打尽**
- 疯狂创客圈 【 博客园 总入口 】
spring security 原理+实战的更多相关文章
- Spring Security原理分析:系列集合
Spring Security 工作原理概览:https://blog.csdn.net/u012702547/article/details/89629415 spring security执行原理 ...
- Spring Security原理与应用
Spring Security是什么 Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架.它提供了一组可以在Spring应用上下文中配置 ...
- spring security原理
spring security通过一系列过滤器实现其功能,入口过滤器如下(web.xml): <filter> <filter-name>springSecurityFilte ...
- spring security原理-学习笔记1-整体概览
整体概述 运行时环境 Spring Security 3.0需要Java 5.0 Runtime Environment或更高版本. 核心组件 SecurityContextHolder,Securi ...
- spring security原理-学习笔记2-核心组件
核心组件 AuthenticationManager,ProviderManager和AuthenticationProvider AuthenticationManager只是一个接口,实际中是如何 ...
- Spring Security原理篇(一) 启动原理
1.概述 spring security有参考的中文翻译文档https://springcloud.cc/spring-security-zhcn.html 在学习spring security的时候 ...
- spring security 简介+实战
过滤器链: 依赖: security 功能列表: 一.登录验证.权限验证 1.1 httpbasic验证 1.2form验证 建立数据需要遵循RBAC模型 用户表要参考UserDetail创建 实例类 ...
- 214. Spring Security:概述
前言 在之前介绍过了Shiro之后,有好多粉丝问SpringSecurity在Spring Boot中怎么集成.这个系列我们就和大家分享下有关这方面的知识. 本节大纲 一.什么是SpringSecur ...
- Spring Security编程模型
1.采用spring进行权限控制 url权限控制 method权限控制 实现:aop或者拦截器(本质就是之前之后进行控制)--------------------proxy就是 2.权限模型: 本质理 ...
随机推荐
- 【Android - 控件】之MD - RecyclerView的使用
RecyclerView是Android 5.0新特性——Material Design中的一个控件,它将ListView.GridView整合到一起,可以使用极少的代码在ListView.GridV ...
- 将String类型转换为int整数类型
示例如下: public class demo { public static void main(String[] args) { String s="10"; 6 7 //St ...
- 压缩打包介绍、gzip、bzip2、xz压缩工具
第5周第1次课(4月16日) 课程内容: 6.1 压缩打包介绍6.2 gzip压缩工具6.3 bzip2压缩工具6.4 xz压缩工具 6.1 压缩打包介绍 为什么要给文件进行压缩呢?首先压缩和不压缩空 ...
- 第四章 开始Unity Shader学习之旅(3)
1. 程序员的烦恼:Debug 调试(debug),大概是所有程序员的噩梦.而不幸的是,对一个Shader进行调试更是噩梦中的噩梦.这也是造成Shader难写的原因之一--如果发现得到的效果不对,我们 ...
- 使用echarts常用问题总结
1,echarts配合element ui的抽屉插件出现报错,上次解决方法是使用element ui 抽屉的open事件,让在打开事件重新加载,我们项目的需求是点击某个数据,要传递这条数据包含的其他值 ...
- 转:关于JAVA项目中CLASSPATH路径详解
在dos下编译Java程序,就要用到classpath这个概念,尤其是在没有设置环境变量的时候.classpath就是存放.class等编译后文件的路径. javac:如果当前你要编译的Java文件中 ...
- luogu P2704 [NOI2001]炮兵阵地
题目描述 司令部的将军们打算在NM的网格地图上部署他们的炮兵部队.一个NM的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P" ...
- 【Nodejs】326- 从零开发一个node命令行工具
本文由 IMWeb 社区授权转载自腾讯内部 KM 论坛.点击阅读原文查看 IMWeb 社区更多精彩文章. 什么是命令行工具? 命令行工具(Cmmand Line Interface)简称cli,顾名思 ...
- 每周一练 之 数据结构与算法(Set)
这是第四周的练习题,五一放假结束,该收拾好状态啦. 下面是之前分享的链接: 1.每周一练 之 数据结构与算法(Stack) 2.每周一练 之 数据结构与算法(LinkedList) 2.每周一练 之 ...
- Vue中使用iconfont
学习博客:https://www.imooc.com/article/33597?block_id=tuijian_wz