Spring Cloud入门教程系列:
- Spring Cloud入门教程(一):服务治理(Eureka)
- Spring Cloud入门教程(二):客户端负载均衡(Ribbon)
- Spring Cloud入门教程(三):声明式服务调用(Feign)
- Spring Cloud入门教程(四):微服务容错保护(Hystrix)
- Spring Cloud入门教程(五):API服务网关(Zuul) 上
- Spring Cloud入门教程(六):API服务网关(Zuul) 下
- Spring Cloud入门教程(七):分布式链路跟踪(Sleuth)
- Spring Cloud入门教程(八):统一配置中心(Config)
- Spring Cloud入门教程(九):基于消息驱动开发(Stream)
- Spring Cloud入门教程(十):消息总线(Bus)
本人和同事撰写的《Spring Cloud微服务架构开发实战》一书也在京东、当当等书店上架,大家可以点击这里前往购买,多谢大家支持和捧场!
安全,几乎在任何应用开发中都是绕不过去的一个基础功能。当我们将应用转移到微服务架构时,安全将会更加复杂。在2016年David Borsos在伦敦的微服务大会上提出了以下四种方案:
单点登录(SSO): 每个微服务都需要和认证服务交互,但这将产生大量非常琐碎的网络流量和重复的工作,当动在应用中存在数十个或更多微服务时,该方案的弊端就非常明显;
分布式会话(Session)方案: 该方案将用户认证的信息存储在共享存储中(如:Redis),并使用用户会话的ID作为key来实现的简单分布式哈希映射。当用户访问微服务时,可以通过会话的ID从从共享存储中获取用户认证信息。该方案在大部分时候非常不错,但其主要缺点在于共享存储需要一定保护机制,此时相应的实现就会相对复杂;
客户端令牌(Token)方案: 令牌在客户端生成,并由认证服务器进行签名,令牌中包含足够的信息,以便各微服务可以使用。令牌会附加到每个请求上,为微服务提供用户身份验证。该解决方案的安全性相对较好,但由于令牌由客户端生成并保存,因此身份验证注销非常麻烦,一个折衷解决方案就是通过短期令牌和频繁检查认证服务来验证令牌是否有效等。对于客户端令牌JSON Web Tokens(JWT)是一个非常好的选择;
客户端令牌与API网关结合: 使用该方案意味着所有请求都通过网关,从而有效地隐藏了微服务。在请求时,网关将原始用户令牌转换为内部会话。这样也就可以网关对令牌进行注销,从而解决上一种方案存在的问题。
在本文中我们将着重介绍基于令牌的解决方案,而基于令牌的解决方案最好的选择就是OAuth2.0。
1. OAuth2.0
关于OAuth2.0在维基百科中描述如下:
开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
OAuth 2.0是OAuth协议的下一版本,但不向下兼容OAuth 1.0。OAuth 2.0关注客户端开发者的简易性,同时为Web应用,桌面应用和手机,和起居室设备提供专门的认证流程。
对于让我们首先了解一下OAuth2.0中的几个关键术语:
- Resource Owner: 资源所有者,我们可以直接理解为:用户(User);
- User Agent: 用户代理,对于Web应用可以直接理解为浏览器;
- Authorization server: 认证服务器,即提供用户认证和授权的服务器,可以是独立服务器;
- Resource server: 资源服务器,这里我们可以理解为需要保护的微服务。
然后,让我们看一下OAuth2.0的认证流程图(摘自RFC6749):
认证流程步骤如下:
- (A)用户打开客户端以后,客户端请求用户给予授权;
- (B)用户同意授权给客户端;
- (C)客户端使用上一步获得的授权,向认证服务器申请令牌;
- (D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌;
- (E)客户端使用令牌,向资源服务器申请获取资源;
- (F)资源服务器确认令牌无误,同意向客户端开放资源。
从流程上可以得知客户端必须在得到用户授权后才能够从认证服务中获取到令牌。OAuth2.0针对客户端授权提供了下面四种授权方式:
- 授权码模式(authorization code): 该种模式是功能最完整、流程最严密的授权模式;
- 简化模式(implicit): 该模式不需要通过第三方应用程序的服务器,跳过了"授权码"这个步骤,直接在浏览器中向认证服务器申请令牌,因此称为简化模式;
- 密码模式(password): 用户向客户端提供自己的用户名和密码,客户端通过这些信息直接向认证服务器获取授权;
- 客户端模式(client credentials): 指客户端以自己的名义,而不是以用户的名义向认证服务器获取认证,这种方式下认证服务器会将客户端作为一个用户来对待。
2. 实现微服务安全
下面让我们着手来实现示例项目的安全管控。
2.1 搭建认证服务器
首先,我们会搭建一个认证服务器(Auth Server)。该服务器也是一个标准的Spring Boot框架应用。
2.1.1 编写Maven文件
<?xml version="1.0" encoding="UTF-8"?>
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>twostepsfromjava.cloud</groupId>
<artifactId>twostepsfromjava-cloud-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../parent</relativePath>
</parent>
<artifactId>auth-server</artifactId>
<name>MS Blog Projects(Security): Auth Server</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在该配置文件中我们需要引入spring-cloud-security
和spring-security-oauth2
依赖。
2.1.2 实现客户端管理
对于OAuth2.0应用来说需要实现一个客户端认证管理,这里我们直接继承AuthorizationServerConfigurerAdapter
,并通过内存管理的方式增加了一个客户端应用,代码如下:
package io.twostepsfromjava.cloud.auth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
/**
* OAuth2Server 配置
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@Configuration
public class OAuthConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("malldemo")
.secret("pgDBd99tOX8d")
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit", "password", "client_credentials")
.scopes("webmall");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
客户端ID我们设置为malldemo
,secret则设置为:pgDBd99tOX8d
,同时我们还为客户端授权了authorization_code
, refresh_token
, implicit
, password
, client_credentials
认证模式。并且在接下来的示例中我们会使用授权码模式和密码模式来进行测试。
2.1.3 实现用户认证和授权的管理
对于使用过Spring Security的同学来说对Security中用户认证和授权的管理应该不会陌生,这里也不再细讲,不熟悉的同学可以自行搜索来了解。示例中代码如下:
package io.twostepsfromjava.cloud.auth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* OAuth2 安全配置
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@Configuration
@Order(org.springframework.boot.autoconfigure.security.SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class OAuthWebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
@Override
@Bean
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user001")
.password("pwd001")
.roles("USER")
.and()
.withUser("admin")
.password("pwdAdmin")
.roles("USER", "ADMIN");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
}
在上面的代码中,我们依然通过内存方式进行管理,并创建了两个用户:
- user001: 是一个普通用户,只有
USER
角色; - admin: 是一个管理员用户,拥有
USER
和ADMIN
角色。
在上面的代码中我们还指定了所有访问都需要认真,并且开启了httpBasic认证,通过这个当一个未认证用户访问时就可以通过浏览器弹出一个认证对话框,可以让用户输入用户名和密码进行认证。
2.1.4 实现应用引导类
对于我们所要实现的认证服务器来说最重要的就是需要在应用引导类中增加@EnableAuthorizationServer
注解,通过该注解就可以启动Spring Cloud Security,并且为我们提供一系列端点,从而实现OAuth2.0的认证。这些端点分别为:
-
/oauth/authorize
: 授权端点; -
/oauth/token
: 获取访问令牌端点; -
/oauth/confirm_access
: 用户确认授权提交端点; -
/oauth/error
: 认证服务器错误信息获取端点; -
/oauth/check_token
: 用于资源服务访问的令牌解析端点; -
/oauth/token_key
: 如果使用JWT令牌,则公开用于令牌验证的公钥。
2.1.5 实现用户信息加载端点
对于认证服务来说我们还需要提供一个用户信息加载端点,这样其它微服务就可以使用令牌从认证服务器获取认证用户的信息,从而能够实现用户认证及鉴权处理。具体实现代码如下:
package io.twostepsfromjava.cloud.auth.api;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 获取当前认证用户的信息
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@RestController
public class AuthEndpoint {
protected Logger logger = LoggerFactory.getLogger(AuthEndpoint.class);
@RequestMapping(value = { "/auth/user" }, produces = "application/json")
public Map<String, Object> user(OAuth2Authentication user) {
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet( user.getUserAuthentication().getAuthorities()));
return userInfo;
}
}
该段代码将从Spring Security中获取到当前用户信息,并转化成一个Map对象返回。
2.1.6 编写配置文件
# 定义应用端口
server.port=8290
# 定义应用名称
spring.application.name=authserver
logging.level.org.springframework=INFO
logging.level.io.twostepsfromjava=DEBUG
2.2 完善用户微服务
接下来我们将对用户微服务进行修改,增加安全处理功能。
2.2.1 引入Security依赖
对于用户微服务来说是一个Resource server,也就是资源服务器,当访问到某些资源时需要进行用户认证及鉴权,因此需要引入对Spring Cloud Security的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</dependency>
2.2.2 完善应用引导类修改
在Spring Cloud Security中我们只需要对需要进行安全管理的应用增加@EnableResourceServer
注解来开启安全管控:
package io.twostepsfromjava.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
/**
* TwoStepsFromJava Cloud -- User Service 服务器
*
* @author CD826(CD826Dong@gmail.com)
* @since 1.0.0
*/
@EnableDiscoveryClient
@EnableResourceServer
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
当引入了spring-cloud-security
并在应用引导类中增加了@EnableResourceServer
注解时,应用就会开启默认的安全管控处理,如果你想在自己的应用中增加更细的安全管控,那么需要继承WebSecurityConfigurerAdapter
并实现安全相关配置,这个在这里就不细讲了。
2.2.3 完善应用配置
我们需要在应用配置文件中增加获取认证用户信息的端点,具体配置如下:
# 保持原来的配置不变
# OAuth2
security.oauth2.resource.user-info-uri=http://localhost:8290/auth/user
所配置的端点也就是之前我们在认证服务器所实现的获取当前登录用户信息的端点。
2.2.4 增加获取当前用户信息的端点
为了进行测试,我们需要在用户微服务中增加一个获取当前已登录用户信息的端点,代码如下:
@RequestMapping(value = "/my", method = RequestMethod.GET)
public UserDto myDetail() {
Map curUser = (Map) SecurityContextHolder.getContext()
.getAuthentication()
.getPrincipal();
String userName = (String)curUser.get("username");
return new UserDto(userName, userName, "/avatar/default.png", "");
}
这里的代码非常简单,就是直接从SecurityContextHolder
中获取到当前已登录用户的信息,并构造成一个UserDto
,然后返回。
Ok,到此用户微服务的改造就完成了。如果你现在启动用户微服务并进行访问上面所提供的端点,就会看到如下返回:
也就是说,现在需要权限才可以访问该端点。那么我们如何提供权限呢?
之前在讲OAuth2.0的时候我们提到客户端授权模式有四种,那么我们来看看如何使用这些授权模式来实现具体的用户认证处理。
2.3 通过授权码模式(authorization code)访问
首先,让我们来看看如何通过OAuth2.0所提供的最全面的授权流程授权码模式来实现用户认证。
我们需要依次启动:服务治理服务器(Eureak Server)、认证服务器(Auth Server)和用户微服务。
第一步,我们首先来构造获取访问令牌的Url:
http://localhost:8290/oauth/authorize?response_type=code&client_id=malldemo&redirect_uri=http://localhost:8260&scope=webmall&state=63879
对于该Url说明如下:
-
/oauth/authorize
: 这个是获取授权码的端点; -
client_id
: 客户端的ID,在请求的Url中必选包含。请注意一下我们这里给的值为:malldemo
,这个就是我们在认证服务器中配置客户端列表是所配置的; -
response_type
: 表示授权类型,也是必选的,对于授权码模式,这里固定填写为:code
; -
scope
: 表示申请的权限范围,可以不填。这个是指当有不同的客户端时所授权是否复用; -
redirect_uri
: 授权成功后重定向到的URI; -
state
: 表示客户端的当前状态,可以指定任意值,不论授权是否成功认证服务器都会原封不动地返回这个值。
我们,接下来可以直接在浏览器中请求这个地址,然后返回的页面如下:
这个一个浏览器自身的用户登录窗口。因为,我们没有登录过,所以认证服务器通过httpBasic
开启浏览器登录模式,这样当用户尚未认证时就会弹出如上的登录窗口。
我们在登录窗口中输入user001
和pwd001
也就是之前认证服务器中所配置的用户列表中的一个,然后就会跳转到如下页面:
这里就是用户是否授权的界面。在该界面中我们可以看到所传入的客户端应用的ID和授权范围都会显示出来。这里我们点击【Approve】,然后浏览器就会跳转到redirect_uri
所指定的地址,这时候仔细观察所跳转回来的地址,如下:
http://localhost:8260/?code=tn8n8F&state=63879
可以发现在地址包含了两个参数:
-
code
: 为认证服务所发放的授权码。该码的有效期很短,默认为10分钟,并且客户端只能使用该码一次,否则会被认证服务器拒绝。还需要说明一点就是该码与客户端ID及重定向URI,是一一对应关系; -
state
: 这个就是上面我们请求时所传入的参数,认证服务器会原封不动的返回来。
Ok,第一步我们已经获取到了授权码。那么第二步就是根据这个授权码来获取访问令牌,在获取访问令牌时我们使用Postman来进行,界面截图如下:
这该截图中我们需要填写以下参数:
-
/oauth/token
: 这个是获取访问令牌的端点; -
grant_type
: 表示使用的授权模式,必须填写,对于授权码模式值固定为"authorization_code"; -
code
: 也就是上一步我们所获得的授权码; -
redirect_uri
: 获取成功后重定向到的URI,同样我们需要设置为之前所给的地址; -
client_id
: 客户端ID,必须填写,而且需要与之前保持一致。
此外,在上面的截图中我们还需要填写授权认证信息,这个因为我们认证服务器开启了权限验证,这里填写客户端的ID和secret即可。此时服务器就会把客户端作为一个用户来对待,从而能够访问/oauth/token
端点。
P.S. 这里这么做主要时简化测试方法,使得我们上一步访问时可以弹出认证对话框。但实际生产使用时不应这么做,需要自己定义用户登录页面、授权页面。
上面的请求,我们可以获得如下返回:
返回的内容如下:
-
access_token
: 这个是所获取访问令牌; -
token_type
: 令牌类型,Spring Cloud OAuth默认返回的值为bearer
; -
expires_in
: 表示令牌过期时间,单位为秒,默认为12小时; -
refresh_token
: 更新令牌,过期后可以用来获取下一次的访问令牌; -
scope
: 权限范围,一般与客户端申请范围一致。
有了访问令牌下一步我就可以访问用户微服务了,访问方式如下:
访问时我们需要在Header中指定Authorization
,并且将值设置为上一步所获取到的访问令牌即可,这样我们就可以得到正确的数据返回了,如上图所示。
这样我们就完成了通过授权码模式实现用户认证的测试。
2.4 通过密码模式(password)访问
如果认证服务器是第三方的,那么使用上面的流程问题不大。如果认证服务器是我们自己搭建的,比如该示例,那么上面的授权显有点复杂。接下来让我们看看如何使用密码模式来简化。
我们按照下图方式来直接请求认证服务器获取访问令牌:
在上图中我们需要同时设置客户端的ID、secret及访问用户的用户名和密码,并将grant_type
设置为password
,这样就可以直接获取到访问令牌,如下图所示:
然后,我们根据所获取到的访问令牌再次访问用户微服务,可以获取如下界面:
这说明,用户认证也是成功的。
可见,通过密码模式可以大大简化用户认证流程,但是需要用户信任客户端的情况下才会提供,因此这种方式适合客户端与认证服务器是同一个应用的情况下。
对于其它客户端授权模式这里就不再一一进行测试了。本文中所提到的示例你可以在这里下载。