通过认证服务进行统一认证,然后通过网关来统一校验认证和鉴权。
将采用 Nacos 作为注册中心,Gateway 作为网关,使用
nimbus-jose-jwtJWT 库操作 JWT 令牌
Spring Security 是强大的且容易定制的,基于 Spring 开发的实现认证登录与资源授权的应用安全框架
SpringSecurity 的核心功能:
configure(HttpSecurity httpSecurity)
用于配置需要拦截的 url 路径、jwt 过滤器及出异常后的处理器
configure(AuthenticationManagerBuilder auth)
用于配置 UserDetailsService 及 PasswordEncoder
RestfulAccessDeniedHandler
当用户没有访问权限时的处理器,用于返回 JSON 格式的处理结果
RestAuthenticationEntryPoint
当未登录或 token 失效时,返回 JSON 格式的结果
UserDetailsService
SpringSecurity 定义的核心接口,用于根据用户名获取用户信息,需要自行实现
UserDetails
SpringSecurity 定义用于封装用户信息的类(主要是用户信息和权限),需要自行实现
PasswordEncoder
SpringSecurity 定义的用于对密码进行编码及比对的接口,目前使用的是 BCryptPasswordEncoder
JwtAuthenticationTokenFilter
在用户名和密码校验前添加的过滤器,如果有 jwt 的 token,会自行根据 token 信息进行登录
用户在应用 A 上登录认证,应用 A 会颁发给他一个 JWT 令牌(一个包含若干用户状态信息的字符串)。当用户访问应用 B 接口的时候,将这个字符串交给应用 B,应用 B 根据 Token 中的内容进行鉴权。不同的应用之间按照统一标准发放 JWT令牌,统一标准验证 JWT 令牌。从而你在应用 A 上获得的令牌,在应用 B 上也被认可,当然这样这些应用之间底层数据库必须是同一套用户、角色、权限数据。

JWT 存在的问题
说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:
续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了
注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便
密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret
基于第2点和第3点,一般建议不同用户取不同secret
以上的所有的单点集群登陆方案,都是有一个前提就是:应用A、应用B、应用1、应用2、应用3都是你们公司的,你们公司内部应用之间进行单点登陆验证。
但是大家都见过这样一个场景:登录某一个网站,然后使用的是在QQ、微信上保存的用户数据。也就是说第三方应用想使用某个权威平台的用户数据做登录认证,那么这个权威平台该如何对第三方应用提供认证服务?目前比较通用的做法就是OAuth2(现代化的社交媒体网站登录基本都使用OAuth2)
在说明OAuth2需求及使用场景之前,需要先介绍一下OAuth2授权流程中的各种角色:

密码模式与授权码模式最大的区别在于:
因为 Spring Security OAuth “认证服务器”支持多种认证模式,所以不想抛弃它。但是想把最后的"资源访问令牌",由 AccessToken 换成 JWT 令牌。因为 AccessToken 不带有任何的附加信息,就是一个字符串,JWT 是可以携带附加信息的。

理想的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他 API 服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑
相关服务划分:

下面介绍下这套解决方案的具体实现,依次搭建认证服务、网关服务和 API 服务
首先来搭建认证服务,它将作为 Oauth2 的认证服务使用,并且网关服务的鉴权功能也需要依赖它
pom.xml 中添加相关依赖,主要是 Spring Security、Oauth2、JWT、Redis 相关依赖<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-rsa</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
</dependencies>
application.yml 中添加相关配置,主要是 Nacos 和 Redis 相关配置server:
port: 9401
spring:
profiles:
active: dev
application:
name: security-oauth2-auth
cloud:
nacos:
discovery:
server-addr: 192.168.123.22:8848
username: nacos
password: nacos
redis:
port: 6379
host: localhost
password: xxx
management:
endpoints:
web:
exposure:
include: "*"
keytool 生成 RSA 证书 jwt.jks,复制到 resource 目录下,在 JDK 的 bin 目录下使用如下命令即可keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
UserServiceImpl 类实现 Spring Security 的 UserDetailsService 接口,用于加载用户信息/**
* 用户管理业务类
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService
{
@Autowired
private UmsAdminService adminService;
@Autowired
private HttpServletRequest request;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
String clientId = request.getParameter("client_id");
UserDto userDto = null;
if (AuthConstant.ADMIN_CLIENT_ID.equals(clientId))
{
userDto = adminService.loadUserByUsername(username);
}
if (null == userDto)
{
throw new UsernameNotFoundException(EC.ERROR_USER_PASSWORD_INCORRECT.getMsg());
}
SecurityUserDetails securityUser = new SecurityUserDetails(userDto);
if (!securityUser.isEnabled())
{
throw new DisabledException(EC.ERROR_USER_ENABLED.getMsg());
}
else if (!securityUser.isAccountNonLocked())
{
throw new LockedException(EC.ERROR_USER_LOCKED.getMsg());
}
else if (!securityUser.isAccountNonExpired())
{
throw new AccountExpiredException(EC.ERROR_USER_EXPIRE.getMsg());
}
else if (!securityUser.isCredentialsNonExpired())
{
throw new CredentialsExpiredException(EC.ERROR_USER_UNAUTHORIZED.getMsg());
}
return securityUser;
}
}
@Component
public class UmsAdminService
{
@Autowired
private PasswordEncoder passwordEncoder;
public UserDto loadUserByUsername(String username)
{
String password = passwordEncoder.encode("123456a");
if("admin".equals(username))
{
return new UserDto("admin", password, 1, "", CollUtil.toList("ADMIN"));
}
else if("langya".equals(username))
{
return new UserDto("langya", password, 1, "", CollUtil.toList("ADMIN", "TEST"));
}
return null;
}
}
Oauth2ServerConfig,需要配置加载用户信息的服务 UserServiceImpl 及 RSA 的钥匙对KeyPair/**
* 认证服务器配置
*/
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter
{
private final PasswordEncoder passwordEncoder;
private final UserDetailsServiceImpl userDetailsService;
private final AuthenticationManager authenticationManager;
private final JwtTokenEnhancer jwtTokenEnhancer;
/**
* 客户端信息配置
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
{
clients.inMemory()
.withClient(AuthConstant.ADMIN_CLIENT_ID)
.secret(passwordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600 * 24)
.refreshTokenValiditySeconds(3600 * 24 * 7)
.and()
.withClient(AuthConstant.PORTAL_CLIENT_ID)
.secret(passwordEncoder.encode("123456"))
.scopes("all")
.authorizedGrantTypes("password", "refresh_token")
.accessTokenValiditySeconds(3600 * 24)
.refreshTokenValiditySeconds(3600 * 24 * 7);
}
/**
* 配置授权(authorization)以及令牌(token)
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
{
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(accessTokenConverter());
//配置JWT的内容增强器
enhancerChain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManager)
//配置加载用户信息的服务
.userDetailsService(userDetailsService)
.accessTokenConverter(accessTokenConverter())
.tokenEnhancer(enhancerChain);
}
/**
* 允许表单认证
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception
{
security.allowFormAuthenticationForClients();
}
/**
* 使用非对称加密算法对token签名
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter()
{
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
//or 设置对称签名
//jwtAccessTokenConverter.setSigningKey("2430B31859314947BC84697E70B3D31F");
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
/**
* 从classpath下的密钥库中获取密钥对(公钥+私钥)
*/
@Bean
public KeyPair keyPair()
{
//从classpath下的证书中获取秘钥对
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"),
"123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
}
}
登录用户的ID,可以自己实现 TokenEnhancer 接口/**
* JWT 内容增强器
*/
@Component
public class JwtTokenEnhancer implements TokenEnhancer
{
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication)
{
SecurityUserDetails securityUser = (SecurityUserDetails) authentication.getPrincipal();
//把用户名设置到JWT中
Map<String, Object> info = new HashMap<>();
info.put("user_name", securityUser.getUsername());
info.put("client_id", securityUser.getClientId());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
/**
* 获取RSA公钥接口
*/
@RestController
public class KeyPairController
{
@Autowired
private KeyPair keyPair;
@GetMapping("/rsa/publicKey")
public Map<String, Object> getKey()
{
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}
}
/**
* SpringSecurity 安全配置
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception
{
http.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers("/rsa/publicKey").permitAll()
.anyRequest().authenticated();
}
/**
* 如果不配置 SpringBoot 会自动配置一个 AuthenticationManager 覆盖掉内存中的用户
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception
{
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
}
}
ResourceServiceImpl,初始化的时候把资源与角色匹配关系缓存到 Redis 中,方便网关服务进行鉴权的时候获取/**
* 资源与角色匹配关系管理业务类
* <p>
* 初始化的时候把资源与角色匹配关系缓存到Redis中,方便网关服务进行鉴权的时候获取
*/
@Service
public class ResourceServiceImpl
{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private Map<String, List<String>> resourceRolesMap;
@PostConstruct
public void initData()
{
resourceRolesMap = new TreeMap<>();
resourceRolesMap.put("/admin/hello", CollUtil.toList("ADMIN"));
resourceRolesMap.put("/admin/user/currentUser", CollUtil.toList("ADMIN", "TEST"));
redisTemplate.opsForHash().putAll(AuthConstant.RESOURCE_ROLES_MAP_KEY, resourceRolesMap);
}
}
如果资源权限存储到数据库,也可以直接使用 SQL 语句形成结果集,如:

接下来就可以搭建网关服务了,它将作为 Oauth2 的资源服务、客户端服务使用,对访问微服务的请求进行统一的校验认证和鉴权操作
pom.xml 中添加相关依赖,主要是 Gateway、Oauth2 和 JWT 相关依赖<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--lb:// need-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
</dependency>
</dependencies>
application.yml 中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置server:
port: 9201
spring:
main:
#springcloudgateway 的内部是通过 netty+webflux 实现的
#webflux 实现和 spring-boot-starter-web 依赖冲突
web-application-type: reactive
profiles:
active: dev
application:
name: security-oauth2-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
username: nacos
password: nacos
gateway:
routes: #配置路由路径
- id: oauth2-api-route
uri: lb://security-oauth2-api
predicates:
- Path=/admin/**
filters:
- StripPrefix=1
- id: oauth2-auth-route
uri: lb://security-oauth2-auth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1
discovery:
locator:
#开启从注册中心动态创建路由的功能
enabled: true
#使用小写服务名,默认是大写
lower-case-service-id: true
security:
oauth2:
resourceserver:
jwt:
#配置RSA的公钥访问地址
jwk-set-uri: 'http://localhost:9401/rsa/publicKey'
redis:
host: 192.168.123.22
port: 6379
password: Hacfin_Redis8
timeout: 6000ms
secure:
ignore:
#配置白名单路径
urls:
- "/actuator/**"
- "/auth/oauth/token"
WebFlux,所以需要使用 @EnableWebFluxSecurity注解开启/**
* 资源服务器配置
*/
@AllArgsConstructor
@Configuration
@EnableWebFluxSecurity
public class ResourceServerConfig
{
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
{
http.oauth2ResourceServer().jwt()
.jwtAuthenticationConverter(jwtAuthenticationConverter());
//自定义处理JWT请求头过期或签名错误的结果
http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
//对白名单路径,直接移除JWT请求头
http.addFilterBefore(ignoreUrlsRemoveJwtFilter, SecurityWebFiltersOrder.AUTHENTICATION);
http.authorizeExchange()
//白名单配置
.pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(), String.class)).permitAll()
//鉴权管理器配置
.anyExchange().access(authorizationManager)
.and()
.exceptionHandling()
//处理未授权
.accessDeniedHandler(restfulAccessDeniedHandler)
//处理未认证
.authenticationEntryPoint(restAuthenticationEntryPoint)
.and()
.csrf().disable();
return http.build();
}
/**
* @linkhttps://blog.csdn.net/qq_24230139/article/details/105091273
* ServerHttpSecurity 没有将 jwt 中 authorities 的负载部分当做 Authentication
* 需要把 jwt 的 Claim 中的 authorities 加入
* 方案:重新定义 ReactiveAuthenticationManager 权限管理器,默认转换器 JwtGrantedAuthoritiesConverter
*/
@Bean
public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter()
{
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
}
}
WebFluxSecurity 中自定义鉴权操作需要实现 ReactiveAuthorizationManager 接口@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
//从Redis中获取当前路径可访问角色列表
URI uri = authorizationContext.getExchange().getRequest().getURI();
Object obj = redisTemplate.opsForHash().get(RedisConstant.RESOURCE_ROLES_MAP, uri.getPath());
List<String> authorities = Convert.toList(String.class,obj);
authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
//认证通过且角色匹配的用户可访问当前路径
return mono
.filter(Authentication::isAuthenticated)
.flatMapIterable(Authentication::getAuthorities)
.map(GrantedAuthority::getAuthority)
.any(authorities::contains)
.map(AuthorizationDecision::new)
.defaultIfEmpty(new AuthorizationDecision(false));
}
}
AuthGlobalFilter,当鉴权通过后将 JWT 令牌中的用户信息解析出来,然后存入请求的 Header 中,这样后续服务就不需要解析 JWT 令牌了,可以直接从请求的 Header 中获取到用户信息/**
* 将登录用户的JWT转化成用户信息的全局过滤器
*/
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered
{
@Autowired
private RedisTemplate redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
{
//认证信息从Header 或 请求参数 中获取
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String token = serverHttpRequest.getHeaders().getFirst(AuthConstant.JWT_TOKEN_HEADER);
if (Objects.isNull(token))
{
token = serverHttpRequest.getQueryParams().getFirst(AuthConstant.JWT_TOKEN_HEADER);
}
if (StrUtil.isEmpty(token))
{
return chain.filter(exchange);
}
try
{
//从token中解析用户信息并设置到Header中去
String realToken = token.replace(AuthConstant.JWT_TOKEN_PREFIX, "");
JWSObject jwsObject = JWSObject.parse(realToken);
String userStr = jwsObject.getPayload().toString();
// 黑名单token(登出、修改密码)校验
JSONObject jsonObject = JSONUtil.parseObj(userStr);
String jti = jsonObject.getStr("jti");
Boolean isBlack = redisTemplate.hasKey(AuthConstant.TOKEN_BLACKLIST_PREFIX + jti);
if (isBlack)
{
}
// 存在token且不是黑名单,request写入JWT的载体信息
ServerHttpRequest request = serverHttpRequest.mutate().header(AuthConstant.USER_TOKEN_HEADER, userStr).build();
exchange = exchange.mutate().request(request).build();
}
catch (ParseException e)
{
e.printStackTrace();
}
return chain.filter(exchange);
}
@Override
public int getOrder()
{
return 0;
}
}
最后搭建一个API服务,它不会集成和实现任何安全相关逻辑,全靠网关来保护它
pom.xml中添加相关依赖,就添加了一个web依赖<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
LoginUserHolder 组件,用于从请求的 Header 中直接获取登录用户信息/**
* 获取登录用户信息
*/
@Component
public class LoginUserHolder
{
public UserDto getCurrentUser(HttpServletRequest request)
{
String userStr = request.getHeader(AuthConstant.USER_TOKEN_HEADER);
JSONObject userJsonObject = new JSONObject(userStr);
UserDto userDTO = new UserDto();
userDTO.setUserName(userJsonObject.getStr("user_name"));
userDTO.setClientId(userJsonObject.getStr("client_id"));
userDTO.setRoles(Convert.toList(String.class, userJsonObject.get(AuthConstant.AUTHORITY_CLAIM_NAME)));
return userDTO;
}
}
@RestController
@RequestMapping("/user")
public class UserController{
@Autowired
private LoginUserHolder loginUserHolder;
@GetMapping("/currentUser")
public UserDTO currentUser() {
return loginUserHolder.getCurrentUser();
}
}
接下来来演示下微服务系统中的统一认证鉴权功能,所有请求均通过网关访问
启动 Nacos 和 Redis 服务
启动 security-oauth2-auth、security-oauth2-gateway 及 security-oauth2-api 服务




Spring Cloud Gateway + Oauth2 实现统一认证和鉴权
Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权鉴权
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
这里是Ruby新手。完成一些练习后碰壁了。练习:计算一系列成绩的字母等级创建一个方法get_grade来接受测试分数数组。数组中的每个分数应介于0和100之间,其中100是最大分数。计算平均分并将字母等级作为字符串返回,即“A”、“B”、“C”、“D”、“E”或“F”。我一直返回错误:avg.rb:1:syntaxerror,unexpectedtLBRACK,expecting')'defget_grade([100,90,80])^avg.rb:1:syntaxerror,unexpected')',expecting$end这是我目前所拥有的。我想坚持使用下面的方法或.join,
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList()Obt
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO
基础版云数据库RDS的产品系列包括基础版、高可用版、集群版、三节点企业版,本文介绍基础版实例的相关信息。RDS基础版实例也称为单机版实例,只有单个数据库节点,计算与存储分离,性价比超高。说明RDS基础版实例只有一个数据库节点,没有备节点作为热备份,因此当该节点意外宕机或者执行重启实例、变更配置、版本升级等任务时,会出现较长时间的不可用。如果业务对数据库的可用性要求较高,不建议使用基础版实例,可选择其他系列(如高可用版),部分基础版实例也支持升级为高可用版。基础版与高可用版的对比拓扑图如下所示。优势 性能由于不提供备节点,主节点不会因为实时的数据库复制而产生额外的性能开销,因此基础版的性能相对于