阅读 125

Spring Cloud Alibaba 实战 |Java 开发实战

这是我参与更文挑战的第2天,活动详情查看:更文挑战本文正在参加「Java主题月 - Java 开发实战」,详情查看 活动链接

2018年11月左右,Springcloud 联合创始人Spencer Gibb在Spring官网的博客页面宣布:阿里巴巴开源 Spring Cloud Alibaba,并发布了首个预览版本。随后,Spring Cloud 官方Twitter也发布了此消息。

一、环境准备

  • Spring Boot: 2.1.8

  • Spring Cloud: Greenwich.SR3

  • Spring Cloud Alibaba: 0.9.0.RELEASE

  • Maven: 3.5.4

  • Java 1.8 +

  • Oauth2 (Spring Security 5.1.6 +)

二、实战

项目模块

主要分为:鉴权中心、服务提供者、服务消费者、网关

实战

本例中,用到了 Nacos 作为注册中心、配置中心,估需要引入其依赖:

<dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>

      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>

复制代码

Oauth2 的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
复制代码

同时利用 redis 来处理鉴权的信息存储:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
复制代码

接下来需要准备配置文件 yaml:

management:
  endpoint:
    restart:
      enabled: true
    health:
      enabled: true
    info:
      enabled: true


spring:
  application:
    name: oauth-cas
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        refreshable-dataids: actuator.properties,log.properties

  redis: #redis相关配置
    database: 8
    host: 127.0.0.1
    port: 6379
    password: qwqwsq
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
    timeout: 10000ms
复制代码

注意,这个配置文件需要是 bootstrap,否则可能失败,至于为什么,大家可以自己试试。

接下来就是 application:

server:
  port: 2000
  undertow:
    accesslog:
      enabled: false
      pattern: combined
  servlet:
    session:
      timeout: PT120M


client:
  http:
    request:
      connectTimeout: 8000
      readTimeout: 30000

mybatis:
  mapperLocations: classpath:mapper/*.xml
  typeAliasesPackage: com.damon.*.model
复制代码

配置完成后,完成 main函数,在函数里配置最多的就是认证服务端,验证账号、密码,存储 token,检查 token ,刷新 token 等都是认证服务端的工作:

package com.damon;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;


@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableDiscoveryClient
public class CasApp {
  public static void main(String[] args) {
    SpringApplication.run(CasApp.class, args);
  }
}
复制代码

接下来就是配置几个 Oauth2 服务端的几个配置类:AuthorizationServerConfig、ResourceServerConfig、SecurityConfig、RedisTokenStoreConfig、MyRedisTokenStore、UserOAuth2WebResponseExceptionTranslator、AuthenticationEntryPointHandle 等。

其中最重要的就是登录时的函数,这里为了做演示,把用户名、密码和所属角色都写在代码里了,正式环境中,这里应该是从数据库或者其他地方根据用户名将加密后的密码及所属角色查出来的。账号admin,密码123456,并且给这个用户设置 "ROLE_ADMIN" 角色:

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      logger.info("clientIp is: {} ,username: {}", IpUtil.getClientIp(req), username);
    logger.info("serverIp is: {}", IpUtil.getCurrentIp());
    // 查询数据库操作
    try {
      SysUser user = userMapper.getUserByUsername(username);
      if (user == null) {
        logger.error("user not exist");
        throw new UsernameNotFoundException("username is not exist");
        //throw new UsernameNotFoundException("the user is not found");
      }
      else {
        // 用户角色也应在数据库中获取,这里简化
        String role = "";
        if(user.getIsAdmin() == 1) {
          role = "admin";
        }
        List<SimpleGrantedAuthority> authorities = Lists.newArrayList();
        authorities.add(new SimpleGrantedAuthority(role));
        //String password = passwordEncoder.encode("123456");// 123456是密码
        //return new User(username, password, authorities);
        // 线上环境应该通过用户名查询数据库获取加密后的密码
        return new User(username, user.getPassword(), authorities);
      }
    } catch (Exception e) {
      logger.error("database collect failed");
      logger.error(e.getMessage(), e);
      throw new UsernameNotFoundException(e.getMessage());
    }
  }
复制代码

函数 loadUserByUsername 需要验证数据库的密码,并且给用户授权角色。

到此,鉴权中心服务端完成。上面说的利用了 Nacos 来作为注册中心被客户端服务发现,并提供配置管理。

安装Nacos

下载 Nacos 地址:github.com/alibaba/nac…

版本:v1.2.1

执行:

  • Linux/Unix/Mac:sh startup.sh -m standalone

  • Windows:cmd startup.cmd -m standalone

启动完成之后,访问:http://127.0.0.1:8848/nacos/,可以进入Nacos的服务管理页面,具体如下:

image.png

默认用户名与密码都是nacos。

登陆后打开服务管理,可以看到注册到 Nacos 的服务列表:

image.png

可以点击配置管理,查看配置:

image.png

如果没有配置任何服务的配置,可以新建:

image.png

上面讲述了Nacos 如何作为注册中心与配置中心的,很简单吧。

接下来我们讲解服务提供者代码,先来看下pom.xml文件:

     
    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

        <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>

      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>

      <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
复制代码

这里是一如既往的引入依赖Oauth2、Nacos的服务注册与发现的依赖。

配置 bootstrap 文件,我们先来看服务应用本身的配置,以及服务需要连接注册中心、配置中心的Nacos配置:

management:
  endpoint:
    restart:
      enabled: true
    health:
      enabled: true
    info:
      enabled: true

spring:
  application:
    name: provider-service

  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        refreshable-dataids: actuator.properties,log.properties

cas-server-url: http://oauth-cas #http://localhost:2000#设置可以访问的地址

复制代码

后面,我们还需要配置Oauth2对于客户端的一些配置,如:应用信息、授权码模式的认证方式、密码模式的认证方式,是否开启负载均衡,以及认证后访问客户端时的token鉴权:

security:
  oauth2: #与cas对应的配置
    client:
      client-id: provider-service
      client-secret: provider-service-123
      user-authorization-uri: ${cas-server-url}/oauth/authorize #是授权码认证方式需要的
      access-token-uri: ${cas-server-url}/oauth/token #是密码模式需要用到的获取 token 的接口
    resource:
      loadBalanced: true
      #jwt: #jwt存储token时开启
        #key-uri: ${cas-server-url}/oauth/token_key
        #key-value: test_jwt_sign_key
      id: provider-service
      #指定用户信息地址,指定user info的URI,原生地址后缀为/auth/user
      user-info-uri: ${cas-server-url}/api/user
      prefer-token-info: false
      #token-info-uri:
    authorization:
      check-token-access: ${cas-server-url}/oauth/check_token
复制代码

当此web服务端接收到来自UI客户端的请求后,需要拿着请求中的 token 到认证服务端做 token 验证,需要请求这个接口:

security:
  oauth2:
    authorization:
      check-token-access: ${cas-server-url}/oauth/check_token
复制代码

最后,我们需要一些cookie的配置,防止冲突产生:

server:
  port: 2001
  undertow:
    accesslog:
      enabled: false
      pattern: combined
  servlet:
    session:
      timeout: PT120M
      cookie:
        name: PROVIDER-SERVICE-SESSIONID #防止Cookie冲突,冲突会导致登录验证不通过
复制代码

还有一些对于请求的设置,比如:请求超时等超时时间,以及数据库的一些基本设置:

client:
  http:
    request:
      connectTimeout: 8000
      readTimeout: 30000

mybatis:
  mapperLocations: classpath:mapper/*.xml
  typeAliasesPackage: com.damon.*.model
复制代码

还有就是对于设置了负载均衡时,需要不断 遍历服务刷新,服务负载时,采用轮询的方式,包括重试策略的设置:

backend:
  ribbon:
    client:
      enabled: true
    ServerListRefreshInterval: 5000


ribbon:
  ConnectTimeout: 3000
  # 设置全局默认的ribbon的读超时
  ReadTimeout: 1000
  eager-load:
    enabled: true
    clients: oauth-cas,consumer-service
  MaxAutoRetries: 1 #对第一次请求的服务的重试次数
  MaxAutoRetriesNextServer: 1 #要重试的下一个服务的最大数量(不包括第一个服务)
  #listOfServers: localhost:5556,localhost:5557
  #ServerListRefreshInterval: 2000
  OkToRetryOnAllOperations: true
  NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
复制代码

接下来我们看看启动类:

package com.damon;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableAutoConfiguration
@ComponentScan(basePackages = {"com.damon"})
@EnableDiscoveryClient
@EnableOAuth2Sso
public class ProviderApp {


    public static void main(String[] args) {
        SpringApplication.run(ProviderApp.class, args);
    }


}
复制代码

注意:注解 @EnableDiscoveryClient、@EnableOAuth2Sso 都需要。

这时,同样需要配置 ResourceServerConfig、SecurityConfig。

如果需要数据库,可以加上:

@Bean(name = "dataSource")
public DataSource getDataSource() throws Exception {
    Properties props = new Properties();
    props.put("driverClassName", envConfig.getJdbc_driverClassName());
    props.put("url", envConfig.getJdbc_url());
    props.put("username", envConfig.getJdbc_username());
    props.put("password", envConfig.getJdbc_password());
    return DruidDataSourceFactory.createDataSource(props);
}


@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource) throws Exception {


    SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
    // 指定数据源(这个必须有,否则报错)
    fb.setDataSource(dataSource);
    // 下边两句仅仅用于*.xml文件,如果整个持久层操作不需要使用到xml文件的话(只用注解就可以搞定),则不加
    fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
    fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapperLocations")));// 指定xml文件位置


    // 分页插件
    PageHelper pageHelper = new PageHelper();
    Properties props = new Properties();
    // 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页
        //禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据
    props.setProperty("reasonable", "true");
    //指定数据库
    props.setProperty("dialect", "mysql");
    //支持通过Mapper接口参数来传递分页参数
    props.setProperty("supportMethodsArguments", "true");
    //总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page
    props.setProperty("returnPageInfo", "check");
    props.setProperty("params", "count=countSql");
    pageHelper.setProperties(props);
    // 添加插件
    fb.setPlugins(new Interceptor[] { pageHelper });


    try {
      return fb.getObject();
    } catch (Exception e) {
            throw e;
    }
  }

@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    return new SqlSessionTemplate(sqlSessionFactory);
}
复制代码

对于数据库事务,Mybatis需要配置:

@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) throws Exception{
   return new DataSourceTransactionManager(dataSource);
}
复制代码

接下来新写一个接口类:


@GetMapping("/getCurrentUser")
@PreAuthorize("hasAuthority('admin')")
public Object getCurrentUser(Authentication authentication) {
  logger.info("test password mode");
    return authentication;
}

复制代码

基本一个代码就完成了。接下来需要测试一下:

认证:

curl -i -X POST -d "username=admin&password=123456&grant_type=password&client_id=provider-service&client_secret=provider-service-123" http://localhost:5555/oauth-cas/oauth/token
复制代码

拿到token后:

curl -i -H "Accept: application/json" -H "Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9" -X GET http://localhost:5555/provider-service/api/user/getCurrentUser
复制代码

这里用到了 5555 端口,这是一个网关服务,好吧,既然提到这个,我们接下来看网关吧,引入依赖:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--基于 reactive stream 的redis -->
    <!-- <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency> -->

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-commons</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
      </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>


        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
        </dependency>
复制代码

同样利用 Nacos 来发现服务。

并且我们并没有给每一个服务单独配置路由而是使用了服务发现自动注册路由的方式,这里的注册配置改为:

spring:
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          lowerCaseServiceId: true

    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        refreshable-dataids: actuator.properties,log.properties

复制代码

好了,网关配置好后,启动在 Nacos dashboard可以看到该服务,表示注册服务成功。接下来就可以利用其来调用其他服务了。具体 curl 命令:

curl -i -H "Accept: application/json" -H "Authorization:bearer f4a42baa-a24a-4342-a00b-32cb135afce9" -X GET http://localhost:5555/consumer-service/api/order/getUserInfo
复制代码

到此鉴权中心、服务提供者、服务消费者、服务的注册与发现、配置中心等功能已完成。

文章分类
后端
文章标签