使用JDBC后端存储
在前面的小节中,我们配置了一个身份验证服务器和客户端应用程序,它授予对资源服务器进行保护的资源的访问权限。但是,整个授权服务器配置已在内存中提供。这种解决方案在开发过程中满足了我们的需求,但在生产模式中却不是最理想的方法。目标解决方案应将所有身份验证凭据和令牌存储在数据库中。开发人员可以在Spring支持的许多关系数据库之间进行选择。在本示例中,我们决定使用MySQL。

因此,第一步就是在本地启动MySQL数据库。实现这一目标的最简便方式是通过Docker容器。除了启动数据库之外,以下命令还会创建一个模式(Schema) 和一个名为oauth2的用户。
一旦启动了MySQL,现在必须在客户端提供连接设置。如果开发人员是在Windows机器和端口33306.上运行Docker,则MySQL在主机地址192.168.99.100下可用。数据源属性应在auth service的application.yml文件中设置。Spring Boot还能够在应用程序启动时在所选数据源上运行一些SQL脚本。这对开发人员来说是一个好消息,因为我们必须在专用于OAuth2流程的架构上创建一些表。
spring:
application:
name: auth-service
datasource:
url: jdbc:mysq1://192.168. 99.100: 33306/oauth2?useSSL=false
username: oauth2
password: oauth2
driver-class-name: com .mysql. jdbc. Driver
schema: classpath:/script/schema. sql
data: classpath:/script/data.sql
已创建的模式包含一些用于存储OAuth2 凭据和令牌的表,如tokens. oauth client_details、oauth_ _client_ token、 oauth_ acess_ token、 oauth_ refresh_ _token. oauth code 和oauth_approvals。此外,在/src/main/esources/script/schema.sql 中还提供了带有SQL创建命令的完整脚本。

实际上,还有第二个SQL脚本(/src/main/resources/scriptdata.sql) ,它带有一些用于测试用途的insert命令。最重要的是添加一些客户端ID/客户端密钥对。

INSERT INTO” oauth client details“( client id,、client secret ,scope ,
authorized grant_ types
access_ token validity,
additional information ) VALUES (‘piotr 。minkowski’, ‘123456’, ” read’
‘ authorization code ,pa ssword, refresh token , implicit’, ‘900’,'{}’);
INSERT INTO、 oauth client details (client id,、client secret,scope” ,
“ authorized grant_ types。, access token validity’
additional information) VALUES (‘john. smith’,’123456’, ‘write’ ,
‘ authorization code,password, refresh token, implicit’,’900’,'(‘) ;
当前版本的身份验证服务器与基本示例中描述的版本之间存在一些实现上的差异。这里最重要的事情是将默认令牌存储设置为数据库,方法是提供一个以默认数据源作为参数的JdbcTokenStore bean。虽然所有令牌现在都存储在数据库中,但我们仍然希望以JWT格式生成它们。这就是为什么必须在该类中提供第二个bean JwtAccessTokenConverter的原因。通过覆盖从基类继承的不同configure方法,开发人员可以为OAuth2客户端详细信息设置默认存储,并将授权服务器配置为始终验证在HTTP标头中提交的API密钥。
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConf igurerAdapter {
@Autowired
private DataSource dataSource;
@Autowi red
private AuthenticationManager authenticat ionManager;
@override
public void configure (AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints。authenticationManager (this。authenticationManager )
.tokenStore (tokenStore())
.accessTokenConverter (accessTokenConverter () ) ;

}
@Override
public void configure (AuthorizationSe rverSecurityConfigurer
oauthServer) throws Exception {
oauthServer . checkTokenAccess (“permitAll ( )”);
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() (
return new JwtAcces sTokenConverter () ;
}
@Override
public void configure (ClientDetailsServiceConfigurer clients) throws
Exception {
clients.jdbc (dataSource) ;
}
@Bean
public JdbcTokenStore tokenStore ()
return new JdbcTokenStore (dataSource) ;
}
}
Spring应用程序可以提供自定义的身份验证机制。要在应用程序中使用它,必须实现UserDetaisService接口并覆盖其loadUserByUsermame方法。在我们的示例应用程序中,用户凭据和权限也存储在数据库中,因而需要将UserRepository bean 注入自定义UserDetailService类。
@Component (“userDetailsService”)
public class UserDetailsServiceImpl impl ements UserDetailsService
private final Logger log =
LoggerFactory. getLogger (UserDetailsServiceImpl.class);
@Autowi red
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername (final String login)
log . debug (“Authenticating (}”, login);
String lowercaseLogin – login. toLowerCase() ;
User userFromDatabase;
if (lowercaseLogin.contains(“Q”)) {
userFromDatabase = userRepository. findByEmail (lowercaseLogin) ;
else {
userFr omDatabase
userRepository. findByUsernameCaseInsensitive (lowercaseLogin) ;
if (userFromDatabase == null) {
throw new UsernameNotFoundException(“User”+ lowercaseLogin +
was not found in the database”) ;
else if (!userFromDatabase. isActivated( ) ) {
throw new UserNotActivatedException(“User”+ lowercaseLogin +
“is not activated”);
Collection<GrantedAuthority> grantedAuthorities = new
ArrayList< > ( ;
for (Authority authority : userFromDatabase . getAuthorities()) {
GrantedAuthority grantedAuthority = new
SimpleGrantedAuthority (authority . getName());
grantedAuthorities . add (grantedAuthority) ;
return new
org. spr ingf ramework. security. core.userdetails .User (userFromDatabase.
getUser name(), userFromDatabase .getPassword(), grantedAuthorities) ;
}
}
服务间授权
使用Feign客户端即可实现本示例中的服务间通信。以下是我们所选的实现之一(这里选择的是本示例中的order-service服务),它将调用来自customer-service服务的端点。
@FeignClient (name – “customer-service”)
public interface CustomerClient {
@GetMapping (” /withAccounts/ {customerId}”)
Customer findByIdWithAccounts (0PathVariable (“customerId”) Long
customerId) ;
}
与其他服务一样, 来自customer service服务的所有可用方法都受到基于OAuth令牌范围的预授权机制的保护。它允许开发人员使用@PrcAuthorize注解每个方法,定义所需的范围。
@PreAuthorize (” #oauth2.hasScope ‘write’)”)
@PutMapping
public Customer update (@RequestBody Customer customer)
return repository. update (customer) ;
}
@PreAuthorize (” #oauth2.hasScope (‘read’)”)
@GetMapping (”/withAccounts/{id}”)
public Customer findByIdWi thAccounts (@PathVariable(“id”) Long id) throws .
JsonProcessingException {
List<Account> accounts = accountClient. findByCustomer (id);
LOGGER. info (“Accounts found: { }”,mapper。wri teValueAsString (accounts)) ;
Customer c = repository.findById(id) ;
c.setAccounts (accounts) ;
return C;
}
默认情况下预授权机制是被禁用的。要为API方法启用它,应该使用@EnableGlobalMethodSecurity批注。开发人员还应该指出这样的预授权将基于OAuth2令牌范围。
@Configuration
@EnableResourceServer
@EnableGloba lMethodSecurity (prePostEnabled = true)
public class OAuth2ResourceServerConfig extends
GlobalMethodSecurityConfiguration (
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
return new OAuth2MethodSecurityExpressionHandler( );
}
}
如果通过Feign客户端调用account-service服务端点,则会出现以下异常。
feign.FeignException: status 401 reading
Cus tomerClient# findByIdWi thAccounts ( ) ;
content: {“error” :”unauthorized”, “error_ description”:
“Full authentication is required to access this resource” }
为什么会出现这种异常呢?当然,customer service服务受OAuth2令牌授权保护,但Feign客户端不会在请求标头中发送授权令牌。可以通过为Feign客户端定义自定义配置类来自定义该方法。它允许开发人员声明一个请求拦截器。在这种情况下,可以使用来自Spring Cloud OAuth2库的OAuth2FeignRequestInterceptor提供的OAuth2实现。出于测试目的,笔者决定使用资源所有者密码授予类型。
public class Cus tomerclientConfiguration {
@Value (“${security.oauth2. client.access-token-uri} ” )
private String accessTokenUri;
@Value (“$ (security. oauth2.client.client-id}”)
private String clientId;
@value (“$ {security. oauth2.client .client-secret }”)
private String clientSecret;
@Value (“‘ (security. oauth2.client.scope }”)
private String scope;
@Bean
RequestInterceptor oauth2FeignRequestInterceptor( ) (
return new OAuth2Fe ignRequest Interceptor (new
Defaul tOAuth2ClientContext(), resource( ) ) ;
}
@Bean
Logger . Level feignLoggerLevel( ) {
return Loaaer .Level. FULL;
}
private OAuth2ProtectedResourceDetails resource ( ) {
ResourceOwnerPa sswordRe sourceDetails resourceDetails = new
ResourceOwne rPasswordResourceDetails( ) ;
resourceDetails. setUsername (“root”) ;
resourceDetails. setPassword (“password”) ;
resourceDetails。setAcces sTokenUri (accessTokenUri) :
resourceDetails. setClientId(clientId) ;
resourceDetails. setClientSecret (clientSecret) ;
resourceDetails . setGrantType (“password”) ;
resourceDetails. setScope (Arrays.asList (scope) );
return resourceDetails;
}
}
最后,我们可以测试已经实现的解决方案。这一次,我们将创建一个JUnit自动化测试,而不是在Web浏览器中单击它或使用其他工具发送请求。测试方法显示在以下代码段中。开发人员可以使用OAuth2RestTemplate和Resource Owner Password Resource Details来执行资源所有者凭据授权操作,并使用请求标头中发送的OAuth2令牌从order-service服务调用POST/API方法。当然,在运行该测试之前,必须启动所有微服务,以及发现和授权服务器。
@Test
public void testClient( ) {
ResourceOwne rPas swordResourceDetails resourceDetails = new
ResourceOwnerPasswordResourceDetails();
resourceDetails. setUsername (“root”) ;
resourceDetails. setPassword(“password”) ;
resourceDetails . setAccessTokenUri (” 999/0auth/token”);
resourceDetails. setClientId(“piotr .minkowski”);
resourceDetails . setClientSecret (“123456”);
resourceDetails. setGrantType (“password”) ;
resourceDetails.setscope (Arrays.asList (“read”)) ;
DefaultOAuth2ClientContext clientContext = new
DefaultOAuth2ClientContext() ;
OAuth2RestTemplate restTemplate = new
OAuth2RestTemplate (resourceDetails, clientContext) ;
restTemplate . setMessageConverters (Arrays.asList (new
MappingJackson2HttpMessageConverter( ) ) );
Random r – new Random( ) ;
Order order = new Order() ;
order. setCustomerId( (long) r.nextInt(3) + 1) :
order. setProductIds (Arrays.asList (new Long[] {
(1ong) r.nextInt(10) + 1, (long) r.nextInt(10) + 1 })) ;
order = restTemplate.postForobject (“#34;, order,
Order.class) ;
if (order.getStatus() != Orderstatus . REJECTED) {
restTemplate .put (“}”, null,
order .getId( ) ) ;
}
}
在API网关上启用SSO
开发人员可以通过使用@EnableOAuth2Sso注解main类来启用API网关上的单点登录功能。实际上,这是微服务架构的最佳选择,它可以强制使Zuul为当前经过身份验证的用户生成或获取访问令牌。
@SpringBootApplication
@EnableOAuth2Sso
@EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
new
SpringApplicat ionBuilder (GatewayApplication.class) .web(true)。run(args);
}
}
通过包含@EnableOAuth2Sso,可以触发ZuulFilter 的自动配置。过滤器负责从当前经过身份验证的用户提取访问令牌,然后将其放入请求标头中,并转发到隐藏在网关后面的微服务中。如果为这些服务激活了@EnableResourceServer,它们将在Authorization HTTP标头中接收预期的令牌。可以通过声明proxy auth.*属性来控制@EnableZuulProxy下游的授权行为。

在架构中使用网关时,可能会隐藏其后面的授权服务器。在这种情况下,应该在Zuul的配置设置中提供其他路由,如uaa。然后,OAuth2 客户端和服务器之间交换的所有消息都将通过网关。以下是网关的application.yml文件中的正确配置。
security:
oauth2 :
client :
accessTokenUri: /uaa/oauth/token
use rAuthorizationuri: /uaa/oauth/authorize
clientId: piotr 。minkowski
clientSecret: 123456
resource:
userInfoUri:
zuul :
routes :
account-service:
path: /account/**
customer- service:
path: /customer/*
order -service :
path: /order/**
product-service:
path: /product/**
uaa:
sensitiveHeaders :
path: /uaa/**
url:
add-proxy-headers: true
小结
本章是专门开辟的一个关于安全主题的章节,以按步骤详细介绍如何保护基于微服务的架构的关键元素。与安全相关的主题通常比其他主题更高级,因此,笔者花了一些时间来解释该领域的若干基本概念。本章通过示例说明了双向SSL身份验证、敏感数据的加密/解密、Spring Security 身份验证以及使用JWT令牌的OAuth2授权。至于在架构中应该使用哪些组件才能提供所需的安全级别,将由开发人员自己来决定。
在阅读完本章之后,开发人员应该能够为应用程序设置基本和更高级的安全配置。此外,还应该能够保护系统架构的每个组件。当然,本章只讨论了一些可能的解决方案和框架。例如,不必仅依赖Spring 作为授权服务器提供程序,我们也可能会使用第三方工具,如Keycloak,它可以充当基于微服务的系统中的授权和身份验证服务器。它还可以很轻松地与Spring Boot应用程序集成。它支持所有最流行的协议,如OAuth2、Openld Connect和SAML。事实上,Keycloak 是一个非常强大的工具,应该被视为Spring授权服务器的替代品,特别是对于大型企业系统和其他更高级的用例而言更是如此。
本文给大家讲解的内容是使用JDBC后端存储
- 下篇文章给大家讲解的是 微服务测试的不同策略之测试Java微服务 ;
- 觉得文章不错的朋友可以转发此文关注小编,有需要的可以私信小编获取资料;
- 感谢大家的支持!