【Spring Security】SpringCloudGateway 整合Spring Security Oauth2
前言
更新到这里呢,终于满足了读者两年的好奇心。从上年年底探索Spring Cloud GateWay
,众所周知,网关最大的用途在于限流、和路由,还有一个功能就是做鉴权,一直在我心中的疑惑,这个Spring Security OAuth2
是和网关怎么关联的呢,最近终于找出了答案。上图:
首先,介绍下流程
- 客户端请求认证服务进行认证。
- 认证服务认证通过向浏览器cookie写入token(身份令牌),认证服务请求用户中心查询用户信息,认证服务请求Spring Security申请令牌,认证服务将token(身份令牌)和jwt令牌存储至redis中,认证服务向cookie写入 token(身份令牌)。
- 前端携带token请求认证服务获取jwt令牌,前端获取到jwt令牌并存储在sessionStorage,前端从jwt令牌中解析中用户信息并显示在页面。
- 前端携带cookie中的token身份令牌及jwt令牌访问资源服务,前端请求资源服务需要携带两个token,一个是cookie中的身份令牌,一个是http header中的jwt令牌,前端请求资源服务前在http header上添加jwt请求资源。
- 网关校验token的合法性,用户请求必须携带token身份令牌和jwt令牌
网关校验redis中token是否合法,已过期则要求用户重新登录 - 资源服务校验jwt的合法性并完成授权
资源服务校验jwt令牌,完成授权,拥有权限的方法正常执行,没有权限的方法将拒绝访问。
身份校验
需求分析
- 从cookie查询用户身份令牌是否存在,不存在则拒绝访问
- 从http header查询jwt令牌是否存在,不存在则拒绝访问
- 从Redis查询user_token令牌是否过期,过期则拒绝访问
分享整体代码结构
已经上传到github上面, https://github.com/fafeidou/fast-cloud-nacos.git,欢迎fork。
配置application.yml,
没有配置redis,使用的默认配置
。
spring:
application:
name: security-api-gateway
cloud:
gateway:
discovery:
locator:
lower-case-service-id: true #gateway可以通过开启以下配置来打开根据服务的serviceId来匹配路由,默认是false大写
enabled: true # 是否可以通过其他服务的serviceId来转发到具体的服务实例。默认为false
routes:
- id: security-auth
uri: lb://security-auth # lb://serviceId
predicates:
- Path=/service-hi/auth/**
filters:
- StripPrefix=1
- id: security-provider
uri: lb://security-provider # lb://serviceId
predicates:
- Path=/security-provider/**
filters:
- StripPrefix=1
- id: security-consumer
uri: lb://security-consumer # lb://serviceId
predicates:
- Path=/security-consumer/**
filters:
- StripPrefix=1
nacos:
discovery:
server-addr: 127.0.0.1:8848 # 使用nacos作为注册中心
创建全局处理器·
TokenGlobalFilter
实现GlobalFilter
接口
public class TokenGlobalFilter implements GlobalFilter {
@Autowired
AuthService authService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String tokenFromCookie = authService.getTokenFromCookie(request);
if (StringUtils.isEmpty(tokenFromCookie)) {
ExceptionCast.cast(CommonCode.UNAUTHENTICATED);
}
//从header中取jwt
String jwtFromHeader = authService.getJwtFromHeader(request);
if (StringUtils.isEmpty(jwtFromHeader)) {
//拒绝访问
ExceptionCast.cast(CommonCode.UNAUTHENTICATED);
}
//从redis取出jwt的过期时间
long expire = authService.getExpire(tokenFromCookie);
if (expire < 0) {
//拒绝访问
ExceptionCast.cast(CommonCode.FORBIDDEN);
}
return chain.filter(exchange);
}
}
添加gataway全局异常处理器
spring cloud gateway 是基于webflux的,用之前的controllerAdvice
,已经不生效了,这里需要我们自己去实现,这里有两个类JsonExceptionHandler
和ErrorHandlerConfiguration
,服务之间的调用controllerAdvice
是生效的。这里只给出代码片段。具体代码请看github。
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
/**
* 获取异常属性
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
int code = 500;
Throwable error = super.getError(request);
if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
code = 404;
}
if (error instanceof fast.cloud.nacos.common.model.exception.CustomException) {
CustomException customException = (CustomException) error;
return response(code, customException.getResultCode().message());
}
return response(code, this.buildMessage(request, error));
}
/**
* 指定响应处理方法为JSON处理的方法
*
* @param errorAttributes
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* 根据code获取对应的HttpStatus
*
* @param errorAttributes
*/
@Override
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
int statusCode = (int) errorAttributes.get("code");
return HttpStatus.valueOf(statusCode);
}
/**
* 构建异常信息
*
* @param request
* @param ex
* @return
*/
private String buildMessage(ServerRequest request, Throwable ex) {
StringBuilder message = new StringBuilder("Failed to handle request [");
message.append(request.methodName());
message.append(" ");
message.append(request.uri());
message.append("]");
if (ex != null) {
message.append(": ");
message.append(ex.getMessage());
}
return message.toString();
}
/**
* 构建返回的JSON数据格式
*
* @param status 状态码
* @param errorMessage 异常信息
* @return
*/
public static Map<String, Object> response(int status, String errorMessage) {
Map<String, Object> map = new HashMap<>();
map.put("code", status);
map.put("message", errorMessage);
map.put("data", null);
return map;
}
}
测试
配置host
127.0.0.1 batman.com
启动
nacos
,api-gateway
,auth
,provider
服务,使用postman测试
查看nacos已经注册这几个服务:
获取令牌。
POST 请求: http://batman.com:40400/auth/userlogin
username和password都是batman
这个时候身份令牌已经写入cookie,jwt令牌写入redis。
获取jwt令牌
GET http://batman.com:40400/auth/userjwt
调用
provider
接口
GET 请求 http://batman.com:18085/security-provider/security/hello
错误实例
token传到有误
由于篇幅问题,这篇就先写到这里,下次会分享下,微服务之间调用如何传递token的问题以及怎样实现授权。
上一篇: OAuth2 入门---采用密码模式搭建demo工程
下一篇: IDEA Hibernate入门配置
推荐阅读
-
Spring Security认证提供程序示例详解
-
使用Spring Security控制会话的方法
-
浅谈Spring Security LDAP简介
-
如何使用Spring Security手动验证用户的方法示例
-
Spring Security OAuth2集成短信验证码登录以及第三方登录
-
Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器
-
Spring Boot Security配置教程
-
Spring Security在标准登录表单中添加一个额外的字段
-
spring security 5.x实现兼容多种密码的加密方式
-
Spring security学习笔记(三)