SpringSecurity配置,简单梳理
生活加油:摘一句子:
“我希望自己能写这样的诗。我希望自己也是一颗星星。如果我会发光,就不必害怕黑暗。如果我自己是那么美好,那么一切恐惧就可以烟消云散。于是我开始存下了一点希望—如果我能做到,那么我就战胜了寂寞的命运。”
-----------------------------王小波《我在荒岛上迎接黎明》
————————————————
嗯,刚学的时候,一大堆,明白是配置了啥,不知道为啥那么配,嗯,今天把配置思想和小伙伴分享,是很简单的那种,不足之处请留言.
SpringSecurity的配置基于WebSecurityConfigurerAdapter的实现类,我们这里主要讲基本配置,即configure(HttpSecurity http)方法的配置,其实大都有默认值,我们可以直接用默认值,也可以自己设置.
私以为简单配置三点:
- 认证(SpringSecurity在第一次登录做认证,这时候只进行认证,不进行授权(鉴权处理))
- 表单认证:涉及到的配置包括前端登录请求的url,登录失败,登录成功返回的页面url,已及表单返回字段的用户名密码K的设置,还有登陆成功回调处理和登录失败回调处理.这里要注意登录数据是通过key/value的形式来传递.
- 自定义认证,(验证码):自定义验证采用,在自定义过滤器的方式,在最开始插入一个过滤器链,需要自定义异常,自定义失败登录失败处理器,注意这里的失败处理器要和表单登录使用同一个失败处理器.
- 授权(基于RBAC权限控制):
- 授权这样分析基于RBAC的权限控制,即用户分配角色,角色分配权限.需要配置根据访问路径得到需要的角色的处理类(FilterlnvocationSecurityrMetadataSource),根据返回角色进行鉴权处理类(AccessDecisionManager) , 同时判断是否登录,还需要配置认证失败的处理器.
- 注销登录
- 注销登录默认的url为"/logout".需要配置,注销登录处理器
配置图:
SpringSecurity简单配置图:
下面就上面讲的代码分析
一.认证:
1.configure(HttpSecurity http)方法的配置:
package com.liruilong.hros.config; import com.fasterxml.jackson.databind.ObjectMapper;
import com.liruilong.hros.filter.VerifyCodeFilter;
import com.liruilong.hros.model.Hr;
import com.liruilong.hros.model.RespBean;
import com.liruilong.hros.service.HrService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.*;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter; /**
* @Description :
* @Author: Liruilong
* @Date: 2019/12/18 19:11
*/ @Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
HrService hrService;
@Autowired
CustomFilterInvocationSecurityMetadataSource customFilterInvocationSecurityMetadataSource;
@Autowired
CustomUrlDecisionManager customUrlDecisionManager;
@Autowired
VerifyCodeFilter verifyCodeFilter ;
@Autowired
MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
//.anyRequest().authenticated()
//所有请求的都会经过这进行鉴权处理。返回当前请求需要的角色。
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O object) {
object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
object.setAccessDecisionManager(customUrlDecisionManager);
return object;
}
})
.and().formLogin().usernameParameter("username").passwordParameter("password")
//设置登录请求的url路径
.loginProcessingUrl("/doLogin")
/*需要身份验证时,将浏览器重定向到/ login
我们负责在请求/ login时呈现登录页面
当身份验证尝试失败时,将浏览器重定向到/ login?error(因为我们没有另外指定)
当请求/ login?error时,我们负责呈现失败页面
成功注销后,将浏览器重定向到/ login?logout(因为我们没有另外指定)
我们负责在请求/ login?logout时呈现注销确认页面*/
.loginPage("/login")
//登录成功回调
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
Hr hr = (Hr) authentication.getPrincipal();
//密码不回传
hr.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", hr);
//将hr转化为Sting
String s = new ObjectMapper().writeValueAsString(ok);
out.write(s);
out.flush();
out.close();
}
})
//登失败回调
.failureHandler(myAuthenticationFailureHandler)
//相关的接口直接返回
.permitAll().and().logout()
//注销登录
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
out.flush();
out.close();
}
})
.permitAll().and().csrf().disable().exceptionHandling()
//没有认证时,在这里处理结果,不要重定向
.authenticationEntryPoint(
//myAuthenticationEntryPoint;
new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse resp, AuthenticationException authException) throws IOException, ServletException {
resp.setContentType("application/json;charset=utf-8");
resp.setStatus(401);
PrintWriter out = resp.getWriter();
RespBean respBean = RespBean.error("访问失败!");
if (authException instanceof InsufficientAuthenticationException) {
respBean.setMsg("请求失败,请联系管理员!");
}
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
});
}
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(hrService);
}
/**
* @Author Liruilong
* @Description 放行的请求路径
* @Date 19:25 2020/2/7
* @Param [web]
* @return void
**/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/auth/code","/login","/css/**","/js/**", "/index.html", "/img/**", "/fonts/**","/favicon.ico");
}
}
2.失败处理器定义:
package com.liruilong.hros.config; import com.fasterxml.jackson.databind.ObjectMapper;
import com.liruilong.hros.Exception.ValidateCodeException;
import com.liruilong.hros.model.RespBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.*;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter; /**
* @Description :
* @Author: Liruilong
* @Date: 2020/2/11 23:08
*/
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = httpServletResponse.getWriter();
RespBean respBean = RespBean.error(e.getMessage());
// 验证码自定义异常的处理
if (e instanceof ValidateCodeException){
respBean.setMsg(e.getMessage());
//Security内置的异常处理
}else if (e instanceof LockedException) {
respBean.setMsg("账户被锁定请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
respBean.setMsg("密码过期请联系管理员!");
} else if (e instanceof AccountExpiredException) {
respBean.setMsg("账户过期请联系管理员!");
} else if (e instanceof DisabledException) {
respBean.setMsg("账户被禁用请联系管理员!");
} else if (e instanceof BadCredentialsException) {
respBean.setMsg("用户名密码输入错误,请重新输入!");
}
//将hr转化为Sting
out.write(new ObjectMapper().writeValueAsString(respBean));
out.flush();
out.close();
}
@Bean
public MyAuthenticationFailureHandler getMyAuthenticationFailureHandler(){
return new MyAuthenticationFailureHandler();
}
}
3.前端将JSON数据转换为K-V形式:
//post请求的封装K-v形式
let base = '';
export const postKeyValueRequest = (url, params) => {
return axios({
method: 'post',
url: `${base}${url}`,
data: params,
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
} return ret
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
}
});
}
4.自定义过滤器:
package com.liruilong.hros.filter; import com.liruilong.hros.Exception.ValidateCodeException;
import com.liruilong.hros.config.MyAuthenticationFailureHandler;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; /**
* @Description :
* @Author: Liruilong
* @Date: 2020/2/7 19:39
*/
@Component
public class VerifyCodeFilter extends OncePerRequestFilter { @Bean
public VerifyCodeFilter getVerifyCodeFilter() {
return new VerifyCodeFilter();
}
@Autowired
MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (StringUtils.equals("/doLogin", request.getRequestURI())
&& StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
// 1. 进行验证码的校验
try {
String requestCaptcha = request.getParameter("code");
if (requestCaptcha == null) {
throw new ValidateCodeException("验证码不存在");
}
String code = (String) request.getSession().getAttribute("yanzhengma");
if (StringUtils.isBlank(code)) {
throw new ValidateCodeException("验证码过期!");
}
code = code.equals("0.0") ? "0" : code;
logger.info("开始校验验证码,生成的验证码为:" + code + " ,输入的验证码为:" + requestCaptcha);
if (!StringUtils.equals(code, requestCaptcha)) {
throw new ValidateCodeException("验证码不匹配");
}
} catch (AuthenticationException e) {
// 2. 捕获步骤1中校验出现异常,交给失败处理类进行进行处理
myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
} finally {
filterChain.doFilter(request, response);
}
} else {
filterChain.doFilter(request, response);
} } }
5.自定义异常:
package com.liruilong.hros.Exception; import org.springframework.security.core.AuthenticationException; /**
* @Description :
* @Author: Liruilong
* @Date: 2020/2/8 7:24
*/ public class ValidateCodeException extends AuthenticationException { public ValidateCodeException(String msg) {
super(msg);
} }
二.授权:
RBAC模型实体图
1.开发者自定义 FilterlnvocationSecurityrMetadataSource
package com.liruilong.hros.config; import com.liruilong.hros.mapper.MenuMapper;
import com.liruilong.hros.model.Menu;
import com.liruilong.hros.model.Role;
import com.liruilong.hros.service.MenuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher; import java.util.Collection;
import java.util.List; /**
* @Description : 权限处理,根据请求,分析需要的角色,该类的主要功能就是通过当前的请求地址,获取该地址需要的用户角色
* @Author: Liruilong
* @Date: 2019/12/24 12:17
*/
@Component
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;
//路径比较工具
AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* @return java.util.Collection<org.springframework.security.access.ConfigAttribute>
* @Author Liruilong
* @Description 当前请求需要的角色
* @Date 18:13 2019/12/24
* @Param [object]
**/
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
//获取当前请求路径
String requestUrl = ((FilterInvocation) object).getRequestUrl();
//获取所有的菜单url路径
List<Menu> menus = menuService.getAllMenusWithRole();
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(), requestUrl)) {
//拥有当前菜单权限的角色
List<Role> roles = menu.getRoles();
String[] strings = new String[roles.size()];
for (int i = 0; i < roles.size(); i++) {
strings[i] = roles.get(i).getName();
}
return SecurityConfig.createList(strings);
}
}
// 没匹配上的资源都是登录
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
} @Override
public boolean supports(Class<?> clazz) {
return true;
}
}
2.自定义 AccessDecisionManager
package com.liruilong.hros.config; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component; import java.util.Collection; /**
* @Description : 判断当前用户是否具备菜单访问,当一个请求走完 FilterlnvocationSecurityMetadataSource 中的 getAttributes 方法后,接下来就会 来到 AccessDecisionManager 类中进行角色信息的比对
* @Author: Liruilong
* @Date: 2019/12/24 19:12
*/
@Component
public class CustomUrlDecisionManager implements AccessDecisionManager { /**
* @return void
* @Author Liruilong
* @Description decide 方法有三个参数, 第一个参数包含当前登录用户的信息;
* 第二个参数则是一个 Filterlnvocation 对 象 ,可以 获 取当前请求对 象等;
* 第 三个参 数就是 FilterlnvocationSecurityMetadataSource 中的 getAttributes 方法的返回值, 即当前请求 URL 所 需要的角色。
* @Date 18:28 2020/2/13
* @Param [authentication, object, configAttributes]
**/ @Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes)
throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : configAttributes) {
String needRole = configAttribute.getAttribute();
if ("ROLE_LOGIN".equals(needRole)) {
//判断用户是否登录
if (authentication instanceof AnonymousAuthenticationToken) {
throw new AccessDeniedException("尚未登录,请登录!");
} else {
return;
}
}
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)) {
return;
}
}
}
throw new AccessDeniedException("权限不足,请联系管理员!");
} @Override
public boolean supports(ConfigAttribute attribute) {
return true;
} @Override
public boolean supports(Class<?> clazz) {
return true;
}
}
// authorities.stream().anyMatch((authority) ->authority.getAuthority().equals(attribute));
RBAC模型分析
3. 当然还需要用户类去实现UserDatails接口
package com.liruilong.hros.model; import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreType;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import java.util.ArrayList;
import java.util.Collection;
import java.util.List; public class Hr implements UserDetails {
private Integer id; private String name; private String phone; private String telephone; private String address; private Boolean enabled; private String username; private String password; private String userface; private String remark; private List<Role> roles; public Integer getId() {
return id;
} public void setId(Integer id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name == null ? null : name.trim();
} public String getPhone() {
return phone;
} public void setPhone(String phone) {
this.phone = phone == null ? null : phone.trim();
} public String getTelephone() {
return telephone;
} public void setTelephone(String telephone) {
this.telephone = telephone == null ? null : telephone.trim();
} public String getAddress() {
return address;
} public void setAddress(String address) {
this.address = address == null ? null : address.trim();
} public void setEnabled(Boolean enabled) {
this.enabled = enabled;
} @Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username == null ? null : username.trim();
} @Override
public String getPassword() {
return password;
} public void setPassword(String password) {
this.password = password == null ? null : password.trim();
} public String getUserface() {
return userface;
} public void setUserface(String userface) {
this.userface = userface == null ? null : userface.trim();
} public String getRemark() {
return remark;
} public void setRemark(String remark) {
this.remark = remark == null ? null : remark.trim();
} @Override
public boolean isAccountNonExpired() {
return true;
} @Override
public boolean isAccountNonLocked() {
return true;
} @Override
public boolean isCredentialsNonExpired() {
return true;
} @Override
public boolean isEnabled() {
return enabled;
} @Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>(roles.size());
roles.stream().forEach( (role) ->authorities.add(new SimpleGrantedAuthority(role.getName())));
return authorities;
} public List<Role> getRoles() {
return roles;
} public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
_______________________________________________________________________________________________________
嗯.以上就是对SpringSecurity配置的理解,不足之处请小伙伴留言/生活加油!
SpringSecurity配置,简单梳理的更多相关文章
- RocketMQ 简单梳理 及 集群部署笔记【转】
一.RocketMQ 基础知识介绍Apache RocketMQ是阿里开源的一款高性能.高吞吐量.队列模型的消息中间件的分布式消息中间件. 上图是一个典型的消息中间件收发消息的模型,RocketMQ也 ...
- Memcached概念、作用、运行原理、特性、不足简单梳理(1)
大家可能对memcached这种产品早有了解,或者已经应用在自己的网站中了,但是也有一些朋友从来都没有听说过或者使用过.这都没什么关系,本文旨在从各个角度综合的介绍这种产品,尽量深入浅出,如果能对您现 ...
- Docker+nginx+tomcat7配置简单的负载均衡
本文为原创,原始地址为:http://www.cnblogs.com/fengzheng/p/4995513.html 本文介绍在Docker上配置简单的负载均衡,宿主机为Ubuntu 14.04.2 ...
- 机器学习&数据挖掘笔记(常见面试之机器学习算法思想简单梳理)
机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 作者:tornadomeet 出处:http://www.cnblogs.com/tornadomeet 前言: 找工作时( ...
- [转]机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理)
机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) 转自http://www.cnblogs.com/tornadomeet/p/3395593.html 前言: 找工作时(I ...
- Nginx负载均衡配置简单配置方法
http://www.jb51.net/article/121235.htm Nginx作为负载均衡服务器,用户请求先到达nginx,再由nginx根据负载配置将请求转发至不同的Web服务器.下面通过 ...
- UEditor之实现配置简单的图片上传示例
UEditor之实现配置简单的图片上传示例 原创 2016年06月11日 18:27:31 开心一笑 下班后,阿华到楼下小超市买毛巾,刚买完出来,就遇到同一办公楼里另一家公司的阿菲,之前与她远远的有过 ...
- C#正则表达式_简单梳理_Emoji表情字符处理
A-最近一直有接触到正则表达式,现对其做简单梳理: private const RegexOptions OPTIONS = RegexOptions.IgnoreCase | RegexOption ...
- RocketMQ 简单梳理 及 集群部署笔记
一.RocketMQ 基础知识介绍Apache RocketMQ是阿里开源的一款高性能.高吞吐量.队列模型的消息中间件的分布式消息中间件. 上图是一个典型的消息中间件收发消息的模型,RocketMQ也 ...
随机推荐
- UltraEdit设置打开的文件类型,怎么打开大文本文件
点击高级,配置,选择文件处理下的临时文件,设置如图即可打开超大文本文件. 补充:视图——显示行号.
- JS高级---把随机数对象暴露给window成为全局对象
通过自调用函数产生一个随机数对象, 在自调用函数外面, 调用该随机数对象方法产生随机数 把随机数对象暴露给window成为全局对象 全局变量 自调用一个函数 创建一个空的随机构造函数,给原型对 ...
- 如何在windows和linux搭建django环境
注:本文以python3为例,python2.x是一样的不再赘述 提前准备: python/python3已经安装好,如果没有请参考以下资料进行安装 linux安装python3 win安装pytho ...
- MongoDB C#驱动使用方法
string connStr = ConfigurationManager.ConnectionStrings["MongoDBConnStr"].ConnectionString ...
- Abp.Core运行时提示XX没有实现,或者没有依赖注入的问题,或者调试时提示做出更改的问题
因为abp的web层对application层有项目引用,但是对domain层并不存在项目引用,而是bin目录下直接引用的dll文件,所以当domain层修改后不会自动将dll文件同步过去.所以有时候 ...
- 8.14-T1村通网(pupil)
题目大意 要建设一个村庄的网络 有两种操作可选 1.给中国移动交宽带费,直接连网,花费为 A. 2.向另外一座有网的建筑,安装共享网线,花费为 B×两者曼哈顿距离. 题解 显然的最小生成树的题 见 ...
- Euler Sums系列(六)
\[\Large\displaystyle \sum_{n=1}^{\infty}\frac{H_{2n}}{n(6n+1)}\] \(\Large\mathbf{Solution:}\) Let \ ...
- Eclipse配置C++11环境详细介绍
转:https://blog.csdn.net/wgxh05/article/details/54021049 本文记录Eclipse配置C++11开发所有作者遇到的情况,包括跨工程文件编译,内联文件 ...
- C语言程序设计(三)——顺序程序设计
目录: 常量.c 常量分类: (1)字面常量(直接常量):数值常量(分为整型常量和浮点型常量).字符串常量和字符常量 (2)符号常量 (3)常变量 \f,换页,将当前位置移到下一页的开头 \v,垂 ...
- Python 多任务(线程) day2 (1)
结论:多线程全局变量是共享的 (03) 因为多线程一般是配合使用,如果不共享,那么就要等到一个线程执行完,再把变量传递给另一个线程,就变成单线程了 但是如果多个线程同时需要修改一个全局变量,就会出现资 ...