图片 56

构建保护微服务系统,cloud编码规范以及编写指导

我们希望自己的微服务能够在用户登录之后才可以访问,而单独给每个微服务单独做用户权限模块就显得很弱了,从复用角度来说是需要重构的,从功能角度来说,也是欠缺的。尤其是前后端完全分离之后,我们的用户信息不一定存在于
Session 会话中,本节内容使用OAuth2+JWT的功能恰好能够弥补这块。

编码规范

本篇内容

  • Spring Cloud 项目结构概览
  • Dockerfile文件准备
  • Jenkins参数设置
  • Jenkins中创建流水线脚本
  • 详细Groovy Pipeline脚本
  • Docker Compose安装
  • WebHook触发构建
  • Git多分支环境下构建
  • Spring Cloud 项目部署Docker中的坑

我以目前我们公司项目为例,我将Spring Cloud项目分成了如下几块

图片 1image.png

  1. eureka:服务发现,端口8761
  2. config:配置中心,端口9118
  3. basic:基础服务,提供与业务没有关系的通用service,端口9102
  4. pms:酒店PMS数据回调service,端口9103
  5. host:后台基础信息service,端口9100
  6. business:业务系统service,端口9101
  7. gateway:网关,对外暴露端口为443和8088

maven项目模块化考虑到项目中的DTO、VO、通用类等在多个service中会使用到,且serivce之间互相调用使用了Fegin,所以,service中项目结构会是这样,以host-service为例:

图片 2image.png

host中pom.xml分了3个模块

图片 3image.png

模块之间互相引用需要如下几步

  1. mvn -Dmaven.test.skip=true -U clean install 构建jar包并安装到本地
  2. mvn install:install-file 使用install-file命令将jar包上传到本地仓库
  3. 在pom中添加依赖包,会从本地仓库获取指定的包

    图片 4image.png

关于详细的模块化步骤,请看此文使用Maven构建多模块项目

图片 5image.png

创建两个文件夹,docker-test,docker-proddocker-test为测试环境docker-prod为正式环境

以测试环境为例,在docker-test中创建Dockerfile

图片 6image.png

FROM hub.c.163.com/library/java:8-alpineMAINTAINER showADD /target/futurekey-host.jar app.jarENTRYPOINT ["java","-jar","/app.jar"]

target下存放的是编译后的项目jar包

为了WebHook能够将请求push到Jenkins,需要如下设置

图片 7image.png

取消防跨站点请求伪造和允许匿名访问读取

图片 8image.png图片 9image.png

创建流水线脚本

图片 10image.png

勾选触发远程构建,身份令牌随便填,触发的url为JENKINS_URL/job/eureka/build?token=TOKEN

图片 11image.png

  • JENKINS_URL:jenkins的访问地址
  • eureka:此处为脚本的名称
  • TOKEN:输入身份验证令牌

Gitlab项目的Webhook输入jenkins的地址,点击Add webhook

图片 12image.png

测试推送

图片 13image.png

显示201则为成功

图片 14image.png

Groovy流水线脚本格式

图片 15image.png

基本格式为

#!groovypipeline { agent any environment { ENV="test" } stages { stage { steps { echo "${ENV}" } } }}
  • environment: 环境变量,使用时为${xx}格式,xx为变量名
  • stages: 所有流水线的阶段,stages中包含多个stage
  • steps: 脚本执行步骤

流水线语法

图片 16image.png

提供了多个步骤的选择

图片 17image.png

我现在需要生成获取git clone的脚本

图片 18image.png图片 19image.png

点击生成

图片 20image.png

生成的脚本复制到文本,后面会使用到。

大致的脚本执行步骤

我的项目前端网页框架用的是VUE Element
UI,所以分了两个git项目,每次发布时,只需要将vue编译后的文件夹copy到网关的static文件夹下,然后重写static/index.html为首页即可。

@Configurationpublic class DefaultViewConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController.setViewName("forward:/index.html"); registry.setOrder(Ordered.HIGHEST_PRECEDENCE); }}

图片 21image.png

脚本步骤如下

#!groovypipeline { agent any environment { REPOSITORY="https://gitee.com/lanzhima-show/project.git" WEB_REPOSITORY="https://gitee.com/lanzhima-show/web.git" JENKINSDIR="/var/lib/jenkins/workspace/" PORJECTDIR="eureka/" ENV="test" FILEDIR="shell/" } stages { stage { steps { } } stage { steps { } } stage('提升shell文件执行权限') { steps { } } stage('生成maven jar包') { steps { } } stage { steps { } } stage('构建docker镜像') { steps { } } stage('docker发布') { steps { } } }}

获取网页代码

stage { echo "start fech code form git:${WEB_REPOSITORY}" deleteDir() git branch: 'develop', credentialsId: 'd964d8c1-366a-42d4-aff7-b429d43952ec', url: "${WEB_REPOSITORY}" sh ''' cd ${JENKINSDIR} rm -rf pages mkdir pages cp -rf ${JENKINSDIR}${PORJECTDIR}dist/. ${JENKINSDIR}pages '''}
  1. 删除原来的文件夹
  2. 从Git克隆代码
  3. 创建一个pages的文件夹
  4. 将VUE编译好的dist文件夹下的所有内容,复制到pages文件夹中

获取代码

stage { steps { echo "start fech code form git:${REPOSITORY}" deleteDir() git branch: 'develop', credentialsId: 'd964d8c1-366a-42d4-aff7-b429d43952ec', url: "${REPOSITORY}" sh ''' mv -f ${JENKINSDIR}pages/* ${JENKINSDIR}${PORJECTDIR}project-gateway/src/main/resources/static/ ''' } }
  1. 删除原来的文件夹
  2. 从Git克隆代码
  3. 将pages文件夹下内容移动到gateway的static下

提升shell文件执行权限

stage('提升shell文件执行权限') { steps { sh ''' cd ${JENKINSDIR}${PORJECTDIR}${FILEDIR} chmod 755 mvn-build.sh chmod 755 packages-build.sh cd ${JENKINSDIR}${PORJECTDIR} chmod +x docker-compose.yml ''' } }

为了能够执行shell脚本,需要赋予权限

maven编译的shell脚本,主要作用是编译可执行jar包,将jar包复制到Dockerfile文件夹中,便于后续docker生成镜像mvn-build.sh

#!/bin/bash -ilexPROJECTDIR=$1MODULEDIR=$2ENV=$3JARNAME=$4cd ${PROJECTDIR}${MODULEDIR}mvn -Dmaven.test.skip=true -U clean installmkdir ${PROJECTDIR}${MODULEDIR}docker-${ENV}/target cp -f ${PROJECTDIR}${MODULEDIR}target/${JARNAME} ${PROJECTDIR}${MODULEDIR}docker-${ENV}/target/${JARNAME}
  1. 生成jar包
  2. 创建target文件夹
  3. 复制jar包到target文件夹下

打包jar包前,需要对各项目中的模块先进行maven打包,可根据自己的项目进行实际修改package-build.sh

图片 22image.png

Docker
Compose编排脚本
经过我测试,如果将所有的容器服务全部放入一个docker-compose.yml中后,由于无法指定启动的先后顺序,并且几个service必须依赖于eureka和config后才能启动,会导致service找不到eureka和config。所以,我的做法是将整个spring
cloud的服务编排做成两个编排脚本

第一个docker-compose.yml负责启动eureka和configdocker-compose.yml

version: "3"services: eureka: container_name: eureka image: demo/eureka:latest ports: - "8761:8761" config: image: demo/config:latest ports: - "9118:9118" links: - "eureka" environment: - eureka.client.service-url.defaultZone=http://eureka:8761/eureka/ - spring.profiles.active=test

指定eureka的container_name为eureka后,就可以通过容器名称找到eureka服务,所以需要修改环境变量eureka.client.service-url.defaultZone=

第二个docker-compose.yml负责把service和gateway拉起docker-compose-service-test.yml

version: "3"services: basic: image: demo/basic:latest ports: - "9102:9102" environment: - eureka.client.service-url.defaultZone=http://eureka:8761/eureka/ - spring.cloud.config.profile=test pms: image: demo/pms:latest ports: - "9103:9103" environment: - eureka.client.service-url.defaultZone=http://eureka:8761/eureka/ - spring.cloud.config.profile=test host: image: demo/host:latest ports: - "9100:9100" environment: - eureka.client.service-url.defaultZone=http://eureka:8761/eureka/ - spring.cloud.config.profile=test business: image: demo/business:latest ports: - "9101:9101" environment: - eureka.client.service-url.defaultZone=http://eureka:8761/eureka/ - spring.cloud.config.profile=test gateway: image: demo/gateway:latest ports: - "443:443" - "8088:8088" environment: - eureka.client.service-url.defaultZone=http://eureka:8761/eureka/ - spring.cloud.config.profile=test 

生成maven jar包

stage('生成maven jar包') { steps { echo "start build maven package" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}packages-build.sh ${JENKINSDIR}${PORJECTDIR}" } }

编译项目

steps { echo "start compile eureka" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-eureka/ ${ENV} demo-eureka.jar" echo "start compile config" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-config/ ${ENV} demo-config.jar" echo "start compile basic" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-basic/web/ ${ENV} demo-basic.jar" echo "start compile pms" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-pms/web/ ${ENV} demo-pms.jar" echo "start compile host" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-host/web/ ${ENV} demo-host.jar" echo "start compile business" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-business/web/ ${ENV} demo-business.jar" echo "start compile gateway" sh "${JENKINSDIR}${PORJECTDIR}${FILEDIR}mvn-build.sh ${JENKINSDIR}${PORJECTDIR} demo-gateway/ ${ENV} demo-gateway.jar" }

构建docker镜像

steps { echo "start build docker images" echo "start build eureka image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-eureka/docker-${ENV} docker build -t demo/eureka --rm . ''' echo "start build config image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-config/docker-${ENV} docker build -t demo/config --rm . ''' echo "start build basic image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-basic/web/docker-${ENV} docker build -t demo/basic --rm . ''' echo "start build pms image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-pms/web/docker-${ENV} docker build -t demo/pms --rm . ''' echo "start build host image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-host/web/docker-${ENV} docker build -t demo/host --rm . ''' echo "start build business image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-business/web/docker-${ENV} docker build -t demo/business --rm . ''' echo "start build gateway image" sh ''' cd ${JENKINSDIR}${PORJECTDIR}futurekey-gateway/docker-${ENV} docker build -t demo/gateway --rm . ''' }

docker发布

steps { echo "start deploy" sh ''' cd ${JENKINSDIR}${PORJECTDIR} docker-compose down --remove-orphans docker-compose -f docker-compose-service-test.yml down docker-compose up -d sleep 45s docker-compose -f docker-compose-service-test.yml up -d docker images|grep none|awk '{print $3}'|xargs docker rmi ''' }

坑1:目前我没有找到让多个docker-compose.yml顺序执行的方法,所以我设置了sleep时间等待,45秒足够让eureka和config启动了。

坑2:在docker-compose down --remove-orphans中,如果不加--remove-orphans,则无法删除依赖的网络,导致后续命令执行时报错,从而发布会中断。

坑3:我使用docker-compose down后,会产品none的镜像,所以我最后会删除none镜像,docker images|grep none|awk '{print $3}'|xargs docker rmi,不知道有没有大佬能够指出我的问题。

安装命令

pip install docker-compose

查看版本

docker-compose -version

重启jenkins

systemctl restart jenkins

我使用的是免费版的gitee,gitee也支持webhook,在gitee中设置

图片 23image.png

当每次提交之后,都会触发构建。

图片 24image.png

现在我们每次push代码都会触发构建,但是一般我们会使用GitFlow管理各分支,这时,自动构建就需要区分是哪个分支的了,一般公司都会按照分支构建开发环境、测试环境、生成环境。我们可以按照GitFlow工作流,将develop指定为测试环境,master指定为正式环境。

安装Generic Webhook Trigger插件

在Jenkins的插件中心搜索webhook,安装Generic Webhook Trigger插件

图片 25image.png

安装成功后,在编辑脚本中,可以看到新增的Generic Webhook Trigger

图片 26image.png

定义两个参数,ref为提交的分支名称,project.git_ssh_url为项目的ssh地址

图片 27image.png

添加验证表达式

图片 28image.png

webhook地址

webhook地址为

图片 29image.png

添加token参数作为验证

图片 30image.png

设置新的webhook地址

图片 31image.png

构建成功后,能看到各步骤花费了多少时间

图片 32image.png

  1. 内存问题:centos中JVM默认给一个java进程分配的最大内存空间是在JAVA_OPTS中设置的,如果不手动调整,内存小一点的主机会马上内存吃紧。而spring
    cloud中明显有多个java项目要执行,所以必须手动调整。解决方法:在Dockerfile中添加-Xmx128m的参数,设置最大内存为128mb,你可以根据自己实际的配置进行调整。

2.时区问题:docker中的默认时区是GMT,与当前时间会相差8小时。解决方法:在Dockerfile中添加-Duser.timezone=GMT+8

FROM hub.c.163.com/library/java:8-alpineMAINTAINER show ADD /target/host.jar app.jarENTRYPOINT ["java","-Xmx128m","-Duser.timezone=GMT+8","-jar","/app.jar"]

常见的应用场景如下图,用户通过浏览器进行登录,一旦确定用户名和密码正确,那么在服务器端使用秘钥创建
JWT,并且返回给浏览器;接下来我们的请求需要在头部增加 jwt
信息,服务器端进行解密获取用户信息,然后进行其他业务逻辑处理,再返回客户端

命名

严格遵守java命名规范

  • 命名采用驼峰式
  • 变量名将拥有一定的含义,要通俗易懂,杜绝使用含糊不清的变量名称
  • 其他参照java官方要求进行编码

图片 33image

注释

必须提供注释说明具体的业务逻辑。可以自行创建intellij注释模板。方法包括,方法注释,类注释,
方法需要写明具体的用途以及注意事项。

我们基于 Spring Cloud 的骨架进行搭建,分为3个工程,eureka
服务器,负责微服务注册;auth 服务器,负责授权,需要提供 clientId
和密码;user
微服务,一个微服务提供,他作为资源服务器,资源是被保护起来的,需要相应的权限才能访问。User
微服务得到用户请求的 JWT 之后,使用公钥解密,得到用户信息和权限信息。

微服务命名

项目中的业务服务需要根据具体的业务类型携带service,比如user-service,
order-service…

图片 34image

编码注意事项

所有微服务将完成服务注册于发现,不同业务之间调用,进项使用REST进行内部链接,不需重复写实现逻辑。服务的拆分本着粗粒度的原则,尽量避免分布式事务。

编写主 maven 工程

构建一个 maven 项目,打包类型是 pom,其中该 pom 文件内容如下

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cnsesan</groupId> <artifactId>cnsean-architecture-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>pom</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.13.RELEASE</version> <relativePath/> </parent> <properties> <security.version>1.0.0-SNAPSHOT</security.version> <java.version>1.8</java.version> </properties> <!-- 替我们管理依赖的版本信息 --> <dependencyManagement> <dependencies> <!-- spring io --> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Brussels-SR11</version> <type>pom</type> <scope>import</scope> </dependency> <!-- spring cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Dalston.SR5</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <compilerVersion>1.8</compilerVersion> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> <modules> <module>cnsesan-eureka-single</module> <module>cnsesan-uaa-service</module> <module>cnsesan-user-service</module> </modules></project>

上述的版本是经过测试可以正常使用的,如果需要更新到
SpringBoot2.0版本,需要更新其他版本进行对应。同时也看到该 pom
内部包含3个 module,接下来我们分别来构建这3个 module。

技术选型

构建 EurekaServer

这里我们构建的是单个 Eureka
服务器作为测试,真实环境是需要集群的。在父项目的基础上,右键构建,如下图(IDE
为 STS)

图片 35image图片 36image

配置 pom,加入依赖

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.cnsesan</groupId> <artifactId>cnsean-architecture-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>cnsesan-eureka-single</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka-server</artifactId> </dependency> </dependencies></project>

这里仅仅引入 eureka 服务器端的依赖即可

配置 yml 文件

spring: application: name: eureka-servereureka: instance: hostname: localhost client: serviceUrl: defaultZone: http://localhost:8762/eureka/ register-with-eureka: false fetch-registry: false# instance: # preferIpAddress: true server: # 关闭自我保护模式 enable-self-preservation: false # 续期时间,即扫描失效服务的间隔时间(缺省为60*1000ms) eviction-interval-timer-in-ms: 5000logging: level: com.netflix: INFO server: port: 8762

端口是8762,名称是eureka-server

在 Application 启动类中添加注解

package com.cnsesan.eureka;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.builder.SpringApplicationBuilder;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@EnableEurekaServer@SpringBootApplicationpublic class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class) .web.run; }}

找到BootDashboard,运行eureka

图片 37image

jdk

本次选择java版本为1.8

构建 Uaa 授权服务

同样构建 maven 项目,导入依赖,pom 文件为

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.cnsesan</groupId> <artifactId>cnsean-architecture-spring-cloud</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>cnsesan-uaa-service</artifactId> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-eureka</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <configuration> <nonFilteredFileExtensions>cert</nonFilteredFileExtensions> <nonFilteredFileExtensions>jks</nonFilteredFileExtensions> </configuration> </plugin> </plugins> </build> </project>

其中最后一段是防止打包的时候把公钥和私钥文件搞乱,读取不了。

接下来配置 application.yml

spring: application: name: uaa-service datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8 username: root password: root jpa: hibernate: ddl-auto: update show-sql: trueserver: port: 9999eureka: client: serviceUrl: defaultZone: http://localhost:8762/eureka/

端口是9999,服务名称是 uaa-service与 application.yml
相同地方还需要2个文件,分别是cnsesan-jwt.jks和
public.cert我们先把这两个文件弄出来

keytool -genkeypair -alias cnsesan-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=cnsesan,O=cnsesan,L=zurich,S=zurich,C=CH" -keypass cnsesan123 -keystore cnsesan-jwt.jks -storepass cnsesan123

如上操作得到cnsesan-jwt.jks然后需要的都公钥文件,如下

keytool -list -rfc --keystore cnsesan-jwt.jks | openssl x509 -inform pem -pubkey

输入密码 cnsesan123,将如下片段拷贝到新文件public.cert

图片 38image

可以得到public.cert将这两个文件拷贝到 resource 目录下

图片 39image

接下来首先编写启动类,主要是几个注解

package com.cnsesan.uaa;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.builder.SpringApplicationBuilder;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;@SpringBootApplication@EnableResourceServer@EnableEurekaClientpublic class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class) .web.run; }}

然后是编写我们的配置类,也是最核心的地方

首先编写配置Spring Security

@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter{ @Autowired private UserServiceDetail userServiceDetail ; @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .exceptionHandling() .authenticationEntryPoint((request,response,authException)->response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .authorizeRequests() .antMatchers.authenticated .httpBasic() ; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userServiceDetail).passwordEncoder(new BCryptPasswordEncoder; } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

这里有个UserServiceDetail,实现了UserDetailsService,他的代码如下,主要是负责用户信息获取的

@Servicepublic class UserServiceDetail implements UserDetailsService { @Autowired private UserDao userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return userRepository.findByUsername; }}

UserDao 类 是一个接口,使用 JPA 的方式,如下

public interface UserDao extends JpaRepository<User, Long>{ User findByUsername(String username);}

User 和 Role 两个实体类需要做如下的实现

@Entitypublic class User implements UserDetails, Serializable{ private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @Column(nullable=false,unique=true) private String username; @Column() private String password; @ManyToMany(cascade = CascadeType.ALL, fetch= FetchType.EAGER) @JoinTable(name="user_role",joinColumns=@JoinColumn(name="user_id",referencedColumnName="id"),inverseJoinColumns=@JoinColumn(name="role_id",referencedColumnName="id")) private List<Role> authorities; public User() { } public Long getId() { return id; } public void setId { this.id = id; } public void setUsername(String username) { this.username = username; } public void setPassword(String password) { this.password = password; } public void setAuthorities(List<Role> authorities) { this.authorities = authorities; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; }}

@Entitypublic class Role implements GrantedAuthority{ private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private Long id; @Column(nullable=false) private String name; @Override public String getAuthority() { return name; } public Long getId() { return id; } public void setId { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }

其次编写OAuth2Config,该类是配置OAuth2相关内容

@Configuration@EnableAuthorizationServer // 开启授权服务功能public class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; // 配置客户端基本信息 @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("user-service")// 创建一个客户端 名字是user-service .secret .scopes("service") .authorizedGrantTypes("refresh_token", "password") .accessTokenValiditySeconds; } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore.tokenEnhancer(jwtTokenEnhancer .authenticationManager(authenticationManager); } public TokenStore tokenStore() { return new JwtTokenStore(jwtTokenEnhancer; } private JwtAccessTokenConverter jwtTokenEnhancer() { KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("cnsesan-jwt.jks"), "cnsesan123".toCharArray; JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("cnsesan-jwt")); return converter; }}

到此为止,授权服务器搭建完毕,启动,在测试之前,数据库需要增加一些表

DROP TABLE IF EXISTS `clientdetails`;CREATE TABLE `clientdetails` ( `appId` varchar NOT NULL, `resourceIds` varchar DEFAULT NULL, `appSecret` varchar DEFAULT NULL, `scope` varchar DEFAULT NULL, `grantTypes` varchar DEFAULT NULL, `redirectUrl` varchar DEFAULT NULL, `authorities` varchar DEFAULT NULL, `access_token_validity` int DEFAULT NULL, `refresh_token_validity` int DEFAULT NULL, `additionalInformation` varchar DEFAULT NULL, `autoApproveScopes` varchar DEFAULT NULL, PRIMARY KEY  ENGINE=InnoDB DEFAULT CHARSET=utf8;;-- ------------------------------ Table structure for oauth_access_token-- ----------------------------DROP TABLE IF EXISTS `oauth_access_token`;CREATE TABLE `oauth_access_token` ( `token_id` varchar DEFAULT NULL, `token` blob, `authentication_id` varchar NOT NULL, `user_name` varchar DEFAULT NULL, `client_id` varchar DEFAULT NULL, `authentication` blob, `refresh_token` varchar DEFAULT NULL, PRIMARY KEY (`authentication_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;;-- ------------------------------ Table structure for oauth_approvals-- ----------------------------DROP TABLE IF EXISTS `oauth_approvals`;CREATE TABLE `oauth_approvals` ( `userId` varchar DEFAULT NULL, `clientId` varchar DEFAULT NULL, `scope` varchar DEFAULT NULL, `status` varchar DEFAULT NULL, `expiresAt` datetime DEFAULT NULL, `lastModifiedAt` datetime DEFAULT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8;;-- ------------------------------ Table structure for oauth_client_details-- ----------------------------DROP TABLE IF EXISTS `oauth_client_details`;CREATE TABLE `oauth_client_details` ( `client_id` varchar NOT NULL, `resource_ids` varchar DEFAULT NULL, `client_secret` varchar DEFAULT NULL, `scope` varchar DEFAULT NULL, `authorized_grant_types` varchar DEFAULT NULL, `web_server_redirect_uri` varchar DEFAULT NULL, `authorities` varchar DEFAULT NULL, `access_token_validity` int DEFAULT NULL, `refresh_token_validity` int DEFAULT NULL, `additional_information` varchar DEFAULT NULL, `autoapprove` varchar DEFAULT NULL, PRIMARY KEY (`client_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;;-- ------------------------------ Table structure for oauth_client_token-- ----------------------------DROP TABLE IF EXISTS `oauth_client_token`;CREATE TABLE `oauth_client_token` ( `token_id` varchar DEFAULT NULL, `token` blob, `authentication_id` varchar NOT NULL, `user_name` varchar DEFAULT NULL, `client_id` varchar DEFAULT NULL, PRIMARY KEY (`authentication_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;;-- ------------------------------ Table structure for oauth_code-- ----------------------------DROP TABLE IF EXISTS `oauth_code`;CREATE TABLE `oauth_code` ( `code` varchar DEFAULT NULL, `authentication` blob) ENGINE=InnoDB DEFAULT CHARSET=utf8;;-- ------------------------------ Table structure for oauth_refresh_token-- ----------------------------DROP TABLE IF EXISTS `oauth_refresh_token`;CREATE TABLE `oauth_refresh_token` ( `token_id` varchar DEFAULT NULL, `token` blob, `authentication` blob) ENGINE=InnoDB DEFAULT CHARSET=utf8;;SET FOREIGN_KEY_CHECKS = 1;

现在可以测试

curl user-service:123456@localhost:9999/oauth/token -d grant_type=password -d username=ts -d password=123456

得到如下{“access_token”:”eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MzA4NTEyNjMsInVzZXJfbmFtZSI6InRzIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9VU0VSIiwiUk9MRV9BRE1JTiIsIkFETUlOIl0sImp0aSI6ImNkYmE1MTExLTlmNmEtNGU1NS04ZmRhLTUzYzAzOWYxOWRiMiIsImNsaWVudF9pZCI6InVzZXItc2VydmljZSIsInNjb3BlIjpbInNlcnZpY2UiXX0.KFO-37xi0z086lbdOzRKNZBijDVSi4dlpdFVzhHvXkvbypsEGLIrurntWf5UhQaFZ9xB8JPGIgjvbybfrpZxWwTJgX04NpXSkrATBsQucI-J181lhuHeefwLDfPsAIRP4QGbzbgLZ_4RrAdi66PU2oKIYV0-REUIhtRNzJhUFCZckWpa2pLo0hwzq8gzBVFoOrsWtwTeDrGKc3F7RWCsDJeByGvyBfI33n6r3S6XOSt0aNvLBrihqBAqPgudWeCHO-4gQ5MBh7SCz9H-oO92vviNaiEVklEJP24l52R0TTFsxky4YbUsozPU6YXyoxa5o2dxJo_pWoek-GmdW7_YJw”,”token_type”:”bearer”,”refresh_token”:”eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ0cyIsInNjb3BlIjpbInNlcnZpY2UiXSwiYXRpIjoiY2RiYTUxMTEtOWY2YS00ZTU1LThmZGEtNTNjMDM5ZjE5ZGIyIiwiZXhwIjoxNTMzNDM5NjYzLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiLCJST0xFX0FETUlOIiwiQURNSU4iXSwianRpIjoiYzAxMGY4MmUtZDRkYS00MTNmLWEwMTctZTM0MzA2YWY2OWViIiwiY2xpZW50X2lkIjoidXNlci1zZXJ2aWNlIn0.RKe3rjgrl3Hu1jAVa68csSJ-Y2b75LWYgke5urscQGv2OH7dOuOmcyUo9K_dfvT9Jz9WNDdz-rmdCBfw7bPdoDfCh4wCi-2Xh0ufl6Q4RO6eWLGSpcA2x7-dJsh325Ylje6PC3-__ID_SS1znM4zw_xBubp1Uah0hpuEkqtKUgPWOnV4eybvGvJlSqbZLhenCQrhYCrWW781jYkCKm8E6AoQHUyVRrQ_jiyfcfYQs9wEuJNtuZXwoYIW4xM-hDr1rVkPab8thjZ3EkVnIgoTXo0t_i_SiVWCrNo2874QZq8BBj3-St7YyW_JyQM0jGT5VrgkcbCiuCZebDdyIBBAdQ”,”expires_in”:3599,”scope”:”service”,”jti”:”cdba5111-9f6a-4e55-8fda-53c039f19db2″}%

spring-cloud

本次使用的spring-cloud版本为Camden.SR4

构建 user 微服务

同样构建 maven module,名称是 cnsesan-user-servicepom 依赖和上面的 uaa
类似,多了如下2个依赖

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

配置文件 application.yml

server: port: 9090 eureka: client: serviceUrl: defaultZone: http://localhost:8762/eureka/ spring: application: name: user-service datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/spring-cloud-auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8 username: root password: root jpa: hibernate: ddl-auto: update show-sql: true feign: hystrix: enabled: true

同时把 public.cert拷贝一份到 resource 目录接下来还是先编写启动类

@EnableFeignClients@SpringBootApplication@EnableEurekaClientpublic class Application { public static void main(String[] args) { new SpringApplicationBuilder(Application.class) .web.run; }}

配置资源服务器

@Configuration @EnableResourceServerpublic class ResourceServerConfig extends ResourceServerConfigurerAdapter{ @Autowired TokenStore tokenStore ; @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/user/login","/user/register").permitAll() .antMatchers.authenticated(); } @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { resources.tokenStore(tokenStore); } }

配置 JWT

@Configurationpublic class JwtConfig { @Autowired JwtAccessTokenConverter jwtAccessTokenConverter; @Bean @Qualifier("tokenStore") public TokenStore tokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter); } @Bean public JwtAccessTokenConverter jwtTokenEnhancer(){ JwtAccessTokenConverter converter= new JwtAccessTokenConverter (); Resource resource= new ClassPathResource ("public.cert"); String publicKey; try { publicKey=new String(FileCopyUtils.copyToByteArray(resource.getInputStream; } catch (IOException e) { throw new RuntimeException(); } converter.setVerifierKey(publicKey); return converter; } }

配置 开启方法级别安全验证

@Configuration@EnableGlobalMethodSecurity(prePostEnabled = true) //开启方法级别安全验证public class GlobalMethodSecurityConfig {}

编写用户相关服务,用户注册和用户登录

@Servicepublic class UserServiceDetail { @Autowired private UserDao userRepository; @Autowired AuthServiceClient client; public User insertUser(String username,String password){ User user=new User(); user.setUsername; user.setPassword(BPwdEncoderUtil.BCryptPassword); return userRepository.save; } public UserLoginDTO login(String username,String password){ User user=userRepository.findUserByUsername; if(user==null){ throw new RuntimeException; } if(!BPwdEncoderUtil.matches(password, user.getPassword{ throw new RuntimeException; } //dXNlci1zZXJ2aWNlOjEyMzQ1Ng== 是 user-service:123456的 base64编码 JWT jwt=client.getToken("Basic dXNlci1zZXJ2aWNlOjEyMzQ1Ng==", "password", username, password); if(jwt==null){ throw new RuntimeException("用户Token有问题"); } UserLoginDTO dto=new UserLoginDTO(); dto.setUser; dto.setJwt; return dto; } }

上面服务有个AuthServiceClient类,他是个接口,使用Feign向 uaa
去请求,同时加以熔断机制进行处理

@FeignClient(value="uaa-service", fallback =AuthServiceHystrix.class )public interface AuthServiceClient { @PostMapping(value ="/oauth/token") JWT getToken(@RequestHeader(value="Authorization")String authorization, @RequestParam("grant_type")String type, @RequestParam("username")String username, @RequestParam("password")String password);}

而AuthServiceHystrix是一个默认的处理方式

@Componentpublic class AuthServiceHystrix implements AuthServiceClient{ @Override public JWT getToken(String authorization, String type, String username, String password) { // TODO Auto-generated method stub return null; }}

JWT 是一个 POJO 类

public class JWT { private String access_token,token_type,refresh_token,scope,jti; private int expires_in; //set和 get}

UserDao,User,Role和之前的 uaa 项目一样,不在赘述。

针对异常做统一处理

@ControllerAdvice@ResponseBodypublic class ExceptionHandle { @ExceptionHandler(RuntimeException.class) public ResponseEntity<String> handleException(Exception e){ return new ResponseEntity (e.getMessage () , HttpStatus.OK) ; } }

编写我们的控制层的类

@RestController@RequestMappingpublic class UserController { @Autowired UserServiceDetail userServiceDetail; @PostMapping("/register") public User postUser(@RequestParam("username")String username,@RequestParam("password")String password){ return userServiceDetail.insertUser(username, password); } @PostMapping  public UserLoginDTO login(@RequestParam ("username")String username,@RequestParam ("password")String password){ return userServiceDetail.login(username, password); } }

其中涉及的一个工具类BPwdEncoderUtil

public class BPwdEncoderUtil { private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder (); public static String BCryptPassword(String password){ return encoder.encode; } public static boolean matches (CharSequence rawPassword, String encodedPassword) { return encoder.matches(rawPassword, encodedPassword); }}

到此为止用户服务编写完毕,我们开始测试,打开 postman
工具先注册一个用户

图片 40image使用
Post
方式,输入
Token 令牌

编写个测试 Controller

@RestControllerpublic class DemoController { @RequestMapping public String hi(){ return "hi,你好"; } @RequestMapping @PreAuthorize("hasAuthority('ROLE_ADMIN')") public String hello(){ return "hello,你好"; } @RequestMapping("/getPrincipal") public OAuth2Authentication getPrinciple(OAuth2Authentication oauth2Authentication,Principal principal,Authentication authentication){ System.out.println("===================================="); System.out.println(oauth2Authentication); System.out.println(principal); System.out.println(authentication); System.out.println("===================================="); return oauth2Authentication; }}

我们直接访问

图片 41image

我们需要在请求头增加 Token

图片 42image

这样才可以正常访问

但是如果需要 admin
权限的,即使带上也是访问不了的我们可以测试
ROLE_ADMIN 权限

图片 43image

我们切换另外一个用户

图片 44image图片 45image

服务编写指南

项目结构初始化将完成基本配置的构建(开发人员了解相关知识即可,后续有时间再深入研究)

  • 配置服务器(configserver)
  • 服务注册器(discovery)
  • 熔断UI(monitor-dashboard)
  • 授权服务器 (auth-service)
  • 路由 (gateway)
  • mysql数据库 (mysql)
  • mongodb数据库(mongodb)

服务的创建过程

yun-cloud 项目属于maven项目。通过spring-init 创建一个spring-cloud 的pom
项目,
该项目作为根pom。微服务通过new->module->maven-next->service-name
进行maven子项目的构建,创建完成之后src和配置文件都没有,我们需要修改pom文件,完善pom信息,创建SpringApplication程序入口,创建bootstrap.yml
进行服务器配置文件的加载。步骤如图所示:

图片 46

Paste_Image.png

图片 47

Paste_Image.png

图片 48

Paste_Image.png

图片 49

Paste_Image.png

图片 50

Paste_Image.png

  • 完善pom信息

图片 51

Paste_Image.png

图片 52

Paste_Image.png

图片 53

Paste_Image.png

  • 创建程序入口

图片 54

QQ图片20170114011243.png

  • 创建 bootstrap.yml

图片 55

Paste_Image.png

  • 创建 服务.yml 比如 user-service.yml 放置到配置服务器(config)中。

图片 56

Paste_Image.png

tips: bootstrap.yml
为配置文件引导程序,application为应用配置,我们更倾向于把应用相关的配置到application.yml中,比如mysql
等。

配置服务器的连接

配置服务器有两种连接方式:

  • 通过指定配置进行连接

spring:
  application:
    name: statistics-service
  cloud:
    config:
      uri: http://config:8888
  • 通过eureka自动进行配置发现(自动发现的前提是config-server的ServerID
    = configserver,默认的配置服务器如果不填写application.name
    默认为configserver)

spring:
  cloud:
    config:
      discovery:
        enabled: true   

开发人员可以根据自己需求任选一种方式进行连接。

注册发现的配置

Eureka服务器已经启动,我们需要配置注册到服务器,并开启注册发现。

  • 引入依赖 (默认我们在根pom中已经全局引入了starter-eureka)
  • 通过注解进行注册和发现 ,注解有两种方式,一种是
    @EnableDiscoveryClient ,另一种是@EnableEurekaClient
    ,两者的区别是,discoveryClient实现了许多的服务发现方法,比如使用eureka,
    consul, zookeeper
    ,而eurekaClient则是netflix提供的。两者选择一个即可。
  • 修改配置bootstrap.yml ,指定注册服务器

eureka:
  client:
    serviceUrl:
      defaultZone: http://discovery:8761/eureka/

hystrix-dashboard 的配置

目前,我们将不配置hystrix-dashboard。后期我们通过引入依赖和稍许修改配置文件就可以实现该功能。详细配置可以参考官方教程或者是提供的sample样例。

授权服务器的配置和使用

待更新。。。

路由配置

我们使用Zuul 作为代理路由服务。

  • Zuul 为唯一对外暴露的端口.

mysql

mysql 我们使用tutumcloud/mysql 提供的镜像文件进行构建.可以cd 到mysql
执行docker build -t lvshangke/mysql .
进行手动镜像的构建,也可以通过compose.yml 指定build build ./mysql
,之后可以通过docker命令编译镜像,也可以通过docker-compose build
进行编译。

mongodb

mongodb 我们使用tutumcloud/mongodb
提供的镜像进行构建,使用版本3.2,具体用法跟mysql类似,可以参考文档进行配置也可以参考demo进行使用。

mybatis

我们在拆分微服务的时候,首先考虑的问题是服务的颗粒度,设计方向是,尽量的粗粒度的设计,避免事务的集群访问,关于涉及到事务的跨主机问题如果无法避免,则使用JMS实现消息队列的处理方案.

  • 服务的粒度直接影响到了服务之间调用复杂度
  • 我们习惯使用jpa处理表关系,之后就进行快速的开发迭代.但是在spring-cloud中,我们并不会首选使用jpa,考虑到JPA涉及到表关系处理,A业务处理用户基本CURD,B业务中处理用户银行卡的CURD,如果使用JPA,则A,B表具有重复的表关系配置,User,Bank.因为1个用户有多个银行卡.这样的业务逻辑设计就过分的细粒度.然而一旦使用JPA,无法避免的即使相互依赖,导致上述说的重复工作,所以我们建议不是必须使用,则首先考虑使用mybatis.
  • 在开发中,我们维护一套JPA,主要用户核心业务和数据库结构初始化工作(也可以不用)
  • 开发人员编写的小业务,复杂查询等,建议使用mybatis来完成数据库操作
    参考资料:
    mybatis,mybatis-spring-boot-autoconfigure

mybatis 依赖

我们选择1.2.0版本.

<dependency>
 <groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>

mybatis 配置

mybatis支持注解的配置方式,对于简单的服务可以使用该方式,但是我们更倾向于使用xml配置的方式,因为自由度更高,对于一些复杂查询,可以更加方便使用.两种方式都做一些介绍

  • 基于注解
    使用注解@Mapper.使用选择器 @Select
    demo

  • 基于xml配置文件
    需要配置mybatis.config-location
    demo

tips:
mybatis

feign

JMS

待更新。。。

docker

我们使用docker作为服务的测试和生产环境.

docker 开发环境

docker 作为开发环境的要求以及常见问题:

  • 所有的微服务创建 src/main/docker/Dockerfile 文件
  • 所有的微服务中pom 中需要增加maven-docker-plugin插件,并进行统一的配置
  • 项目根目录创建docker-compose.yml 文件,进行服务编写.

tips: 我们通过links
进行容器host关联,不过,由于容器启动顺序问题,导致有些服务器还需要重新启动才可以,通过使用version
2 中的depends_on 语法替代links,
虽然容器在初始化网络中可以按照顺序,不过,微服务中很多需要等待配置服务器启动之后才可以调用配置.容器起来了,应用还没有起来.通过compose.yml
启动以后,建议使用手动重新启动下,必须保证配置服务器在注册服务器上已经注册服务.
官方参考中给予了关于启动顺序的解决方案,depends_on,可以使用wait-for-it,因为我们基于服务自动发现,所以导致无法正确的监听,在上述服务器配置中,我们通过主动声明的方式显式的指定配置服务器,之后就可以通过wait-for-it
command: ["./wait-for-it.sh","configserver:8888","-t","0","--strict","--","java","-jar","/app/user-service-1.0.jar"]
启动监控,那么,当配置服务器启动之后将会触发微服务进行启动.在开发的过程中可以降低服务发现的时间,默认的每次心跳为30′,我们可以修改eureka.instance.lease-renewal-interval-in-seconds: 1
进行时间压缩,生产环境将还原该配置,使用系统默认的心跳时间

开发常用操作

  • 我们通过使用命令进行快速的编译,通过使用 mvn clean install
    进行镜像的快速构建
  • 镜像构建以后,我们通过docker-compose up -d进行启动
  • 常用的compose 一般有,stop.start,rm,up.
  • 对于局部改动的镜像,我们可以通过intellij maven工具进行可视化操作.
  • 开发中我们经常使用UI工具作为快速查看容器日志,CURD等操作.

docker 生产环境

调试环境

我们在开发微服务时候,将频繁的修改config,假如频繁的重新打包镜像,发布服务,这样下来无形中就耽误了很多时间,

  • 本地安装docker 环境
  • 项目初始化 ,执行mvn clean install
    docker-compose up -d,初始化环境包括,配置服务器,服务发现,路由,数据库等.
  • 创建自己的微服务模块,
    运行Application.因为所有的docker服务编排已经提供了基本的服务,并且对外提供了端口,默认开发环境中都映射到localhost,比如数据库等等.
    我们在开发测试过程中,可以直接运行自己的微服务,查看服务是否有问题,及时进行修改.
  • 编写application.yml
    放置在resources中,因为开发阶段需要频繁的修改该文件,一般情况下这个文件在配置服务器中,难免会造成时间的浪费,我们通过手动指定,待程序测试通过后,把该文件重命名为服务.yml,放置到config中.
  • 开发测试没有问题之后,把配置文件放置在配置仓库,docker-compose.yml
    中进行自己开发服务模块的编写.
  • 最后提交到git.

tips:在开发测试阶段,尽量的压缩
镜像构建的时间,避免其他服务的问题导致开发时间的延长,所以,建议大家编写服务前首先更新git,然后运行一次镜像,发布服务,
进行自己服务的开发工作,
对于原有服务的更新,我们建议注释服务编排中的该服务,本地启动待修改的程序进行开发测试,完成之后再进行修改以及代码提交.(对于服务依赖,我们也可以本地启动多个服务进行调试),通过本地运行可以避免构建,依赖造成的时间浪费.

安全机制

我们使用oauth2.0作为授权机制来完成集群的授权处理.具体的配置如下:

  1. 授权服务器
    1.1 引入依赖

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

1.2 Application
中使用@EnableGlobalMethodSecurity(prePostEnabled = true)
开启全局的方法鉴权,比如使用@PreAuthorize
1.3 编写授权服务器的安全验证配置

@Configuration
@EnableResourceServer
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().anyRequest().authenticated()
            .and()
            .csrf().disable()
            .httpBasic().disable().anonymous().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/info");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("123456").roles("USER").and()
                .withUser("admin").password("123456").roles("USER","ADMIN");
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

安全配置规则默认为所有的连接都需要授权.这里使用默认的方式采用内存存储token,初始化测试用户的时候也是通过配置,
我们在开发中通过注入service,实现自定义鉴权处理,主要涉及到的有
UserDetailsService,UserDetails.还需要初始化一个authenticationManagerBean
注意这个 @Bean

1.4 编写oauth2 配置

@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends AuthorizationServerConfigurerAdapter {

    private TokenStore tokenStore = new InMemoryTokenStore();

    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                    .withClient("mobile")
                    .authorizedGrantTypes("refresh_token", "password")
                    .scopes("api")
                .and()
                    .withClient("user-service")
                    .secret("123456")
                    .authorizedGrantTypes("client_credentials", "refresh_token")
                    .scopes("server");
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .tokenStore(tokenStore)
                .authenticationManager(authenticationManager);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }
}

oauth2的授权,需要有客户端链接完成请求,
这里分配了多个客户端链接,默认我们使用mobile作为移动端api的调用请求(外部),
其他客户端作为 服务之间进行鉴权的链接(内部).
外部的鉴权方式,我们采用password方式,方便api调用,内部采用client_credentials的方式进行鉴权.
token我们暂时使用内存token,后续再进行redis的迁移.
这里我们需要忽略的链接可以配置到config(WebSecurity web)中.

1.5 配置文件的修改

server:
  context-path: /uaa

默认所有的内部微服务都有context-path 假如外部方位 /uaa/test 通过zuul
代理之后,就到了
service/uaa/test.在进行spring-security开发的过程中,我们一般需要开启日志

logging:
  level:
    org.springframework.cloud: DEBUG
    org.springframework.security: DEBUG

还有在开发过程中,一旦开启了DEBUG,需要控制下eureka心跳时间,要不然日志特别多,不好定位问题.

1.6 配置zuul 代理
因为文档上面说application 加上@EnableOAuth2Sso @EnableZuulProxy
就可以开启token转发,我们就按要求配置下, 实际情况是 好像没什么用.
作为对外的zuul
,我们这里可以按照上面1.3的方式给他单独配置一个安全过滤器,不过作为对外提供的端口,我们这里不做安全控制,
所有的校验机制交给鉴权服务器去处理.

  routes:
    auth-service:
        path: /uaa/**
        url: http://localhost:8084

    account-service:
        path: /user/**
        serviceId: user-service

我们定义一个转发规则.
zuul这里我们就配置spring-security了,我们开放这个微服务,
允许访问的连接进行代理配置即可

1.7 微服务编写
微服务相当于一个小应用,
这里可以配置微服务自己的安全机制,参考上面的1.3.进行自己服务的连接处理,可以忽略一些连接,也可以验证一些连接.我们先看配置,具体的流程下面再整理
我们通过

@SpringBootApplication
@EnableOAuth2Client
@EnableEurekaClient
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UserApplication {

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

我们按照上面的配置方法,开启全局方法验证,开启Oauth2Client.
之后编写自己服务的安全控制.

@Configuration
@EnableResourceServer
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests().anyRequest().authenticated()
            .and()
            .csrf().disable()
            .httpBasic().disable();
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

这里测试使用全部需要授权才能访问. 假如用户已经获取到token,
访问该资源,该资源又要去鉴权服务器查看是否正确.
默认我们内部的访问为”server”通过客户端鉴权,查看token的合法性.
所以这里需要注入几个bean

@Bean
    @ConfigurationProperties(prefix = "security.oauth2.client")
    public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
        return new ClientCredentialsResourceDetails();
    }

    @Bean
    public RequestInterceptor oauth2FeignRequestInterceptor(){
        return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails());
    }

    @Bean
    public OAuth2RestTemplate clientCredentialsRestTemplate() {
        return new OAuth2RestTemplate(clientCredentialsResourceDetails());
    }

通过@Configuration进行注入.
1.8 微服务的配置

security:
  oauth2:
    client:
      clientId: user-service
      clientSecret: 123456
      accessTokenUri: http://localhost:8084/uaa/oauth/token
      grant-type: client_credentials
      scope: server
    resource:
      user-info-uri: http://localhost:8084/uaa/me

注意user-info-uri,当用户实际已经获取token,则访问该资源,该资源会去查看是否已经被鉴权服务器认证,
我们在auth-service 中需要创建一个controller

@RequestMapping("/me")
    public Principal getCurrentLoggedInUser(Principal user) {
        return user;
    }

1.9 使用
我们开放了zuul作为对外暴露的端口,
当用户通过zuul访问资源时候,每个资源都属于一个微服务,每个微服务都是一个spring项目,都有自己的一套验证机制,是否需要鉴权呀?
如果需要, 则用户没有权利就收到反馈结果, 用户需要申请一个token

申请token: localhost:8080/uaa/oauth/token
请求类型: POST
参数: username ,password, grant_type, scope(选填)
请求头: Authorization: Basic bW9iaWxlOg== (这里的Basic 后面跟的是mobile:的base64编码方式,代表请求的客户端为mobile,密码为空)

用户一旦申请了token 在遇到一些需要授权的连接的时候就可以携带该token

请求: localhost/user/
类型: GET
请求头: Authorization: Bearer $token

我们可以通过log日志查看整体的流程, 通过token访问zuul,
zuul转发到对应的微服务,是否有安全配置,
一层层过滤器下来,用户通过userinfouri访问鉴权服务器,(携带client:
user-service,password:,scope:) ,鉴权服务器查看client是否合法,完成鉴权.

jenkins自动化构建

监控系统