在本节中,我们将保护我们的 Spring Cloud 配置,并且为了简单起见,暂时将 Journaler API 从安全限制中释放出来。我们将演示保护微服务的基本原则,并逐步指导您实现这一目标。
我们的每个模块都必须支持 Spring Security 和 Spring 会话。为此,使用 Spring Security 和 Spring 会话支持依赖项扩展每个 build.gradle 配置:
...
dependencies {
compile 'org.springframework.boot:spring-boot-starter-security'
}
...
我们会将所有会话存储在内存中。为此,我们将使用 Redis 内存数据库。我们必须通过扩展具有以下依赖关系的 build.gradle 来扩展我们的每个应用程序以支持它:
...
dependencies {
compile 'org.springframework.boot:spring-boot-starter-data-redis'
}
...
现在,我们准备添加会话配置。在定义主应用程序类的同一包级别中添加会话配置类:
package com.journaler.api
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer
@EnableRedisHttpSession
class SessionConfiguration : AbstractHttpSessionApplicationInitializer()
对以下应用程序执行相同操作:发现、网关和 Journaler API。您将拥有三个具有相同实现的类!完成会话配置类后,通过添加以下代码扩展您在 Git 存储库中定义的应用程序配置(适用于所有三个应用程序):
spring.redis.host=localhost
spring.redis.port=6379
这将为每个应用程序提供正确的 Redis 配置。
对于 Configuration 应用程序,使用以下内容扩展其 application.properties:
eureka.client.region = default
eureka.client.registryFetchIntervalSeconds = 5
eureka.client.serviceUrl.defaultZone=
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/
security.user.name=configAdmin
security.user.password=configPassword12345
security.user.role=SYSTEM
spring.session.store-type=redis
通过这样做,我们将确保应用程序通过发现登录。我们也必须保护发现服务。使用如下定义的 WebSecurityConfiguration 类创建包安全性:
package com.journaler.discovery.security
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
@Configuration
@EnableWebSecurity
@Order(1)
class SecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth
.inMemoryAuthentication()
.withUser("discoveryAdmin")
.password("discoveryPassword12345")
.roles("SYSTEM")
}
override fun configure(http: HttpSecurity) {
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.and().requestMatchers().antMatchers("/eureka/**")
.and().authorizeRequests().antMatchers("/eureka/**")
.hasRole("SYSTEM").anyRequest().denyAll().and()
.httpBasic().and().csrf().disable()
}
}
这将匹配 Configuration 应用程序属性中的用户名和密码组合。我们必须注意,我们使用了 @Order 注释,以便我们可以告诉 Spring 将此配置用作其第一优先级。使用 sessionCreationPolicy() 方法和 ALWAYS 参数,我们定义会话将在每次用户登录尝试时创建。
我们要做的下一件事是告诉发现应用程序有关用于登录配置应用程序的凭据。扩展其 bootstrap.properties 配置,使其如下所示:
spring.cloud.config.name=discovery
spring.cloud.config.uri=http://localhost:9001
spring.cloud.config.username=configAdmin
spring.cloud.config.password=configPassword12345
最后,从我们的 Git 存储库中修改 discovery.properties 配置:
...
eureka.client.serviceUrl.defaultZone=
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
...
我们使用发现应用程序的凭据扩展了配置。
由于我们已经保护了配置和发现应用程序,是时候为我们的网关应用程序做同样的事情了。创建一个包含 WebSecurityConfiguration 类的 security 包:
package com.journaler.gateway.security
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Configuration
import org.springframework.core.annotation.Order
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
@Configuration
@EnableWebSecurity
@Order(1)
class SecurityConfig : WebSecurityConfigurerAdapter() {
@Autowired
fun configureGlobal(auth: AuthenticationManagerBuilder) {
auth
.inMemoryAuthentication()
.withUser("user")
.password("12345")
.roles("USER")
.and()
.withUser("admin")
.password("12345")
.roles("ADMIN")
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.antMatchers("/journaler/**")
.permitAll()
.antMatchers("/eureka/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.logout().permitAll()
.logoutSuccessUrl("/journaler/**").permitAll()
.and()
.csrf().disable()
}
}
我们定义了两个具有两个角色的用户:普通用户和管理员用户。我们还使用表单登录定义了安全过滤器。现在,切换到网关的会话配置类并更新它:
package com.journaler.gateway
import org.springframework.session.data.redis.RedisFlushMode
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer
@EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE)
class SessionConfiguration : AbstractHttpSessionApplicationInitializer()
会话上发生的任何更改都将立即保留!最后,我们将添加功能,以便我们可以在登录后转发身份验证令牌。创建一个名为 SessionFilter 的新类:
package com.journaler.gateway
import com.netflix.zuul.ZuulFilter
import com.netflix.zuul.context.RequestContext
import org.springframework.stereotype.Component
import org.springframework.session.SessionRepository
import org.springframework.beans.factory.annotation.Autowired
@Component
class SessionFilter : ZuulFilter() {
@Autowired
lateinit var repository: SessionRepository<*>
override fun shouldFilter(): Boolean {
return true
}
override fun run(): Any? {
val context = RequestContext.getCurrentContext()
val httpSession = context.request.session
val session = repository?.getSession(httpSession.id)
context.addZuulRequestHeader("Cookie", "SESSION=" + httpSession.id)
println("Session ID available: ${session.id}")
return null
}
override fun filterType(): String {
return "pre"
}
override fun filterOrder(): Int {
return 0
}
}
我们刚刚定义的过滤器将接受一个请求并将会话密钥作为 cookie 添加到请求的标头中。
在我们更新 Journaler API 应用程序之前,我们需要进行一些小的更新。更新 bootstrap.properties 配置:
spring.cloud.config.name=gateway
spring.cloud.config.discovery.service-id=config
spring.cloud.config.discovery.enabled=true
spring.cloud.config.username=configAdmin
spring.cloud.config.password=configPassword12345
eureka.client.serviceUrl.defaultZone=
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/
此外,在我们的 Git 存储库中更新 gateway.properties:
spring.application.name=gateway
server.port=9003
eureka.client.region = default
eureka.client.registryFetchIntervalSeconds = 5
management.security.sessions=always
spring.redis.host=localhost
spring.redis.port=6379
zuul.routes.journaler.path=/journaler/**
zuul.routes.journaler.sensitive-headers=Set-Cookie,Authorization
hystrix.command.journaler.execution.isolation.thread.timeoutInMilliseconds=600000
zuul.routes.discovery.path=/discovery/**
zuul.routes.discovery.sensitive-headers=Set-Cookie,Authorization
zuul.routes.discovery.url=http://localhost:9002
hystrix.command.discovery.execution.isolation.thread.timeoutInMilliseconds=600000
我们定义会话管理将始终生成会话。我们还移动了 Redis 的配置,以便在会话管理之后定义它。
我们可以将 gateway.properties 文件中的 serviceUrl.defaultZone 属性删除到我们的配置 Git 存储库中。该值在引导文件中重复。
提交所有具有配置更改的 Git 存储库,以便这些更改在我们的下一次运行中生效!
我们非常接近完成对 Journaler API 的最后润色!在我们这样做之前,让我们运行配置、发现和网关应用程序。为了能够运行它们,我们将安装 Redis 服务器。我们将在 macOS 上使用 Homebrew 进行安装过程。这非常简单易行。打开终端并执行以下命令:
$ brew install redis
一段时间后,将安装 Redis。现在,通过发出以下命令启动它:
$ redis-server
Redis 服务器将启动:
对于任何其他操作系统,请按照 Redis 官方主页的说明进行操作:https://redis.io/。
现在,一一运行配置、发现和网关应用程序。一切都应该正常工作!
从 Journaler API 应用程序中打开 WebSecurityConfiguration 类并进行更改。新的实现应该是这样的:
package com.journaler.api.security
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.core.annotation.Order
@Configuration
@EnableWebSecurity
@Order(1)
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.httpBasic().disable().authorizeRequests()
.antMatchers("/notes").permitAll()
.antMatchers("/notes/**").permitAll()
.antMatchers("/todos").permitAll()
.antMatchers("/todos/**").permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
}
}
请删除所有不再使用的与用户实体相关的类和其他与安全相关的类!
现在,更新 bootstrap.properties 配置:
spring.cloud.config.name=journaler
spring.cloud.config.discovery.service-id=config
spring.cloud.config.discovery.enabled=true
spring.cloud.config.username=configAdmin
spring.cloud.config.password=configPassword12345
eureka.client.serviceUrl.defaultZone=
http://discoveryAdmin:discoveryPassword12345@localhost:9002/eureka/
在我们的 Git 存储库中更新并提交 journaler.properties 文件:
...
management.security.sessions=never
...
我们还删除了 serviceUrl.defaultZone 属性,因为它已经在 boots.properties 配置中定义。
由于我们在开发过程中更新了我们的依赖关系,我们没有注意到我们现在遇到了请求类和 DTO 的问题。我们必须为每一个引入一个默认的空构造函数。让我们快速执行此操作:
data class NoteDTO(
var title: String,
var message: String,
var location: String = ""
) {
constructor() : this("", "", "")
...
}
data class TodoDTO(
var title: String,
var message: String,
var schedule: Long,
var location: String = ""
) {
...
constructor() : this("", "", -1, "")
}
package com.journaler.api.controller
data class NoteFindByTitleRequest(val title: String) {
constructor() : this("")
}
package com.journaler.api.controller
import java.util.*
data class TodoLaterThanRequest(val date: Date? = null) {
constructor() : this(null)
}
@Service("Todo service")
class TodoService {
...
fun getScheduledLaterThan(date: Date?): Iterable<TodoDTO> {
date?.let {
return repository.findScheduledLaterThan(date.time).map { it -> TodoDTO(it) }
}
return listOf()
}
}
如果您需要从数据库中删除所有表,请运行 Journaler API 应用程序。让我们尝试几个以网关应用程序为目标的调用,并确认所有调用都被重定向:
- [ PUT ] http://localhost:9003/journaler/notes: Execute the call a couple of times:
- [ GET ] http://localhost:9003/journaler/notes:
The GET API call for notes through the gateway application
- [ POST ] http://localhost:9003/journaler/notes/by_title with the title parameter "My 1st note":
- [ POST ] http://localhost:9003/journaler/notes/by_title with the title parameter "My 3rd note":
The POST API call for notes through the gateway application