目录
Spring Cloud Security + OAuth 2.0 + JWT的应用
应用访问安全性基本都是围绕认证(Authentication)和授权(Authorization)两大核心概念。
首先确定用户身份(对用户进行认证),确认身份后再确定用户是否有访问指定资源的权限,即身份认证是验证身份的过程,而授权是验证是否有权访问的过程。
举个例子,坐飞机登记之前需要身份证和机票:身份证是为了证明张三确实是张三,这就是Authentication;机票是为了证明张三确实买了票,可以上飞机,这就是Authorization。
主流认证的解决方案是OAuth 2.0,授权的解决方案有Spring Security和Shiro。在公司里在不同的项目里都有使用过。
OAuth 2.0 是什么
OAuth 2.0 的标准是 RFC 6749 文件,是一个开放的、安全的用户认证协议,通过认证用户身份并颁发token(令牌),使得第三方应用可以在限定时间、限定范围使用该令牌访问指定资源。
OAuth 2.0 协议流程
(1)用户打开客户端以后,客户端要求用户给予授权。
(2)用户同意给予客户端授权。
(3)客户端使用上一步获得的授权,向认证服务器申请令牌。
(4)认证服务器对客户端进行认证以,确认无误后同意发放令牌。
(5)客户端使用令牌,向资源服务器申请获取资源。
(6)资源服务器确认令牌无误,同意向客户端开放资源。
从OAuth 2.0 授权流程可以看出,授权涉及4种角色:
- 客户端/第三方应用(Client / Third-party Application):客户端/第三方应用代表资源所有者对资源服务器发出访问受保护资源的请求。
- 资源拥有者(Resource Owner):资源拥有者是对资源具有授权能力的人,通常也就是我们所说的用户。
- 授权服务器(Authorization Server):就是通常所说的认证服务器,为客户端应用程序提供不同的访问令牌。授权服务器可以和资源服务器在统一服务器上,也可以独立部署。
- 资源服务器(Resource Server):资源所在的服务器,也就是受安全认证保护的资源。
OAuth 2.0 的4种授权方式
从OAuth 2.0 授权流程可以看出,客户端(第三方应用)需要得到用户的授权(Authorization Grant)才能获得令牌(Access Token)。
OAuth 2.0 定义了4种授权方式:授权码模式(Authorization Code)、简化模式(Implicit)、密码模式(Resource Owner Password Credentials)和客户端模式(Client Credentials)。
注意:不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
凭证式(client credentials),
适用于没有前端的命令行应用,即在命令行下请求令牌。这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
第一步,A 应用在命令行向 B 发出请求。grant_type
参数等于client_credentials
表示采用凭证式,client_id
和client_secret
用来让 B 确认 A 的身份。第二步,B 网站验证通过以后,直接返回令牌。
eg: https://xxx/auth-server/oauth/token?grant-type=client-credentials
授权码模式(Authorization Code)
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
https://b.com/oauth/authorize? response_type=code& client_id=CLIENT_ID& redirect_uri=CALLBACK_URL& scope=read
上面 URL 中,response_type
参数表示要求返回授权码(code
),client_id
参数让 B 知道是谁在请求,redirect_uri
参数是 B 接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围(这里是只读)。
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri
参数指定的网址。跳转时,会传回一个授权码
第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
https://b.com/oauth/token? client_id=CLIENT_ID& client_secret=CLIENT_SECRET& grant_type=authorization_code& code=AUTHORIZATION_CODE& redirect_uri=CALLBACK_URL
上面 URL 中,client_id
参数和client_secret
参数用来让 B 确认 A 的身份(client_secret
参数是保密的,因此只能在后端发请求),grant_type
参数的值是AUTHORIZATION_CODE
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址。
第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri
指定的网址,发送一段 JSON 数据。
{ "access_token": "ACCESS_TOKEN", "token_type": "bearer", "expires_in": 2592000, "refresh_token": "REFRESH_TOKEN", "scope": "read", "uid": 100101, "info": {...} }
上面 JSON 数据中,access_token
字段就是令牌,A 网站在后端拿到了。
其他模式:
简化模式(Implicit)、密码模式(Resource Owner Password Credentials)很少用,此处忽略。
JWT(JSON Web Token)
JWT是什么?
JWT是一个开放标准RFC7519,其代表的是一种紧凑的、URL安全的、能够在网络应用间传输的声明。JWT 由 header.payload.signature 三段信息构成,“.” 链接成字符串。可使用在线校验工具(https://jwt.io)将 token 编/解码。
header 头部描述了该JWT的最基本信息,如类型、签名算法等:
(1)令牌类型 typ:JWT
(2)加密算法 alg:JWT签名默认的算法为HMAC SHA256,即HS256。
payload 载荷(是JWT主体)存放了令牌有效信息:
标准中注册的声明(Registered Claims)是对令牌中的一些标准属性信息进行声明,标准规定是建议,不强制。
常用的属性信息有:
- iss:令牌的签发者;
- sub:令牌所面向的用户;
- aud:接收令牌的一方;
- iat:令牌签发时间;
- exp:令牌过期时间;
- nbf:定义令牌有效起始时间,在该时间之前是不可用的;
- jti:令牌唯一身份标识,主要用来作为一次性令牌,避免重放攻击。
signature:
将头部和载荷使用Base 64编码后,通过所使用的加密方法进行签名,签名后的结果放在这部分内容中。例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在传递过程中有没有被更改,并且对于使用私钥签名的Token还可以验证JWT的发送方是否为它所称的发送方。secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和验证的,所以secret是服务器端的私钥,在任何场景都不应该泄露出去。
JWT解决了什么问题?
OAuth 2.0最大的痛点是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,以验证Token的有效性、获取Token对应的用户信息。在分布式架构下,如果有大量相关请求,处理效率是很低的,并且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求。JWT就是为解决这些问题而诞生的。普通的OAuth 2.0颁发的是一串随机hash字符串,本身无意义,而JWT格式的Token是有特定含义的。
JWT相对于传统的Token来说,解决了两个痛点:
- 通过验证签名,Token的验证可以直接在本地完成,不需要连接认证服务器。
- 在Payload中可以定义用户相关信息,这样就轻松实现了Token和用户信息的绑定。
认证时,当用户用他们的凭证成功登录以后,一个JSON Web Token就会被返回。此后,Token就是用户凭证了,用户想要访问受保护的路由或者资源,用户代理(通常是浏览器)都应该带上JWT,通常用Bearer schema放在Authorization header中,例如:
‘Authorization': 'Bearer ’ + token
Spring Security + OAuth 2.0 + JWT的应用
Spring Security 除了用户认证,访问授权,还提供了加解密,CSRF保护、CORS、方法级安全访问、单点登录等核心功能。
spring-cloud-starter-oauth2 依赖整合了 spring-cloud-starter-security、spring-security-oauth2、spring-security-jwt 这3个依赖。
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Edgware.SR3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
配置认证服务器:@EnableAuthorizationServer 并扩展AuthorizationServerConfigurerAdapter类
AuthorizationServerConfigurerAdapter 提供了以下3个重载的configure方法:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception{
// 用于配置令牌端点(Token Endpoint)的安全约束
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 用来配置及初始化客户端详情服务(ClientDetailService)
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 用于配置授权以及Token的访问端点和Token服务,以及Token的存储方式(eg:inMemory)
}
Spring Security OAuth 2.0 公开两个端点用于检查 token 和获取 token:/oauth/check_token,/oauth/token_key,这些端点默认受保护 denyAll()。
配置资源服务器:@EnableResourceServer 并扩展 ResourceServerConfigurerAdapter 类
@Override
protected void configure(HttpSecurity http) throws Exception{
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
}
默认情况下,Token只会有用户名等基本信息,实际应用中需要把关于用户的更多信息返回给客户端,通过自定义Token增强器来丰富Token内容。
如何自定义Token增强器?
Spring Security 自带 TokenEnhancer 接口:
public interface TokenEnhancer {
/**
* 增强令牌
*
* @param accessToken 当前具有scope及过期时间的访问令牌
* @param authentication 当前认证用户信息
* @return 添加了额外信息的新令牌
*/
OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);
}
实现TokenEnhancer接口;
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> additionalInformation = new HashMap<>();
// 1. 获取认证信息
// 客户端
String clientId = authentication.getOAuth2Request().getClientId();// 客户端ID
Set<String> resourceIds = authentication.getOAuth2Request().getResourceIds(); // 资源集合
// 用户
Authentication userAuthentication = authentication.getUserAuthentication();
Object principal = userAuthentication.getPrincipal();
if (principal instanceof User){
User user= (User) principal;
additionalInformation.put("userName", user.getUsername());
}
// 2.设置到accessToken中
additionalInformation.put("resourceIds", resourceIds);
additionalInformation.put("clientId", clientId);
additionalInformation.put("deptId", "0001");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
return accessToken;
}
}
在AuthorizationServer配置类中声明TokenEnhancer Bean对象,然后在端点配置类中添加令牌增强器。
// 端点配置
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置端点允许的请求方式
endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
// 配置认证管理器
endpoints.authenticationManager(authenticationManager);
// 自定义异常翻译器,用于处理OAuth2Exception
endpoints.exceptionTranslator(myWebResponseExceptionTranslator);
// 重新组装令牌颁发者,加入自定义授权模式
endpoints.tokenGranter(getTokenGranter(endpoints));
/* // 添加JWT令牌
// JWT令牌转换器
endpoints.accessTokenConverter(jwtAccessTokenConverter);
// JWT 存储令牌*/
endpoints.tokenStore(redisTokenStore);
// 刷新令牌模式添加 userDetailsService
endpoints.userDetailsService(userDetailsService);
// 添加令牌增强器
endpoints.tokenEnhancer(tokenEnhancer());
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new MyTokenEnhancer();
}
- 加解密
WebSecurityConfigurerAdapter,可以配置 BCryptPasswordEncoder 哈希来保存用户密码。
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("admin").password("admin").roles("USER");
}
}
除了 inMemoryAuthentication,还可以采用 ldapAuthentication,jdbcAuthentication等。
- CSRF有CsrfFilter。
- CORS有@CrossOrigin。
- 方法级安全访问有@PreAuthorize和@PostAuthorize。
- 单点登录需要有一个SSO服务器。
<待整理>