1、项目结构介绍

项目有使用到,redis和swagger,不在具体介绍;

2、手动鉴权和用户信息参数获取(繁杂,冗余)

2.1用户实体类

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户实体
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User { //用户编号
private String userId; //用户名
private String userName; //用户密码
private String userPwd; //手机号
private String userTel; //邮箱
private String userEmail; //登录ip
private String lastLoginIp; }

2.2 业务层

2.2.1 接口

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户业务接口
*/
public interface UserService { /**
* @author : huayu
* @date : 4/11/2022
* @param : [userName, userPwd]
* @return : java.lang.String
* @description : 处理用户登录请求,校验用户信息是否正确,如果正确返回令牌
*/
String userLogin(String userName,String userPwd); /**
* @author : huayu
* @date : 5/11/2022
* @param : [userToken]
* @return : void
* @description : 用户登出
*/
void userLogout(String userToken); }

2.2.3 实现类

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户业务接口实现类
*/
@Service
public class UserServiceImpl implements UserService { @Autowired
private RedisUtils redisUtils; @Override
public String userLogin(String userName, String userPwd) { //TODO 调用持久层接口,查询用户信息是否真确,如果查询到用户信息,说明用户存在,如果查询不到没说明用户不存在
if("KH96".equals(userName) && "123456".equals(userPwd)){
//代表用户的登录信息是正确的,可以生成token令牌,返回该客户端
//令牌的生成规则,一般是随机串,长度不一,一般使用方式:可以选择UUID生成,或者将用户编号+其他信息进行md5加密,比如jwt
String userToken = UUID.randomUUID().toString().replace("-", ""); //简单模拟数据库查询出的用户详情
User userLogin = User.builder()
.userId("T001")
.userName("KH96")
.userTel("13801020304")
.userEmail("kh97@kgc.com")
.lastLoginIp("127.0.0.1")
.build(); //将查询的用户详情,直接一生成的token作为key,存入到redis缓存中,并增加时效(有过期时间,比如30分钟)
redisUtils.set(userToken,userLogin,10*60); //返回有效的token令牌,此令牌就代表登录成功的用户
return userToken; } //鉴权失败,返回null;
return null;
} @Override
public void userLogout(String userToken) {
// 直接将用户的token令牌长redis中删除
if(redisUtils.hasKey(userToken)){
redisUtils.del(userToken);
} } }

2.3 控制层

2.3.1 BaseController

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 所有控制器的供父类,将所有有控制器要使用的公共方法,抽离到父类中,方便方法复用
*/
public class BaseController { /**
* @author : huayu
* @date : 4/11/2022
* @param : [request, paramName]
* @return : java.lang.String
* @description : 从请求中获取参数,获取参数值,如果没有获取到,用空字符地带默认值的null
*/
protected String getParameter(HttpServletRequest request,String paramName){
return request.getParameter(paramName) == null ? "" : request.getParameter(paramName);
} protected String getParameter(HttpServletRequest request,String paramName,String defaultValue){
return request.getParameter(paramName) == null ? defaultValue : request.getParameter(paramName);
} /**
* @author : huayu
* @date : 4/11/2022
* @param : [request]
* @return : java.lang.String
* @description : getRemoteIp
*/
protected String getRemoteIp(HttpServletRequest request) {
// 获取ip
String ip = request.getHeader("X-Real-IP");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("x-forwarded-for");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
} return ip;
} }

2.3.2 LoginController

用户登录:

  1. 根据用户名密码判断用户是否存在
  2. 存在生成token,返回给前端;不存在提示用户名或密码错误;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户登录登出
*/
@Slf4j
@RestController
@Api(tags = "用户登录登出类")
public class LoginController extends BaseController { @Autowired
private UserService userService; @PostMapping("/login")
@ApiOperation(value = "用户登录",notes = "支持token鉴权")
@ApiImplicitParams({
@ApiImplicitParam(value = "用户名",name = "userName",defaultValue = "KH96"),
@ApiImplicitParam(value = "用户密码",name = "userPwd",defaultValue = "123456")
})
public RequestResult<String> doLogin(HttpServletRequest request){ //获取请求中的用户名和密码参数
String loginName = this.getParameter(request, "userName", "KH96");
String loginPwd = this.getParameter(request, "userPwd", "123456"); //调用业务接口,校验登录请求用户信息是否正确,如果正确,返回token令牌,否者返回null
String userToken = userService.userLogin(loginName, loginPwd); //判断用户是否鉴权成功
if(StringUtils.isNotBlank(userToken)){
//登录鉴权成功,返回给客户端有限token令牌,前端保存,后续请求使用
return ResultBuildUtil.success(userToken);
} return ResultBuildUtil.fail("901","用户名或密码错误!");
} }

2.3.3 UserController

收藏列表查询:

  1. 看请求头参数中是否携带正确的token,进行鉴权
  2. 鉴权成功获取用户信息,查询对应数据,鉴权失败,跳转到用户登录页面;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户操作入口
*/
@Slf4j
@RestController
@Api(tags = "用户个人中心")
public class UserController { @Autowired
private RedisUtils redisUtils; /**
* @author : huayu
* @date : 4/11/2022
* @param : [request]
* @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
* @description : 用户查询收藏列表,需要 token鉴权操作
*/
@GetMapping("/collectionList")
@ApiOperation(value = "收藏列表",notes = "支持token自动鉴权")
public RequestResult<String> collectList(HttpServletRequest request){ //直接获取前端请求的token参数进行鉴权操作,省略业务层接口操作
String userToken = request.getHeader("token"); //判断token是否合法,如果没有直接鉴权失败,跳转到登录
if(StringUtils.isBlank(userToken)){
//返回 鉴权失败
return ResultBuildUtil.fail("902","token参数为空,请求失败,请求重新登录");
} //判断token是否有效,如果redis中可以根据此token获取到信息,说明用户登录成功,且有效,否者鉴权失败,跳转到登录
Object userObj = redisUtils.get(userToken);
if(ObjectUtils.isEmpty(userObj)){
//redis中没有该token的鉴权信息,饭后鉴权失败
return ResultBuildUtil.fail("903","token 参数失效,请重新登录!");
} //请求token值有效,直接将redis中存放的用户信息,转换为登录用户详情
User loginUser = JSON.parseObject(userObj.toString(), User.class); //TODO 将鉴权通过的用户信息作为信息,调用查询用户收藏列表业务接口,获取该用户的收藏信息,返回给前端
log.info("------ 用户查看收藏列表,鉴权通过,当前登录用户:{} ------",loginUser); //返货成功的收藏列表数据
return ResultBuildUtil.success("查询用户收藏列表成功!\n "+loginUser); } }

2.4 测试

2.4.1 测试用户登录

2.4.1.1 用户登陆成功

2.4.1.2 用户token添加成功

2.4.2 测试查询用户收藏信息

2.4.2.1 使用错误的token

2.4.2.2 使用正确的token

2.5 总结

虽然业务可以完成,但是每次都进行这样的手动鉴权和手动获取用户数据,比较繁琐,而且大量代码冗余;

3、自动鉴权和自动用户信息参数获取

3.1 原理

  • 自动鉴权

    • 自定义注解+自定义拦截器
  • 自动参数获取
    • 自定义注解+自定义解析器

3.2 自定义注解

3.2.1 自定义token鉴权注解

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 请求token许可自定义注解,只要请求处理方法上加了此注解,就需要token鉴权
*/ @Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPermission { }

3.2.2 自定义参数解析(获取)注解

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义请求用户注解,凡是在目标请求处理方法中,使用此注解,就自动解析redis中保存的登录用户,绑定到实体属性上
*/
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestUser { }

3.3 自定义请求token许可拦截器

  1. 判断目标请求方法是否需要鉴权,是返回true,否发false

    • 判断目标请求方法上是否有 添加了 请求token许可注解 @RequestPermission;
    • 判断目标请求类方法上是否 添加了 请求token许可注解 @RequestPermission;
  2. 鉴权
    • 鉴权成功不拦截;
    • 鉴权失败拦截;

回顾过滤器和拦截器的执行时机

​ 过滤器是在DispatcherServlet处理之前拦截,拦截器是在DispatcherServlet处理请求然后调用控制器方法(即我们自己写的处理请求的方法,用@RequestMapping标注)之前进行拦截。

/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义请求token许可拦截器,拦截所有增加了请求token许可注解的请求,进行鉴权操作
*/
@Slf4j
public class TokenPermissionInterceptor implements HandlerInterceptor { @Autowired
private RedisUtils redisUtils; @Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object obj) throws Exception { // 判断是否需要校验请求token许可,只需要看目标请求处理方法上是否有自定义请求token许可注解-TokenPermission
if (this.checkTargetMethodHasTokenPermission(obj)){
// 需要进行请求token许可校验,从请求头中获取token参数,做token鉴权业务逻辑处理
String userToken = httpServletRequest.getHeader("token"); // 判断token是否合法,如果没有,直接鉴权失败,跳转到登录
if(StringUtils.isBlank(userToken)){
// token参数为空,返回鉴权失败
this.returnTokenCheckJson(httpServletResponse, "902", "token参数为空,鉴权失败,请重新登录!"); // 权限校验失败,需要拦截请求
return false;
} // 判断token是否有效,如果redis中可以根据此token值获取到信息,说明用户登录鉴权成功,且有效,否则鉴权失败,跳转到登录
if(ObjectUtils.isEmpty(redisUtils.get(userToken))){
// redis中没有该token的鉴权信息,返回鉴权失败
this.returnTokenCheckJson(httpServletResponse, "903", "token参数失效,鉴权失败,请重新登录!"); // 权限校验失败,需要拦截请求
return false;
}
} // 不需要拦截,直接放行
return true;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object obj, ModelAndView modelAndView) throws Exception { } @Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object obj, Exception ex) throws Exception { } /**
* @author : zhukang
* @date : 2022/11/4
* @param : [handler]
* @return : boolean
* @description : 判断目标请求方法是否需要鉴权,是返回true,否发false
*/
public boolean checkTargetMethodHasTokenPermission(Object handler){ // 判断当前处理的handler是否已经映射到目标请求处理方法,看是不是HandlerMethod的实例对象
if(handler instanceof HandlerMethod){
// 强转为目标请求处理方法的实例对象,因为:HandlerMethod对象封装了目标请求处理方法的所有内容,包括方法所有的声明
HandlerMethod handlerMethod = (HandlerMethod) handler; // 尝试获取目标请求处理方法上,是否添加了自定义请求token许可注解-TokenPermission,取到了就是加了,取不到就没加
RequestPermission requestPermission = handlerMethod.getMethod().getAnnotation(RequestPermission.class); // 判断是否成功获取到请求token许可注解,如果没有获取到,不一定代表不需要进行权限校验,因为此注解还可能加载处理类,要再次尝试从请求处理方法所在处理类上获取该注解
if(ObjectUtils.isEmpty(requestPermission)){
requestPermission = handlerMethod.getMethod().getDeclaringClass().getAnnotation(RequestPermission.class);
} // 最终判断是否需要进行请求token许可校验,如果获取到了,说明需要校验,否则直接放行
return null != requestPermission;
} // 请求不是需要进行鉴权操作,直接返回false
return false;
} /**
* @author : zhukang
* @date : 2022/11/4
* @param : [response, returnCode, returnMsg]
* @return : void
* @description : 拦截器中,token鉴权失败的统一返回json处理
*/
public void returnTokenCheckJson(HttpServletResponse response, String returnCode, String returnMsg){
response.setCharacterEncoding("utf-8");
response.setContentType("application/json; charset=utf-8");
try {
response.getWriter().print(JSON.toJSONString(ResultBuildUtil.fail(returnCode, returnMsg)));
} catch (IOException e) {
log.warn("****** 请求token许可拦截器返回结果异常:{} ******", e.getMessage());
}
} }

3.4 自定义请求用户参数解析器

通过鉴权后:

  1. 判断 目标请求处理方法是否 自定义参数解析注解@RequestUser,且目标实体参数类型是User;
  2. 通过token为key取用redis中的用户信息;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义请求用户参数解析器,自动根据 @RequestUser 注解,解析通过鉴权的用户信息,绑定到请求处理方法的用户参数上,要配合请求token许可鉴权使用
*/
public class MyDefineUserResolver implements HandlerMethodArgumentResolver { @Autowired
private RedisUtils redisUtils; @Override
public boolean supportsParameter(MethodParameter parameter) {
// 决定是否需要执行参数解析,如果目标请求处理方法使用了自定义参数注解@RequestUser,且目标实体参数类型是User,就需要进行解析,否则不需要解析
return parameter.hasParameterAnnotation(RequestUser.class) && parameter.getParameterType().isAssignableFrom(User.class);
} @Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
// 根据上面supportsParameter方法,如果返回的是true,代表需要执行方法参数解析,如果返回false,不需要执行参数解析
// 从redis中获取token令牌保存的用户信息,转换为目标用户对象,绑定到请求处理方法的入参中,前提:鉴权是通过
// TODO 在获取redis中保存的用户信息时,需要做非空校验,防止解析时过期
return JSON.parseObject(redisUtils.get(webRequest.getHeader("token")).toString(), User.class);
} }

3.5 自定义webmvc配置类

  1. 手动创建请求token许可拦截器对象,放入容器

    • 手动添加自定义拦截器到系统的拦截器组中;
  2. 手动创建自定义解析器对象,放入容器
    • 手动添加自定义拦截器到系统的拦截器组中;
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 自定义webmvc配置类,可以自定义
*/
@Configuration
public class MyDefineWebMVcConfig implements WebMvcConfigurer { /**
* @author : huayu
* @date : 4/11/2022
* @param : []
* @return : com.kgc.scd.interceptor.TokenPermissionInterceptor
* @description : 手动创建请求token许可拦截器对象,放入容器,方便加入到系统拦截器组中
*/
@Bean
public TokenPermissionInterceptor tokenPermissionInterceptor(){
return new TokenPermissionInterceptor();
} /**
* @author : huayu
* @date : 4/11/2022
* @param : []
* @return : com.kgc.scd.resolver.MyDefineUserResolver
* @description : 手动创建自定义解析器对象,放入容器,方便加入到系统解析器中
*/
@Bean
public MyDefineUserResolver myDefineUserResolver(){
return new MyDefineUserResolver();
} @Override
public void configurePathMatch(PathMatchConfigurer configurer) { } @Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { } @Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) { } @Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { } @Override
public void addFormatters(FormatterRegistry registry) { } @Override
public void addInterceptors(InterceptorRegistry interceptorRegistry) { //手动添加自定义拦截器到系统的拦截器组中,才可以生效,否者不生效
interceptorRegistry.addInterceptor(tokenPermissionInterceptor()).addPathPatterns("/**"); } @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { } @Override
public void addCorsMappings(CorsRegistry registry) { } @Override
public void addViewControllers(ViewControllerRegistry registry) { } @Override
public void configureViewResolvers(ViewResolverRegistry registry) { } @Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
//手动将容器中自定义请求用户解析器,加入到系统解析器中
argumentResolvers.add(myDefineUserResolver());
} @Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { } @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { } @Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { } @Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { } @Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { } @Override
public Validator getValidator() {
return null;
} @Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}

3.5 UserController

  1. 在方法上或类上添加 自定义请求token许可注解 @RequestPermission ;

    • 进行用户token自动鉴权;
  2. 在参数添加 自定义参数解析注解 @RequestUser
    • 进行用户类型参数自动解析;(通过健全后,自动获取用户参数)
/**
* Created On : 4/11/2022.
* <p>
* Author : huayu
* <p>
* Description: 用户操作入口
*/
@Slf4j
@RestController
@Api(tags = "用户个人中心")
@RequestPermission //使用自定义请求token许可注解 当查看足迹列表时,需要进行token鉴权; 如果在类上增加了此注解,就地表当前类的所有处理方法都需要鉴权;
public class UserController { @Autowired
private RedisUtils redisUtils; /**
* @author : huayu
* @date : 4/11/2022
* @param : [request]
* @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
* @description : 用户查询 足迹列表,需要 token鉴权操作
*/
@GetMapping("/footList")
@ApiOperation(value = "足迹列表",notes = "支持token自动鉴权")
@RequestPermission //使用自定义请求token许可注解 当查看足迹列表时,需要进行token鉴权
public RequestResult<String> footList(@RequestUser @ApiIgnore User loginUser){ //TODO 当遇到需要进行token鉴权操作,就必须重复上面的收藏鉴权操作,代码冗余,不利于扩展和维护
//TODO 推荐用法:使用自定义实现自动鉴权,当添加了需要进行鉴权的自定义注解,执行鉴权操作,如果没添加则不需要 //TODO 如果token鉴权成功,直接获取用户信息,调用业务接口,查询用户的足迹列表数据,返回前端 log.info("------ 用户查看足迹列表,鉴权通过,当前登录用户:{} ------",loginUser); //返回成功的收藏列表数据
return ResultBuildUtil.success("查询用户足迹列表成功!"+loginUser); } }

3.6 LoginController 用户登出

/**
* @author : huayu
* @date : 4/11/2022
* @param : [token]
* @return : com.kgc.scd.uitl.RequestResult<java.lang.String>
* @description : 用户退出登录
*/
@GetMapping("/logout")
@ApiOperation(value = "用户登出", notes = "用户删除token,退出系统")
public RequestResult<String> doLogout(@RequestHeader String token){ // 调用业务接口,删除用户的token令牌
userService.userLogout(token); return ResultBuildUtil.success("退出登录成功!");
}

3.7 测试

3.7.1 测试获取用户足迹

3.2.1.1 使用错误的token

3.2.1.2 使用正确的token

3.7.2 测试用户登出

3.7.2.1 用户登出成功

3.7.2.2 用户token被删除

3.8 总结

使用自定义鉴权注解 自动鉴权,和自定义参数解析注解 自动获取参数;代码量大大减少,而且操作方便;

SpringCloud(八) - 自定义token令牌,鉴权(注解+拦截器),参数解析(注解+解析器)的更多相关文章

  1. 基于token机制鉴权架构

    常见的鉴权方式有两种,一种是基于session,另一种是基于token方式的鉴权,我们来浅谈一下两种 鉴权方式的区别. 两种鉴权方式对比 session 安全性:session是基于cookie进行用 ...

  2. Session, Token, OAuth 鉴权那些事儿

    鉴权那些事 整体思路 无论什么样的服务, Web 服务总是不能绕开鉴权这个话题的, 通过有效的鉴权手段来保护网站数据, 来为特定用户提供服务. 整体来说, 有三种方式: Session-Cookie ...

  3. SpringBoot使用token简单鉴权

    本文使用SpringBoot结合Redis进行简单的token鉴权. 1.简介 刚刚换了公司,所以最近有些忙碌,所以一直没有什么产出,最近朋友问我登录相关的,所以这里先写一篇简单使用token鉴权的文 ...

  4. Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(五):鉴权 gRPC-Interceptor 拦截器实战

    拦截器(gRPC-Interceptor)类似于 Gin 中间件(Middleware),让你在真正调用 RPC 服务前,进行身份认证.参数校验.限流等通用操作. 系列 云原生 API 网关,gRPC ...

  5. 简单实现Shiro单点登录(自定义Token令牌)

    1. MVC Controller 映射 sso 方法. /** * 单点登录(如已经登录,则直接跳转) * @param userCode 登录用户编码 * @param token 登录令牌,令牌 ...

  6. springboot 入门八-自定义配置信息(编码、拦截器、静态资源等)

    若想实际自定义相关配置,只需要继承WebMvcConfigurerAdapter.WebMvcConfigurerAdapter定义些空方法用来重写项目需要用到的WebMvcConfigure实现.具 ...

  7. shiro,基于springboot,基于前后端分离,从登录认证到鉴权,从入门到放弃

    这个demo是基于springboot项目的. 名词介绍: ShiroShiro 主要分为 安全认证 和 接口授权 两个部分,其中的核心组件为 Subject. SecurityManager. Re ...

  8. SpringBoot系列: Web应用鉴权思路

    ==============================web 项目鉴权============================== 主要的鉴权方式有:1. 用户名/密码鉴权, 然后通过 Sess ...

  9. Go+gRPC-Gateway(V2) 微服务实战,小程序登录鉴权服务(六):客户端基础库 TS 实战

    小程序登录鉴权服务,客户端底层 SDK,登录鉴权.业务请求.鉴权重试模块 Typescript 实战. 系列 云原生 API 网关,gRPC-Gateway V2 初探 Go + gRPC-Gatew ...

  10. AOP自定义注解鉴权

    刚出来工作那会或者在学校的时候,经常听到说AOP(面向对象编程,熟称切面)的用途是日志.鉴权等.但是那会不会,后面学会了,又没有写博客记录,今天写给大伙,希望能帮到大家 一.学习目标:利用AOP+自定 ...

随机推荐

  1. 思科 ISE 3.4 发布新增功能概览

    思科 ISE 3.4 发布,新增功能概览   目录 Active Directory 首选 DC 选择 保留使用设置 本地化 ISE 安装 FQDN 到 SGT 映射 思科 ISE 和 TrustSe ...

  2. 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-4-启动浏览器-基于Maven(详细教程)

    1.简介 上一篇文章,宏哥已经在搭建的java项目环境中添加jar包实践了如何启动浏览器,今天就在基于maven项目的环境中给小伙伴们或者童鞋们演示一下如何启动浏览器. 2.eclipse中新建mav ...

  3. Slate文档编辑器-WrapNode数据结构与操作变换

    Slate文档编辑器-WrapNode数据结构与操作变换 在之前我们聊到了一些关于slate富文本引擎的基本概念,并且对基于slate实现文档编辑器的一些插件化能力设计.类型拓展.具体方案等作了探讨, ...

  4. SQL SERVER日常运维巡检系列——结构设计

    前言 做好日常巡检是数据库管理和维护的重要步骤,而且需要对每次巡检日期.结果进行登记,同时可能需要出一份巡检报告. 本系列旨在解决一些常见的困扰: 不知道巡检哪些东西不知道怎么样便捷体检机器太多体检麻 ...

  5. Qt+OPC开发笔记(一):OPCUA介绍、open62541介绍、编译与基础环境Demo

    前言   本篇介绍OPC协议,相关开源库.编译并搭建Qt开发OPC的基础环境.   Demo      OPC   OPC(OLE for Process Control)是一个工业标准,用于实现工业 ...

  6. 叮咚~ 你的Techo大会云存储专场邀请函到了!

    12月19日至20日,由腾讯主办的2020 Techo Park开发者大会将于北京召开.Techo Park 开发者大会是由腾讯发起的面向全球开发者和技术爱好者的年度盛会,作为一个专注于前沿技术研讨的 ...

  7. 对象存储 COS 推出一站式内容审核服务,助力打造绿色互联网

    今年,国家网信办深入推进"清朗·春节网络环境"专项行动.截至3月24日,网信办共累计清理相关违法违规信息208万余条,处置账号7.2万余个,协调关闭.取消备案网站平台2300余家. ...

  8. cocos2d 的故事

    https://en.wikipedia.org/wiki/Cocos2d The history of Cocos2d in a glimpse – RETRO.MOE http://los-coc ...

  9. Golang实战:深入解析国密算法在Go语言中的应用与优化

    Golang实战:深入解析国密算法在Go语言中的应用与优化 引言 随着信息技术的迅猛发展,数据安全成为企业和个人关注的焦点.国密算法(SM系列算法)作为我国自主研发的密码算法标准,逐渐在各个领域中得到 ...

  10. docker - 将几个目录复制到另一个目录

    您如何将多个目录复制到Docker中的目标目录?我不想复制目录内容,而是复制整个目录结构.COPY和ADD命令复制目录内容,展平结构,这是我不想要的.也就是说,如果这些是我的来源: . ├── a │ ...