百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Cloud Gateway + Security OAuth2 + JWT实现微服务统一认证授权鉴权

myzbx 2025-03-28 19:10 8 浏览

一. 前言

二. OAuth2和JWT的关系

1. 什么是OAuth2?

OAUth2就是一套广泛流行的认证授权协议,大白话说呢OAuth2这套协议中有两个核心的角色,认证服务器和资源服务器。

两个角色和 youlai-mall 模块对应关系如下:

模块名称

youlai-mall模块

OAuth2角色

服务地址

认证中心

youlai-auth

认证服务器

localhost:8000

网关

youlai-gateway

资源服务器

localhost:9999

用户不能直接去访问资源服务器(网关),必须先到认证服务器认证,通过后颁发一个token令牌给你,你只有拿着token访问资源服务器才能通过,令牌token是有时间限制的,到时间了就无效。

这个模式相信经常到甲方爸爸的地方做驻场的小伙伴深有体会,一般人家可不会给你一个正式员工工牌,要么拿身份证抵押换个临时访问牌,隔天就失效,这样人家才有安全感嘛~

其中网关为什么能作为“资源服务器”呢? 网关是作为各个微服务(会员服务、商品服务、订单服务等)统一入口,也就是这些资源服务的统一门面,在这里可以对JWT验签、JWT有效期判断、JWT携带角色权限判断。

2. 什么是JWT?

JWT(JSON Web Token)它没啥悬乎的,就是一个特殊的token,最大的特性就是无状态,因为它本身可以携带用户的信息(用户ID、用户名、用户的角色集合等),我们先看下一个解析过后的JWT是什么样子的。

JWT字符串由Header(头部)、Payload(负载)、Signature(签名)三部分组成。

复制代码12345LANGUAGE-MAKEFILEHeader: JSON对象,用来描述JWT的元数据,alg属性表示签名的算法,typ标识token的类型

Payload: JSON对象,重要部分,除了默认的字段,还可以扩展自定义字段,比如用户ID、姓名、角色等等

Signature: 对Header、Payload这两部分进行签名,认证服务器使用私钥签名,然后在资源服务器使用公钥验签,防止数据被人动了手脚

JWT和传统的Cookie/Session会话管理相比较有着多方面的优势,因为Cookie/Session需要在服务器Session存用户信息,然后拿客户端Cookie存储的SessionId获取用户信息,这个过程需要消耗服务器的内存和对客户端的要求比较严格(需支持Cookie),而JWT最大的特性在于就是无状态、去中心化,所以JWT更适用分布式的场景,不需要在多台服务器做会话同步这种消耗服务器性能的操作。

另外JWT和Redis+Token这两种会话管理小伙伴们看项目情况选择,别有用了JWT还使用Redis存储的,因为你这种做法对JWT来说就是“伤害不大,但侮辱性极强”的做法,就当着它的面说我就看不上你的最自以为是的“无状态”特性。

3. OAuth2和JWT关系?

  • OAuth2是一种认证授权的协议规范。
  • JWT是基于token的安全认证协议的实现。

OAuth2的认证服务器签发的token可以使用JWT实现,JWT轻量且安全。

三. 认证服务器

认证服务器落地 youlai-mall 的youlai-auth认证中心模块,完整代码地址: Gitee| Github

1. pom依赖

复制代码123456789LANGUAGE-XML    
        org.springframework.cloud
        spring-cloud-starter-oauth2
    

    
        org.springframework.security
        spring-security-oauth2-jose
    

2. 认证服务配置(AuthorizationServerConfig)

复制代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990JAVA/**
 * 认证服务配置
 */
@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private DataSource dataSource;
    private AuthenticationManager authenticationManager;
    private UserDetailsServiceImpl userDetailsService;

    /**
     * 客户端信息配置
     */
    @Override
    @SneakyThrows
    public void configure(ClientDetailsServiceConfigurer clients) {
        JdbcClientDetailsServiceImpl jdbcClientDetailsService = new JdbcClientDetailsServiceImpl(dataSource);
        jdbcClientDetailsService.setFindClientDetailsSql(AuthConstants.FIND_CLIENT_DETAILS_SQL);
        jdbcClientDetailsService.setSelectClientDetailsSql(AuthConstants.SELECT_CLIENT_DETAILS_SQL);
        clients.withClientDetails(jdbcClientDetailsService);
    }

    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        List tokenEnhancers = new ArrayList<>();
        tokenEnhancers.add(tokenEnhancer());
        tokenEnhancers.add(jwtAccessTokenConverter());
        tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);

        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(jwtAccessTokenConverter())
                .tokenEnhancer(tokenEnhancerChain)
                .userDetailsService(userDetailsService)
                // refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
                //      1.重复使用:access_token过期刷新时, refresh token过期时间未改变,仍以初次生成的时间为准
                //      2.非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新而无需失效再次登录
                .reuseRefreshTokens(false);
    }

    /**
     * 允许表单认证
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.allowFormAuthenticationForClients();
    }

    /**
     * 使用非对称加密算法对token签名
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * 从classpath下的密钥库中获取密钥对(公钥+私钥)
     */
    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory factory = new KeyStoreKeyFactory(
                new ClassPathResource("youlai.jks"), "123456".toCharArray());
        KeyPair keyPair = factory.getKeyPair(
                "youlai", "123456".toCharArray());
        return keyPair;
    }

    /**
     * JWT内容增强
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (accessToken, authentication) -> {
            Map map = new HashMap<>(2);
            User user = (User) authentication.getUserAuthentication().getPrincipal();
            map.put(AuthConstants.JWT_USER_ID_KEY, user.getId());
            map.put(AuthConstants.JWT_CLIENT_ID_KEY, user.getClientId());
            ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
            return accessToken;
        };
    }
}

AuthorizationServerConfig这个配置类是整个认证服务实现的核心。总结下来就是两个关键点,客户端信息配置和access_token生成配置。

2.1 客户端信息配置

配置OAuth2认证允许接入的客户端的信息,因为接入OAuth2认证服务器首先人家得认可你这个客户端吧,就比如上面案例中的QQ的OAuth2认证服务器认可“有道云笔记”客户端。

同理,我们需要把客户端信息配置在认证服务器上来表示认证服务器所认可的客户端。一般可配置在认证服务器的内存中,但是这样很不方便管理扩展。所以实际最好配置在数据库中的,提供可视化界面对其进行管理,方便以后像PC端、APP端、小程序端等多端灵活接入。

Spring Security OAuth2官方提供的客户端信息表oauth_client_details

复制代码1234567891011121314SQLCREATE TABLE `oauth_client_details`  (
  `client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

添加一条客户端信息

复制代码1SQLINSERT INTO `oauth_client_details` VALUES ('client', NULL, '123456', 'all', 'password,refresh_token', '', NULL, NULL, NULL, NULL, NULL);

2.2 token生成配置

项目使用JWT实现access_token,关于access_token生成步骤的配置如下:

1. 生成密钥库

使用JDK工具的keytool生成JKS密钥库(Java Key Store),并将youlai.jks放到resources目录

keytool -genkey -alias youlai -keyalg RSA -keypass 123456 -keystore youlai.jks -storepass 123456

复制代码1234567891011LANGUAGE-DIFF-genkey 生成密钥

-alias 别名

-keyalg 密钥算法

-keypass 密钥口令

-keystore 生成密钥库的存储路径和名称

-storepass 密钥库口令

2. JWT内容增强

JWT负载信息默认是固定的,如果想自定义添加一些额外信息,需要实现TokenEnhancer的enhance方法将附加信息添加到access_token中。

3. JWT签名

JwtAccessTokenConverter是生成token的转换器,可以实现指定token的生成方式(JWT)和对JWT进行签名。

签名实际上是生成一段标识(JWT的Signature部分)作为接收方验证信息是否被篡改的依据。原理部分请参考这篇的文章:RSA加密、解密、签名、验签的原理及方法

其中对JWT签名有对称和非对称两种方式:

对称方式:认证服务器和资源服务器使用同一个密钥进行加签和验签 ,默认算法HMAC

非对称方式:认证服务器使用私钥加签,资源服务器使用公钥验签,默认算法RSA

非对称方式相较于对称方式更为安全,因为私钥只有认证服务器知道。

项目中使用RSA非对称签名方式,具体实现步骤如下:

复制代码123LANGUAGE-SCSS(1). 从密钥库获取密钥对(密钥+私钥)
(2). 认证服务器私钥对token签名
(3). 提供公钥获取接口供资源服务器验签使用

公钥获取接口

复制代码1234567891011121314151617LANGUAGE-TYPESCRIPT/**
 * RSA公钥开放接口
 */
@RestController
@AllArgsConstructor
public class PublicKeyController {

    private KeyPair keyPair;

    @GetMapping("/getPublicKey")
    public Map getPublicKey() {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAKey key = new RSAKey.Builder(publicKey).build();
        return new JWKSet(key).toJSONObject();
    }

}

3. 安全配置(WebSecurityConfig)

复制代码123456789101112131415161718192021222324252627LANGUAGE-SCSS@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
         http
            .authorizeRequests().requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
        .and()
            .authorizeRequests().antMatchers("/getPublicKey").permitAll().anyRequest().authenticated()
        .and()
            .csrf().disable();
    }

    /**
     *  如果不配置SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public PasswordEncoder passwordEncoder()  {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

安全配置主要是配置请求访问权限、定义认证管理器、密码加密配置。

四. 资源服务器

资源服务器落地 youlai-mall 的youlai-gateway微服务网关模块,完整代码地址: Gitee | Github |

上文有提到过网关这里是担任资源服务器的角色,因为网关是微服务资源访问的统一入口,所以在这里做资源访问的统一鉴权是再合适不过。

1. pom依赖

复制代码12345678XML    
        org.springframework.security
        spring-security-oauth2-resource-server
    
    
        org.springframework.security
        spring-security-oauth2-jose
    

2. 配置文件(youlai-gateway.yaml)

复制代码123456789101112131415161718192021222324252627282930313233343536YAMLspring:
  security:
    oauth2:
      resourceserver:
        jwt:
          # 获取JWT验签公钥请求路径
          jwk-set-uri: 'http://localhost:8000/getPublicKey'
  redis:
    database: 0
    host: localhost
    port: 6379
    password:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 启用服务发现
          lower-case-service-id: true
      routes:
        - id: youlai-auth
          uri: lb://youlai-auth
          predicates:
            - Path=/youlai-auth/**
          filters:
            - StripPrefix=1
        - id: youlai-admin
          uri: lb://youlai-admin
          predicates:
            - Path=/youlai-admin/**
          filters:
            - StripPrefix=1

# 配置白名单路径
white-list:
    urls:
      - "/youlai-auth/oauth/token"

3. 鉴权管理器

鉴权管理器是作为资源服务器验证是否有权访问资源的裁决者,核心部分的功能先已通过注释形式进行说明,后面再对具体形式补充。

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556JAVA/**
 * 鉴权管理器
 */
@Component
@AllArgsConstructor
@Slf4j
public class AuthorizationManager implements ReactiveAuthorizationManager {

    private RedisTemplate redisTemplate;
    private WhiteListConfig whiteListConfig;

    @Override
    public Mono check(Mono mono, AuthorizationContext authorizationContext) {
        ServerHttpRequest request = authorizationContext.getExchange().getRequest();
        String path = request.getURI().getPath();
        PathMatcher pathMatcher = new AntPathMatcher();
        
        // 1. 对应跨域的预检请求直接放行
        if (request.getMethod() == HttpMethod.OPTIONS) {
            return Mono.just(new AuthorizationDecision(true));
        }

        // 2. token为空拒绝访问
        String token = request.getHeaders().getFirst(AuthConstants.JWT_TOKEN_HEADER);
        if (StrUtil.isBlank(token)) {
            return Mono.just(new AuthorizationDecision(false));
        }

        // 3.缓存取资源权限角色关系列表
        Map resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstants.RESOURCE_ROLES_KEY);
        Iterator<Object> iterator = resourceRolesMap.keySet().iterator();

        // 4.请求路径匹配到的资源需要的角色权限集合authorities
        List authorities = new ArrayList<>();
        while (iterator.hasNext()) {
            String pattern = (String) iterator.next();
            if (pathMatcher.match(pattern, path)) {
                authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
            }
        }
        Mono authorizationDecisionMono = mono
                .filter(Authentication::isAuthenticated)
                .flatMapIterable(Authentication::getAuthorities)
                .map(GrantedAuthority::getAuthority)
                .any(roleId -> {
                    // 5. roleId是请求用户的角色(格式:ROLE_{roleId}),authorities是请求资源所需要角色的集合
                    log.info("访问路径:{}", path);
                    log.info("用户角色roleId:{}", roleId);
                    log.info("资源需要权限authorities:{}", authorities);
                    return authorities.contains(roleId);
                })
                .map(AuthorizationDecision::new)
                .defaultIfEmpty(new AuthorizationDecision(false));
        return authorizationDecisionMono;
    }
}

第1、2处只是做些基础访问判断,不做过多的说明

第3处从Redis缓存获取资源权限数据。首先我们会关注两个问题:

复制代码12LANGUAGE-CSSa. 资源权限数据是什么样格式数据?
b. 数据什么时候初始化到缓存中?

以下就带着这两个问题来分析要完成第4步从缓存获取资源权限数据需要提前做哪些工作吧。

a. 资源权限数据格式

需要把url和role_ids的映射关系缓存到redis,大致意思的意思可以理解拥有url访问权限的角色ID有哪些。

b. 初始化缓存时机

SpringBoot提供两个接口CommandLineRunner和ApplicationRunner用于容器启动后执行一些业务逻辑,比如数据初始化和预加载、MQ监听启动等。两个接口执行时机无差,唯一区别在于接口的参数不同。有兴趣的朋友可以了解一下这两位朋友,以后会经常再见的哈~

那么这里的业务逻辑是在容器初始化完成之后将从MySQL读取到资源权限数据加载到Redis缓存中,正中下怀,来看下具体实现吧。

Redis缓存中的资源权限数据

至此从缓存数据可以看到拥有资源url访问权限的角色信息,从缓存获取赋值给resourceRolesMap。

第4处根据请求路径去匹配resourceRolesMap的资url(Ant Path匹配规则),得到对应资源所需角色信息添加到authorities。

第5处就是判断用户是否有权访问资源的最终一步了,只要用户的角色中匹配到authorities中的任何一个,就说明该用户拥有访问权限,允许通过。

4. 资源服务器配置

这里做的工作是将鉴权管理器AuthorizationManager配置到资源服务器、请求白名单放行、无权访问和无效token的自定义异常响应。配置类基本上都是约定俗成那一套,核心功能和注意的细节点通过注释说明。

复制代码1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950LANGUAGE-TYPESCRIPT/**
 * 资源服务器配置
 */
@AllArgsConstructor
@Configuration
// 注解需要使用@EnableWebFluxSecurity而非@EnableWebSecurity,因为SpringCloud Gateway基于WebFlux
@EnableWebFluxSecurity
public class ResourceServerConfig {

    private AuthorizationManager authorizationManager;
    private CustomServerAccessDeniedHandler customServerAccessDeniedHandler;
    private CustomServerAuthenticationEntryPoint customServerAuthenticationEntryPoint;
    private WhiteListConfig whiteListConfig;
    
    @Bean
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
        http.oauth2ResourceServer().jwt()
                .jwtAuthenticationConverter(jwtAuthenticationConverter());
        // 自定义处理JWT请求头过期或签名错误的结果
        http.oauth2ResourceServer().authenticationEntryPoint(customServerAuthenticationEntryPoint);
        http.authorizeExchange()
                .pathMatchers(ArrayUtil.toArray(whiteListConfig.getUrls(),String.class)).permitAll()
                .anyExchange().access(authorizationManager)
                .and()
                .exceptionHandling()
                .accessDeniedHandler(customServerAccessDeniedHandler) // 处理未授权
                .authenticationEntryPoint(customServerAuthenticationEntryPoint) //处理未认证
                .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> jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstants.AUTHORITY_PREFIX);
        jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstants.AUTHORITY_CLAIM_NAME);

        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
        return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
    }
    
}
复制代码123456789101112131415161718JAVA/**
 * 无权访问自定义响应
 */
@Component
public class CustomServerAccessDeniedHandler implements ServerAccessDeniedHandler {

    @Override
    public Mono handle(ServerWebExchange exchange, AccessDeniedException e) {
        ServerHttpResponse response=exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin","*");
        response.getHeaders().set("Cache-Control","no-cache");
        String body= JSONUtil.toJsonStr(Result.custom(ResultCodeEnum.USER_ACCESS_UNAUTHORIZED));
        DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
        return response.writeWith(Mono.just(buffer));
    }
}
复制代码12345678910111213141516171819JAVA/**
 * 无效token/token过期 自定义响应
 */
@Component
public class CustomServerAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

    @Override
    public Mono commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        response.getHeaders().set("Access-Control-Allow-Origin", "*");
        response.getHeaders().set("Cache-Control", "no-cache");
        String body = JSONUtil.toJsonStr(Result.custom(ResultCodeEnum.USER_ACCOUNT_UNAUTHENTICATED));
        DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
        return response.writeWith(Mono.just(buffer));
    }

}

5. 网关鉴权测试

模拟数据说明,admin用户拥有角色2,角色2有菜单管理、用户管理、部门管理的资源权限,无其他权限

用户

角色ID

角色名称

admin

2

系统管理员

资源名称

资源路径

要求角色权限

系统管理

/youlai-admin/**

[1]

菜单管理

/youlai-admin/menus/**

[1,2]

用户管理

/youlai-admin/users/**

[1,2]

部门管理

/youlai-admin/depts/**

[1,2]

字典管理

/youlai-admin/dictionaries/**

[1]

角色管理

/youlai-admin/roles/**

[1]

资源管理

/youlai-admin/resources/**

[1]

启动管理平台前端工程 youlai-mall-admin 完整代码地址:Gitee | Github |

访问除了菜单管理、用户管理、部门管理这三个系统管理员拥有访问权限的资源之外,页面都会提示“访问未授权”,直接的说明了网关服务器实现了请求鉴权的目的。

五. 结语

至此,Spring Cloud的统一认证授权就实现了。其实还有很多可以扩展的点,文章中把客户端信息存储在数据库中,那么可以添加一个管理界面来维护这些客户端信息,这样便可灵活配置客户端接入认证平台、认证有效期等等。同时也还有未完成的事项,我们知道JWT是无状态的,那用户在登出、修改密码、注销的时候怎么能把JWT置为无效呢?因为不可能像cookie/session机制把用户信息从服务器删除。所以这些都是值得思考的东西,我会在下篇文章提供对应的解决方案。

大家对文章或项目有好的建议,欢迎留言,感谢~

如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,咱们下期见。















本文链接:
https://www.cnblogs.com/haoxianrui/p/13719356.html

相关推荐

SUS631化学成分特性及适用范围(sus316的材料成分)

sus631特性及适用范围:添加铝的沉淀硬化型钢种,用作弹簧、热圈及计器部件。sus631/17-7PH/S17700/0Cr17Ni7Al沉淀硬化型不锈钢日本SUS631(17-7PH国标0Cr17...

4年仅掉价不到3成!这辆45万的奥迪S5纯素车,值不值得买?

哈喽~大家好,检车家老司机又和你们见面啦!我是你们的老朋友建国,我曾是一枚修过7年车的汽车修理工,现在是一名专业的二手车检测技师,更是一枚事故车劝退师。欢迎收看本期二手车检测趣事,今天给大家带来的车型...

新旗舰什么样?7张图全方位预测三星Note4

夏天已经快结束了,这也就意味着今年上半年的重磅机型已经悉数发布完毕,我们先要做的就是过一段平淡的日子然后期待着下半年的重磅产品陆续登场。三星GALAXYNote4无疑是下半年最值得我们期待的机型之一,...

拍照为主兼顾视频,将来可能接单,索尼a7m3和松下s5怎么选?

最近有小伙伴问了我下面这么一个问题。想买一台全画幅微单,但是预算有限,看好了两款机型,他的问题是:拍照为主兼顾视频,将来可能接单,索尼a7m3和松下s5怎么选?这个问题,我就想到这个网友也是做过一些功...

GolangWeb框架Iris项目实战-JWT和中间件(Middleware)的使用EP07

前文再续,上一回我们完成了用户的登录逻辑,将之前用户管理模块中添加的用户账号进行账号和密码的校验,过程中使用图形验证码强制进行人机交互,防止账号的密码被暴力破解。本回我们需要为登录成功的用户生成Tok...

对焦大提升!松下S5M2 3.0固件&amp;S5M2X 2.0固件详细测试

前段时间,松下发布了最新的S5M2和S5M2X的固件,并且这是一个比较大的更新,对对焦性能、防抖性能有较大提升,还新增了录制代理文件以及预连拍功能。这都是超级实用的功能,松下不愧是固件大厂!自动对焦升...

三星Galaxy S5安卓5.0测试版曝光,总算好看点了

三星GalaxyS5安卓5.0测试版曝光,总算好看点了出处:IT之家原创(远洋)2014-10-3123:11:04评论IT之家报道,显然,三星正在将自家的TouchWiz用户界面适配谷歌最...

松下S5M2对焦变换功能讲解#松下相机

大家好,今天和大家分享松下相机一个非常好用的功能:对焦变换。这个功能通常用于近景的视频拍摄,比如像现在画面里出现的人偶、象棋,看到视频的对焦点非常平顺的依次从画面的上方移动到中间,再移动到靠下的位置。...

松下 S5M2的几个重要功能(拍照篇) 松下 S5M2的几个重要功能

赶在618结束之前,我来讲几个松下S5二代的重要功能,也许其中的某一项功能会成为你购买它的理由。·首先来讲一讲高分辨率模式,这是一个非常好用的功能。使用松下S52代的左边波轮,可以直接进入这个模式。进...

奥的斯电梯OTIS DCSS5-E门机自学习详细过程

奥的斯电梯DCSS5-E门机自学习详细过程1.将轿顶打检修2.插入TT时按“M”键,提示你按“3”键,按“3”3.关键一步,因为怕你误操作而更改门机运行曲线和重要参数,正常情况下你是看不到第三个菜单的...

玩转smardaten | 逻辑编排很难吗?看无代码平台操作

1、前言不知道各位小伙伴有没有使用过无代码软件平台的经历呢?有没有想过你可以通过简单的拖拽就能构建和部署强大的应用程序?简单地说,smardaten无代码系统使任何人都能够开发软件,无需高深的开发知...

关于编码的那些事 - URL 编码(url编码的作用)

作者:redmed背景Web项目中经常会遇到处理URL中Query的情况,来看下下面问题你有疑惑吗?项目中发现会用到qs、query-string、URLSearchParams、甚至q...

国服一周数据观察:新英雄千珏胜率不足50%

随着千珏的更新,5.20版本到现在已经有一周的时间了,这段时间我们的排位赛数据有什么变化呢?快来看下排位赛数据分析。选择率最高的英雄榜单与过去的变化不大,仍然是盲僧、薇恩和赵信荣登榜首。但这不代表这些...

iPhone系列领跑消费者最喜爱拍摄设备名单

现在连GalaxyS5都被iPhone4s,iPhone4这类老机型压在后面,由此可见苹果的产品比起三星真是畅销太多了。国外著名图片分享网站Flickr,今天公布了2015年用户们最喜欢使...

Galaxy S6图像处理性能不如iPhone6 Plus?

三星最近公布了旗舰机GalaxyS6,有许多人批评、褒奖,但更常拿来跟iPhone6比较(明明不同系统阿?),而这次又有报告指GalaxyS6的「图像处理」不如iPhone6Plus,究竟是...