基于 OAuth2, Token 的 SSO 的缺点
- 请求每次到网关,网关都会去认证服务器认证,多一次网络开销,认证服务器压力大;
- 网关向下游微服务传递用户名的时候,是明文在 Header 中传的,不安全;
- 微服务之间传递用户名的时候很麻烦,每次都要在 Header 中添加用户名;
JWT | Java Web Token
- 是 Token 的一个规范,之前的 access_token 是无意义的字符串,JWT 的串本身就包含了用户是谁,通过这样一个有用户信息的 Token,前端服务器的请求到网关,网关就不用去认证服务器查这个 Token 对应的是谁了,而且往下传的时候,直接传这个 JWT 就行了,不用传明文的用户名了;Spring 还提供了一个工具,在微服务之间传递 JWT,不用写额外的代码;
JWT 改造实现步骤
- TokenStore 要从 JdbcTokenStore 变成 JwtTokenStore;
- JwtTokenStore 中要定义一个 JwtAccessTokenConverter,里面定义了签名的 key;
- 在 endpoints 中要配置一下
.tokenEnhancer(jwtTokenEnhancer())
; - 在 security 中要配置一下
.tokenKeyAccess("isAuthenticated()")
,这会使 Spring 的 OAuth2 服务器对外暴露一个服务,之后通过认证的请求能访问这个服务,通过这个服务拿到的是用于 JWT 签名的 key;
JWT 举例分析
怎么获取 JWT
- 和一般的 access_token 的 API 是一样的:
http://localhost:9090/oauth/token
,Header 和 Body 中要带的东西也是一样的;
JWT 长什么样
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXItc2VydmVyIl0sInVzZXJfbmFtZSI6ImxlaWxlaSIsInNjb3BlIjpbInJlYWQiXSwiZXhwIjoxNTk1MzIyMzAxLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl0sImp0aSI6Ijg4ZDk3OTYzLTFmMWMtNGVjOS1hZTNiLWVjODg0Y2ZmODU1NSIsImNsaWVudF9pZCI6ImFkbWluIn0.3MR0sDydaxiJknIdNGNkd5vZJLbZhr994rLMzvf3vMk",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXItc2VydmVyIl0sInVzZXJfbmFtZSI6ImxlaWxlaSIsInNjb3BlIjpbInJlYWQiXSwiYXRpIjoiODhkOTc5NjMtMWYxYy00ZWM5LWFlM2ItZWM4ODRjZmY4NTU1IiwiZXhwIjoxNTk1MzIyMzExLCJhdXRob3JpdGllcyI6WyJST0xFX0FETUlOIl0sImp0aSI6IjM2NjU4ZTZhLTI5MWUtNDg1MS1hODZkLTYzYjY0MGE5Y2MwNiIsImNsaWVudF9pZCI6ImFkbWluIn0.IiJSzEwE9IINUfCW--2lGrxcbwPNmY3r3o_yWzJarWQ",
"expires_in": 9,
"scope": "read",
"jti": "88d97963-1f1c-4ec9-ae3b-ec884cff8555"
}
JWT 的 access_token 字段分析
- 把 access_token 中的值,帖到 https://jwt.io/ 这个网站,就可以 解析出来;
- JWT 分为 3 部分:HEADER,PAYLOAD,VERIFY_SIGNATURE;
HEADER:
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD:
{
"aud": [
"order-server" // 拿着这个令牌,可以访问哪些资源服务器,和 oauth_client_details.resource_ids 字段中的值是对应的
],
"user_name": "leilei", // 这个令牌是发给 admin 应用的 leilei 这个用户的
"scope": [
"read"
],
"exp": 1595322301,
"authorities": [
"ROLE_ADMIN"
],
"jti": "88d97963-1f1c-4ec9-ae3b-ec884cff8555", // 可以认为是这个令牌的 Id
"client_id": "admin" // 这个令牌是发给哪个应用的
}
VERIFY SIGNATURE:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
) secret base64 encoded
JWT 实现 | 网关改造
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
配置文件
security:
oauth2:
client:
# 访问 /oauth/token_key 的时候,必修要身份认证
client-id: gateway
client-secret: 123456
resource:
jwt:
# Zuul 在启动的时候,要知道从哪里拿到用于签名的秘钥,这个秘钥在证书(jojo.key)中
key-uri: http://localhost:9090/oauth/token_key
资源服务器配置
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 所有去认证服务器的申请 Token 的请求都放过
.antMatchers("/token/**").permitAll()
.anyRequest().authenticated();
}
}
JWT 实现 | 微服务改造
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
配置文件
security:
oauth2:
client:
# 访问 /oauth/token_key 的时候,必修要身份认证
client-id: gateway
client-secret: 123456
resource:
jwt:
# Zuul 在启动的时候,要知道从哪里拿到用于签名的秘钥,这个秘钥在证书(jojo.key)中
key-uri: http://localhost:9090/oauth/token_key
启动类
// order 微服务作为 OAuth 的资源服务器
@EnableResourceServer
@SpringBootApplication
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
Controller 层
/**
* 换用 JWT 之后,就要使用 @AuthenticationPrincipal 注解了
* @param id
* @param username
* @return
*/
@GetMapping("/{id}")
public OrderInfo getInfo(@PathVariable Long id, @AuthenticationPrincipal String username) {
log.info("user name " + username);
log.info("orderId is " + id);
OrderInfo info = new OrderInfo();
info.setId(id);
info.setProductId(id * 5);
return info;
}
JWT 的网关工作流程
- 申请 token 的请求会放过;
- 带着 token 的请求到达 zuul 后,zuul 直接验签这个 jwt 是否被篡改过,验证需要的私钥,zuul 在启动的时候,就通过 /oauth/token_key 接口拿到了;
- 如果验证 jwt 没被篡改过,就把这个 jwt 解析出来,如果 jwt 的 aud 中的值,不包含 zuul 作为 OAuth2 资源服务器的 id,那么这个 token 无法访问 zuul,返回 403,完了把 jwt 给到 order 服务;order 服务再从 jwt 中解析出 username;
- 如果验证 jwt 被篡改过,那就认证就失败了,返回 401;
使用 JWT 之后的权限控制
类 ACL 的权限控制
- 最简单的就是 ACL(OAuth2 中的 scopes),在发令牌的时候,就带着 scopes,完了请求带着 JWT 访问微服务,微服务的 Controller 层通过注解
@PreAuthorize("#oauth2.hasScope('fly')")
控制权限;scopes 这个东西是针对客户端应用的,但是无法聚焦到某个人身上; - 可以通过
@PreAuthorize("#hasRole('ROLE_ADMIN')")
把权限聚焦到人身上,这个 ROLE_ADMIN 是在 UserDetailsServiceImpl 中为用户指定的,在 loadUserByUsername 这个方法中,可以根据不同的用户名,为用户指定不同的角色;
缺点:每次修改权限,都要修改代码,都要重启应用;
网关调用权限服务

先配置,所有的请求都要通过一个方法验证是否有权限:
package com.lixinlei.security.gateway.config;
/**
* 网关现在作为资源服务器了,即使网关,也是资源服务器
*/
@Configuration
@EnableResourceServer
public class GatewaySecurityConfig extends ResourceServerConfigurerAdapter {
@Autowired
private GatewayWebSecurityExpressionHandler gatewayWebSecurityExpressionHandler;
/**
* 设置:作为资源服务器的 zuul,其 id 是什么,如果这个 id 在客户端应用的 resource_ids 中,
* 那么发给这个客户端应用的 jwt,就可以用来访问 zuul;
* @param resources
* @throws Exception
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId("gateway")
// 表达式 #permissionService.hasPermission(request, authentication) 谁来解析,是在这个类中指定的,
// gatewayWebSecurityExpressionHandler 就是继承了 OAuth2WebSecurityExpressionHandler 的类;
.expressionHandler(gatewayWebSecurityExpressionHandler);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 所有去认证服务器申请 Token 的请求都放过
.antMatchers("/token/**").permitAll()
// request 就是当前的请求,authentication 就是当前的用户,
// 对于任何氢气,Zuul 会调一个服务的 hasPermission 方法,
// 返回 true 就能访问,返回 false 就不能访问;
// 默认配置下,#permissionService.hasPermission(request, authentication) 这个 Spring Security 是看不懂的,
// 需要在一个继承了 OAuth2WebSecurityExpressionHandler 的类中,配置让谁来解析表达式 permissionService;
.anyRequest().access("#permissionService.hasPermission(request, authentication)");
}
}
配置权限表达式谁来解析:
@Component
public class GatewayWebSecurityExpressionHandler extends OAuth2WebSecurityExpressionHandler {
@Autowired
private PermissionService permissionService;
/**
* 如果解析表达式 #permissionService.hasPermission(request, authentication),
* 通过 permissionService 这个注入进来的 Bean
* @param authentication
* @param invocation
* @return
*/
@Override
protected StandardEvaluationContext createEvaluationContextInternal(Authentication authentication,
FilterInvocation invocation) {
StandardEvaluationContext sec = super.createEvaluationContextInternal(authentication, invocation);
// 告诉 Spring Security,表达式 permissionService 用这个 Bean permissionService 来解析
sec.setVariable("permissionService", permissionService);
return sec;
}
}
具体用来解析表达式的类:
@Service
public class PermissionServiceImpl implements PermissionService {
/**
* 这个方法中,要判断用户是否有权限访问他要访问的 URI;
* 在这个方法中,就应该调用远程服务,Redis,MySQL,或者 Zuul 启动的时候,就把权限信息都缓存到本地内存了;
* @param request
* @param authentication
* @return
*/
@Override
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
// TODO: 2020/7/22 方法的实现换成自己业务下的权限控制逻辑就可以了
System.out.println(request.getRequestURI());
System.out.println(ReflectionToStringBuilder.toString(authentication));
// if(authentication instanceof AnonymousAuthenticationToken) {
// throw new AccessTokenRequiredException(null);
// }
return RandomUtils.nextInt() % 2 == 0;
}
}
微服务之间的权限控制
- 通过 JWT,网关可以完成认证;
- 通过调用授权服务器,网关可以完成授权;
- 比如这个用户有访问 order 的权限,但是没有访问扣钱服务的权限,但是 order 服务器会调用扣钱的服务,怎么做到控制扣钱服务的权限呢?一般会在扣钱服务上做一个白名单,指定之后哪个服务才能调用扣钱服务,所有需要调用扣钱服务的请求,都由这个服务来调;
微服务安全中心是认证服务器 + 授权服务器的统称。
JWT 网关返回 401 的两种可能
- 令牌传错了:这种情况下,Spring Security 的认证过滤器直接抛异常,进入 401 的处理逻辑,添加一条 401 的日志;
- 没传令牌:请求会进入授权逻辑,授权逻辑在调用授权服务器之前,会判断是不是匿名用户来要授权,如果是匿名用户,直接抛异常,这个异常会进入 401 的处理逻辑,401 的处理逻辑判断是授权阶段抛的异常,就把之前已经创建的日志更为为 401;
不传 token 访问 JWT 网关返回 401 的两种情况
授权成功 | 后端微服务返回 401
- 没传 token 还授权成功,这种情况不存在的,但是在实现授权逻辑的时候,要考虑到这种情况,要堵住这种漏洞;
- 没带 token 的话,认证的过滤器会放过,然后审计过滤器记下匿名用户访问,授权过滤器授权成功,把请求往后放了,然后审计过滤器,把日志更新为 success;但是客户端收到的还是 401,这个 401 是网关后的微服务,因为请求没带 token,返回给客户端的;
授权失败 | 网关返回 401
- 没带 token 的话,认证的过滤器会放过,然后审计过滤器记下匿名用户访问,授权过滤器授权失败,进到 403 的错误处理中,错误处理发现没带 token,就进入 401 的错误处理中,401 的错误处理把日志更新为 401;然后网关给客户端响应 401;
JWT 网关总结

- 蓝色的都是 Spring Security 实现的,绿色的都是自己实现的;
- 左边都是过滤器,右边是自己写的组件;
- OAuth2ClientAuthenticatinProcessingFilter 是解析 JWT 的;
- FilterSecurityInterceptor 就是判断权限,自己写的 PermissionService 就是在这里生效的;
- ExceptionTranslationFilter 会根据捕获到的异常,调相应的处理器,整个安全里面只有 2 种异常:401 和 403; 401 就是 GatewayAuthenticationEntryPoint 处理,403 就是 GatewayAccessDeniedHandler 处理;401 可能是没传令牌,还可能是令牌不对
网友评论