SpringSecurity 微服务权限方案_微服务springsecurity_有点小奇怪的博客-程序员宝宝

技术标签: spring  java  SpringSecurity  mysql  

1.什么是微服务

1.1微服务由来:
微服务最早由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言实现,以及不同数据存储技术,并保持最低限度的集中式管理。

1.2微服务优势:
(1)微服务每个模块就相当于一个单独的项目,代码量明显减少,遇到问题也相对来说比较好解决。
(2)微服务每个模块都可以使用不同的存储方式(比如有的用 redis,有的用 mysql等),数据库也是单个模块对应自己的数据库。
(3)微服务每个模块都可以使用不同的开发技术,开发模式更灵活。

1.3微服务本质:
(1)微服务,关键其实不仅仅是微服务本身,而是系统要提供一套基础的架构,这种架构使得微服务可以独立的部署、运行、升级,不仅如此,这个系统架构还让微服务与微服务之间在结构上“松耦合”,而在功能上则表现为一个统一的整体。这种所谓的“统一的整体”表现出来的是统一风格的界面,统一的权限管理,统一的安全策略,统一的上线过程,统一的日志和审计方法,统一的调度方式,统一的访问入口等等。
(2)微服务的目的是有效的拆分应用,实现敏捷开发和部署。

2.微服务认证与授权实现思路

2.1认证授权过程分析:
(1)如果是基于 Session,那么 Spring-security 会对 cookie 里的 sessionid 进行解析,找到服务器存储的 session 信息,然后判断当前用户是否符合请求的要求。
(2)如果是 token,则是解析出 token,然后将当前请求加入到 Spring-security 管理的权限信息中去。

在这里插入图片描述
如果系统的模块众多,每个模块都需要进行授权与认证,所以我们选择基于 token 的形式进行授权与认证,用户根据用户名密码认证成功,然后获取当前用户角色的一系列权限值,并以用户名为 key,权限列表为value 的形式存入 redis 缓存中,根据用户名相关信息生成 token 返回,浏览器将 token 记录到 cookie 中,每次调用 api 接口都默认将 token 携带到 header 请求头中,Spring-security 解析 header 头获取 token 信息,解析 token 获取当前用户名,根据用户名就可以从 redis 中获取权限列表,这样 Spring-security 就能够判断当前请求是否有权限访问。

2.2权限管理数据模型:
在这里插入图片描述
2.3JWT介绍:
1.访问令牌的类型:
在这里插入图片描述
2.JWT组成:
典型的,一个 JWT 看起来如下图:
在这里插入图片描述
该对象为一个很长的字符串,字符之间通过"."分隔符分为三个子串。
每一个子串表示了一个功能块,总共有以下三个部分:JWT 头、有效载荷和签名。

JWT 头
JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{
    
	"alg": "HS256",
	"typ": "JWT"
}

在上面的代码中,alg 属性表示签名使用的算法,默认为 HMAC SHA256(写为 HS256);
typ 属性表示令牌的类型,JWT 令牌统一写为 JWT。最后,使用 Base64 URL 算法将上述
JSON 对象转换为字符串保存。

有效载荷
有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT指定七个默认字段供选择。
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID 用于标识该 JWT
除以上默认字段外,我们还可以自定义私有字段,如下例:

{
    
	"sub": "1234567890",
	"name": "Helen",
	"admin": true
}

请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息
字段,存放保密信息,以防止信息泄露。

JSON 对象也使用 Base64 URL 算法转换为字符串保存。

签名哈希
签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为 HMAC SHA256)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(claims), secret)在计算出签名哈希后,JWT 头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个 JWT 对象。

Base64URL 算法
如前所述,JWT 头和有效载荷序列化的算法都用到了 Base64URL。该算法和常见 Base64 算法类似,稍有差别。

作为令牌的 JWT 可以放在 URL 中(例如api.example/?token=xxx)。 Base64 中用的三个字符是"+","/“和”=",由于在 URL 中有特殊含义,因此Base64URL 中对他们做了替换:"=“去掉,”+“用”-“替换,”/“用”_"替换,这就是 Base64URL 算法。

2.4具体代码实现:
在这里插入图片描述
1 编写核心配置类
Spring Security 的核心配置就是继承WebSecurityConfigurerAdapter 并注解**@EnableWebSecurity** 的配置。这个配置指明了用户名密码的处理方式、请求路径、登录登出控制等和安全相关的配置;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class TokenWebSecurityConfig extends WebSecurityConfigurerAdapter {
    
	 //自定义查询数据库用户名密码和权限信息
	 private UserDetailsService userDetailsService;
	 //token 管理工具类(生成 token)
	 private TokenManager tokenManager;
	 //密码管理工具类
	 private DefaultPasswordEncoder defaultPasswordEncoder;
	 //redis 操作工具类
	 private RedisTemplate redisTemplate;
	 @Autowired
	 public TokenWebSecurityConfig(UserDetailsService userDetailsService, 
	 DefaultPasswordEncoder defaultPasswordEncoder,TokenManager tokenManager, RedisTemplate  redisTemplate) {
    
		 this.userDetailsService = userDetailsService;
		 this.defaultPasswordEncoder = defaultPasswordEncoder;
		 this.tokenManager = tokenManager;
		 this.redisTemplate = redisTemplate;
	 }
	 /**
	 * 配置设置
	 */
	 //设置退出的地址和 token,redis 操作地址
	 @Override
	 protected 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 
		 TokenLogoutHandler(tokenManager,redisTemplate)).and()
		 .addFilter(new TokenLoginFilter(authenticationManager(), 
		 tokenManager, redisTemplate))
		 .addFilter(new  TokenAuthenticationFilter(authenticationManager(), tokenManager,  redisTemplate)).httpBasic();
	 }
	 /**
	 * 密码处理
	 */
	 @Override
	 public void configure(AuthenticationManagerBuilder auth) throws Exception {
    
		auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
	 }
	 /**
	 * 配置哪些请求不拦截
	 */
	 @Override
	 public void configure(WebSecurity web) throws Exception {
    
		 web.ignoring().antMatchers("/api/**" , "/swagger-ui.html/**);
	 }
}

2.创建认证授权相关的工具类
在这里插入图片描述
(1)DefaultPasswordEncoder:密码处理的方法

@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
    
	 public DefaultPasswordEncoder() {
    
	 	this(-1);
	 }
	 /**
	 * @param strength 
	 * the log rounds to use, between 4 and 31
	 */
	 public DefaultPasswordEncoder(int strength) {
    
	 }
	 public String encode(CharSequence rawPassword) {
    
		 return MD5.encrypt(rawPassword.toString());
	 }
	 public boolean matches(CharSequence rawPassword, String encodedPassword) {
    
	 	 return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
	 }
}

(2)TokenManager:token 操作的工具类

@Component
public class TokenManager {
    
	 private long tokenExpiration = 24*60*60*1000;
	 private String tokenSignKey = "123456";
	 public String createToken(String username) {
    
		 String token = Jwts.builder().setSubject(username)
		 .setExpiration(new Date(System.currentTimeMillis() + tokenExpiration))
	 	 .signWith(SignatureAlgorithm.HS512, tokenSignKey).compressWith(CompressionCodecs.GZIP).compact();
	 	 return token;
	 }
	 public String getUserFromToken(String token) {
    
	 	String user = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody().getSubject();
	 	return user;
	 }
	 public void removeToken(String token) {
    
	 	//jwttoken 无需删除,客户端扔掉即可。
	 }
}

(3)TokenLogoutHandler:退出实现

public class TokenLogoutHandler implements LogoutHandler {
    
	 private TokenManager tokenManager;
	 private RedisTemplate redisTemplate;
	 public TokenLogoutHandler(TokenManager tokenManager, RedisTemplate redisTemplate) {
    
		 this.tokenManager = tokenManager;
		 this.redisTemplate = redisTemplate;
	 }
	 @Override
	 public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    
		 String token = request.getHeader("token");
		 if (token != null) {
    
			 tokenManager.removeToken(token);
			 //清空当前用户缓存中的权限数据
			 String userName = tokenManager.getUserFromToken(token);
			 redisTemplate.delete(userName);
		 }
		 ResponseUtil.out(response, R.ok());
	}
}

(4)UnauthorizedEntryPoint:未授权统一处理

public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
    
	 @Override
	 public void commence(HttpServletRequest request, HttpServletResponse response,AuthenticationException authException) throws IOException, ServletException {
    
	 	ResponseUtil.out(response, R.error());
	 }
}

2 创建认证授权实体类
在这里插入图片描述
(1) SecutityUser

@Data
@Slf4j
public class SecurityUser implements UserDetails {
    
	 //当前登录用户
	 private transient User currentUserInfo;
	 //当前权限
	 private List<String> permissionValueList;
	 public SecurityUser() {
    
	 }
	 public SecurityUser(User user) {
    
		 if (user != null) {
    
		 	this.currentUserInfo = user;
	 	}
	 }
	 @Override
	 public Collection<? extends GrantedAuthority> getAuthorities() {
    
	 Collection<GrantedAuthority> authorities = new ArrayList<>();
	 for(String permissionValue : permissionValueList) {
    
		 if(StringUtils.isEmpty(permissionValue)) continue;
		 SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
		 authorities.add(authority);
		 }
	 	return authorities;
	 }
	 @Override
	 public String getPassword() {
    
		 return currentUserInfo.getPassword();
	 }
	 @Override
	 public String getUsername() {
    
	 	return currentUserInfo.getUsername();
	 }
	 @Override
	 public boolean isAccountNonExpired() {
    
	 	return true;
	 }
	 @Override
	 public boolean isAccountNonLocked() {
    
		 return true;
	 }
	 @Override
	 public boolean isCredentialsNonExpired() {
    
		 return true;
	 }
	 @Override
	 public boolean isEnabled() {
    
	 	return true;
	 }
}

(2)User

@Data
@ApiModel(description = "用户实体类")
public class User implements Serializable {
    
	 private String username;
	 private String password;
	 private String nickName;
	 private String salt;
	 private String token;
}

3 创建认证和授权的 filter
在这里插入图片描述
(1)TokenLoginFilter:认证的 filter

public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
    
	 private AuthenticationManager authenticationManager;
	 private TokenManager tokenManager;
	 private RedisTemplate redisTemplate;
	 public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
    
		 this.authenticationManager = authenticationManager;
		 this.tokenManager = tokenManager;
		 this.redisTemplate = redisTemplate;
		 this.setPostOnly(false);
		 this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/acl/login","POST"));
	 }
	 @Override
	 public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException {
    
		 try {
    
			 User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
			 return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
		 } catch (IOException e) {
    
			 throw new RuntimeException(e);
		 }
	 }
	 /**
	 * 登录成功
	 */
	 @Override
	 protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain, Authentication auth) throws IOException, ServletException {
    
		 SecurityUser user = (SecurityUser) auth.getPrincipal();
		 String token;
		 tokenManager.createToken(user.getCurrentUserInfo().getUsername());
		
		 redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), 
		 user.getPermissionValueList());
		 ResponseUtil.out(res, R.ok().data("token", token));
	 }
	 /**
	 * 登录失败
	 */
	 @Override
	 protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws 
	 IOException, ServletException {
    
	 	ResponseUtil.out(response, R.error());
	 }
}

(2)TokenAuthenticationFilter:授权 filter

public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    
	 private TokenManager tokenManager;
	 private RedisTemplate redisTemplate;
	 public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
    
		 super(authManager);
		 this.tokenManager = tokenManager;
		 this.redisTemplate = redisTemplate;
	 }
	 @Override
	 protected void doFilterInternal(HttpServletRequest req,HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
    
			logger.info("================="+req.getRequestURI());
			 if(req.getRequestURI().indexOf("admin") == -1) {
    
			 	chain.doFilter(req, res);
				 return;
		 	}
		 	UsernamePasswordAuthenticationToken authentication = null;
		    try {
    
		 		authentication = getAuthentication(req);
		 	} catch (Exception e) {
    
		 		ResponseUtil.out(res, R.error());
		 	}
			 if (authentication != null) {
    
				SecurityContextHolder.getContext().setAuthentication(authentication);
			 } else {
    
		 		ResponseUtil.out(res, R.error());
		 	}
	 		chain.doFilter(req, res);
	 	}
	 private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
    
	 // token 置于 header 里
	 String token = request.getHeader("token");
	 if (token != null && !"".equals(token.trim())) {
    
	 	 String userName = tokenManager.getUserFromToken(token);
		 List<String> permissionValueList = (List<String>) 
		 redisTemplate.opsForValue().get(userName);
		 Collection<GrantedAuthority> authorities = new ArrayList<>();
	 	for(String permissionValue : permissionValueList) {
    
	 		if(StringUtils.isEmpty(permissionValue)) continue;
			 SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
			 authorities.add(authority);
		 }
	 	if (!StringUtils.isEmpty(userName)) {
    
		 return new UsernamePasswordAuthenticationToken(userName, token, authorities);
	 	}
	 	return null;
	 	}
	 	return null;
	 }
}

3.完整流程图

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.SpringSecurity 原理总结

1 SpringSecurity 的过滤器介绍
SpringSecurity 采用的是责任链的设计模式,它有一条很长的过滤器链。现在对这条过滤器链的 15 个过滤器进行说明:

(1) WebAsyncManagerIntegrationFilter:将 Security 上下文与SpringWeb 中用于处理异步请求映射的 WebAsyncManager 进行集成。
(2) SecurityContextPersistenceFilter:在每次请求处理之前将该请求相关的安全上下文信息加载到 SecurityContextHolder 中,然后在该次请求处理完成之后,将SecurityContextHolder 中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder 中的信息清除,例如在 Session 中维护一个用户的安全信息就是这个过滤器处理的。
(3) HeaderWriterFilter:用于将头信息加入响应中。
(4) CsrfFilter:用于处理跨站请求伪造。
(5)LogoutFilter:用于处理退出登录。
(6)UsernamePasswordAuthenticationFilter:用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自 /login 的请求。从表单中获取用户名和密码时,默认使用的表单 name 值为 username 和password,这两个值可以通过设置这个过滤器的usernameParameter 和passwordParameter 两个参数的值进行修改。
(7)DefaultLoginPageGeneratingFilter:如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。
(8)BasicAuthenticationFilter:检测和处理 http basic 认证。
(9)RequestCacheAwareFilter:用来处理请求的缓存。
(10)SecurityContextHolderAwareRequestFilter:主要是包装请求对象 request。
(11)AnonymousAuthenticationFilter:检测 SecurityContextHolder 中是否存在Authentication 对象,如果不存在为其提供一个匿名 Authentication。
(12)SessionManagementFilter:管理 session 的过滤器
(13)ExceptionTranslationFilter:处理 AccessDeniedException 和AuthenticationException 异常。
(14)FilterSecurityInterceptor:可以看做过滤器链的出口。
(15)RememberMeAuthenticationFilter:当用户没有登录而直接访问资源时, 从 cookie 里找出用户的信息, 如果 Spring Security 能够识别出用户提供的 remember me cookie, 用户将不必填写用户名和密码, 而是直接登录进入系统,该过滤器默认不开启。

2 SpringSecurity 基本流程
Spring Security 采取过滤链实现认证与授权,只有当前过滤器通过,才能进入下一个过滤器:
在这里插入图片描述
绿色部分是认证过滤器,需要我们自己配置,可以配置多个认证过滤器。认证过滤器可以使用 Spring Security 提供的认证过滤器,也可以自定义过滤器(例如:短信验证)。认证过滤器要在 configure(HttpSecurity http)方法中配置,没有配置不生效。下面会重点介绍以下三个过滤器:

UsernamePasswordAuthenticationFilter 过滤器:该过滤器会拦截前端提交的 POST 方式的登录表单请求,并进行身份认证。

ExceptionTranslationFilter 过滤器:该过滤器不需要我们配置,对于前端提交的请求会直接放行,捕获后续抛出的异常并进行处理(例如:权限访问限制)。

FilterSecurityInterceptor 过滤器:该过滤器是过滤器链的最后一个过滤器,根据资源权限配置来判断当前请求是否有权限访问对应的资源。如果访问受限会抛出相关异常,并由 ExceptionTranslationFilter 过滤器进行捕获和处理。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_45834459/article/details/121429406

智能推荐

jpa无主键表查询结果重复(复合主键的使用)_表没有主键 条件查询时 返回重复_qq_40059532的博客-程序员宝宝

jpa要求数据表映射的实体类必须有id,但是实际的表中往往是没有主键的,如果在实体中随便给某个属性添加@id注解,那么在使用查询时得到的结果往往是相同的,比如下面的例子:数据表:表中videoID不是主键(在其他表中它是主键)表映射的实体:因为实体必须要有@id,所以我把注解添加到videoID使用jpa 提供的方法查询所有数据查询结果:videoID相同的查询结果都相同,也就是所有...

java学习第二章-数据类型和运算符(仅做个人学习笔记用)_Nlsrr的博客-程序员宝宝

2.6.4 boolean类型变量/常量boolean类型有两个常量值,true和false,在内存中占一位(不是一个字节),不可以使用 0 或非 0 的整数替代 true 和 false ,这点和C语言不同。 boolean 类型用来判断逻辑条件,一般用于程序流程控制 。...

Uncaught ReferenceError: XXX is not defined_哈哈我是大健的博客-程序员宝宝

在写javascript函数的时候,JavaScript写在哪里?通常有三种形式: 1. 内部:Html网页的&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;中; 2. 内部:Html网页的&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;中; 3. 外部:外部JS文件里。 (1) 写在&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;中  当浏览器载入网页Body部分的时候,就执行其中的Jav...

移动端蚂蚁组件(antd-mobile)- 解决日期组件中的语系问题_chp-22的博客-程序员宝宝

目标:期望:我们项目需要有繁简体两种语系,对于一般的选择器是有可配置文案的属性的,但是日期选择器却没有。通过研究还是发现了这个属性是可以配置语系问题的。试过之后发现竟然就是没有我需要的繁体(zh_TW)。无奈之下,只能研究看这些语系是怎么写的,我仿照写个繁体的语系文件放到包里,但很快这个想法被我pass了,一个是因为语系并不是直接写在这些文件里的,另一个因为我们项目较多,人员也多,万一下次不是我改,别人可能就不知道到底怎样修改。于是又产生了另一个想法,包不能改,那我就在文

git上传代码到git/码云gitee_上传gitee_qinyong^_^的博客-程序员宝宝

一、首先需要下载githttps://git-scm.com/输入命令:git --version 可查看当前git版本二.安装后需要一些配置 配置用户名和邮箱:$ git config --global user.name &quot;qinyong&quot; $ git config --global user.email &quot;[email protected]&quot; qinyong是...

修改docker.daemon文件后启动失败_ITROOKIEIS的博客-程序员宝宝

前言在 docker 的官方文档中, 介绍了三种访问 docker Remote API 的方式, 分别是 unix 套接字文件/ tcp 监听端口和 fd 文件描述符. 由于 docker 默认会为我们开启本地 socket 套接字(/var/run/docker.sock) 所以本篇文章主要介绍如何让 docker 监听 tcp端口在 docker 配置文件中设置docker 1.12...

随便推点

13 A_咸鸭蛋黄月饼的博客-程序员宝宝

题目描述有n个互不相同的整数,存储在数组中。在这n个整数中查找m个整数,如果存在,则打印出与之相邻的整数;否则就显示无此数。输入与输出要求:首先输入一个整数n以及n个整数(n的范围是1—1000),然后输入整数m,代表查询次数(m的范围是1—100),接着输入m个待查询的数。输出m行查询的结果,每次的查询结果中可能有一个数、两个数或没有数。若没有数则输出“NULL”。要求:实现如下函数完成一...

postman-enterprise-API_postman企业版_sdk大全的博客-程序员宝宝

Postman API 平台为团队提供高生产力,为开发人员提供高质量的 API,并为全球组织提供无懈可击的治理

React路由设置_super苏然的博客-程序员宝宝

路由的意思就是分页,实现不同页面的跳转。在React框架下需要先安装对应的依赖库:npm install --save-dev react-router-dom然后创建项目,我用的是WebStorm开发工具。我将App.js改为了router.js,导入包这里需要说明一下,HashRouter和BrowserRouter可以互换使用,区别在于HashRouter在浏览器页面显示时会出现#号,如图,而BrowserRouter则不会,如图。下面导入了3个页面,分别是IndexPage,NewsPa

超级好用论文写作工具NoteExpress下载和安装_纯粹.的博客-程序员宝宝

(1)点击下方的NoteExpress链接下载NoteExpress链接提取码:tn7w(2)用压缩软件双击解压瑞所示:(3)点击下一步,如图所示:(4)接下来就是一步步,按部就班的安装

DDS QoS - RELIABILITY_dds qos 可靠性_程序员之一的博客-程序员宝宝

https://download.csdn.net/download/eidolon_foot/1256876814. RELIABILITYOpenDDS Developer's Guide(OpenDDS Version 3.14)The RELIABILITY policy applies to the topic, data reader, and data writer entities via the reliability member of their respec..

丑数(第n个丑数)_皮蛋囍卤蛋的博客-程序员宝宝

题目描述把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。输入输出示例输入:7返回值:8解题思路丑数:1, 2, 3, 4, 5, 6, 8, 9, 10…可发现规律:每个丑数都可以由其他两个丑数相乘而得到。12=2;13=3;15=5;22=4;23=6;24=222=8;33=9;25=10代码示例# -*- coding:utf-8 -*-clas

推荐文章

热门文章

相关标签