上一篇通过网关,
解决了 问题1:微服务场景下,客户端访问服务的复杂性
未解决 问题2:安全逻辑和业务逻辑的耦合;问题3:微服务过多对认证服务器的压力增大
本篇将微服务里的安全相关的逻辑挪到网关上来,这样就能解决这两个问题。
在之前的订单服务里(资源服务器),主要做了两件事:
1,认证,拿token去认证服务器验令牌
2,授权,post请求的token必须要有write权限,get请求的token必需要有read权限
有了网关之后,所有的请求都要走网关来转发到微服务上,所以网关上处理认证和授权,之前篇章说的所有的认证机制都要加到网关上:认证、授权、审计、限流,
下面开始在网关上实现 认证、授权、审计、限流
1,认证Filter
新建类过滤器 OAuthFilter,继承 ZuulFilter,重写其方法
package com.nb.security.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.constants.ZuulConstants; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.annotation.FilterType; import org.springframework.http.*; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; /** * OAuth认证过滤器 * Created by: 李浩洋 on 2019-12-28 **/ @Slf4j @Component public class OAuthFilter extends ZuulFilter { private RestTemplate restTemplate = new RestTemplate(); /** * 过滤器类型: * "pre":在业务逻辑执行之前执行run()的逻辑 * "post":在业务逻辑执行之后执行run()的逻辑 * "error":在业务逻辑抛出异常执行run()的逻辑 * "route":控制路由,一般不用这个,zuul已实现 * @return */ @Override public String filterType() { return "pre"; } //执行顺序 @Override public int filterOrder() { return 1; } //是否过滤 @Override public boolean shouldFilter() { return true; } /** * 具体的业务逻辑 * 这里是认证逻辑, */ @Override public Object run() throws ZuulException { log.info("oauth start "); //获取请求和响应 RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); if(StringUtils.startsWith(request.getRequestURI(),"/token")){ // /token开头的请求,是发往认证服务器的请求,获取token的,直接放行 return null; } //获取请求头的token String authHeader = request.getHeader("Authorization"); if(StringUtils.isBlank(authHeader)){ //如果请求头没有带token,不管认证信息有没有,对不对,都往下走,(要做审计日志) return null; } if(!StringUtils.startsWithIgnoreCase(authHeader,"bearer ")){ //这个过滤器只处理OAuth认证的请求,不是OAuth的token(如 HTTP basic),也往下走 return null; } //走到这里,说明携带的OAuth认证的请求,验token try { TokenInfo info = getTokenInfo(authHeader); request.setAttribute("tokenInfo",info); }catch (Exception e){ log.info("获取tokenInfo 失败!",e); } return null; } /** * 去认证服务器校验token * @param authHeader * @return */ private TokenInfo getTokenInfo(String authHeader) { //截取请求头里的bearer token String token = StringUtils.substringAfter(authHeader,"bearer "); //认证服务器验token地址 /oauth/check_token 是 spring .security.oauth2的验token端点 String oauthServiceUrl = "http://localhost:9090/oauth/check_token"; HttpHeaders headers = new HttpHeaders();//org.springframework.http.HttpHeaders headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json请求 //网关的appId,appSecret,需要在数据库oauth_client_details注册 headers.setBasicAuth("gateway","123456"); MultiValueMap<String,String> params = new LinkedMultiValueMap<>(); params.add("token",token); HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers); ResponseEntity<TokenInfo> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, TokenInfo.class); log.info("token info : {}",response.getBody().toString()); return response.getBody();//返回tokenInfo } }
TokenInfo封装token信息:
package com.nb.security.filter; import lombok.Data; import java.util.Date; /** * 包装从认证服务器获取token信息响应对象 */ @Data public class TokenInfo { //token是否可用 private boolean active; //令牌发给那个客户端应用的 客户端id private String client_id; //令牌scope private String[] scope; //用户名 private String user_name; //令牌能访问哪些资源服务器,资源服务器的id private String[] aud; //令牌过期时间 private Date exp; //令牌对应的user的 权限集合 UserDetailsService里loadUserByUsername()返回的User的权限集合 private String[] authorities; }
2,审计日志Filter
审计日志过滤器,请求过来的时候,记录一条日志,请求出去的时候更新日志
package com.nb.security.filter; import com.nb.security.entity.AuditLog; import com.nb.security.service.IAuditLogService; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import jdk.nashorn.internal.parser.Token; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Date; /** * 审计过滤器 * 1流控--2认证--3审计--4授权 */ @Slf4j @Component public class AuditLogFilter extends ZuulFilter { @Autowired private IAuditLogService auditLogService; @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 2; //在OAuthFilter后 } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { log.info(" audit log insert ...."); RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); AuditLog log = new AuditLog(); log.setCreateTime(new Date()); log.setPath(request.getRequestURI()); log.setMethod(request.getMethod()); TokenInfo info = (TokenInfo) request.getAttribute("tokenInfo"); if(info != null){ log.setUsername(info.getUser_name()); } auditLogService.save(log); request.setAttribute("auditLogId",log.getId()); return null; } }
3,授权过滤器
在授权过滤器里,需要自己去查数据库,判断当前用户是否有权限。
package com.nb.security.filter; import com.nb.security.entity.AuditLog; import com.nb.security.service.IAuditLogService; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Date; /** * 授权过滤器 */ @Slf4j @Component public class AuthorizationFilter extends ZuulFilter { @Autowired private IAuditLogService auditLogService; @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 3; //在审计过滤器后 } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { log.info("authorization start"); RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); //判断是否需要认证 if(isNeedAuth(request)){ //需要认证,从request取出AuthFilter放入的tokenInfo TokenInfo tokenInfo = (TokenInfo)request.getAttribute("tokenInfo"); if(tokenInfo != null && tokenInfo.isActive()){//不为空且为激活状态 //认证成功,看是否有权限 if(!hasPermission(tokenInfo,request)){ //没有权限 log.info("audit log update fail 403 "); //更新审计日志 ,403 Long auditLogId = (Long)request.getAttribute("auditLogId"); AuditLog log = auditLogService.getById(auditLogId); log.setUpdateTime(new Date()); log.setStatus(403); auditLogService.updateById(log); handleError(403,requestContext); } //走到这里说明权限也通过了,将用户信息放到请求头,供其他微服务获取 requestContext.addZuulRequestHeader("username",tokenInfo.getUser_name()); }else{ //不是以 /token开头的,才拦截,否则登录请求也就被拦截了。这里放过 if(!StringUtils.startsWith(request.getRequestURI(),"/token")){ //////////更新审计日志//////////////// log.info("audit log update fail 401 "); Long auditLogId = (Long)request.getAttribute("auditLogId"); AuditLog log = auditLogService.getById(auditLogId); log.setUpdateTime(new Date()); log.setStatus(401); auditLogService.updateById(log); //认证失败,没有tokenInfo,报错,修改审计日志状态 handleError(401,requestContext); } } } return null; } /** * 认证成功,看是否有权限 * TODO:从数据库查询权限,这里直接返回 * @param tokenInfo * @param request * @return */ private boolean hasPermission(TokenInfo tokenInfo, HttpServletRequest request) { return true;//RandomUtils.nextInt() % 2 == 0; } /** * 处理认证失败或者没有权限 * @param status http状态码 * @param requestContext */ private void handleError(int status, RequestContext requestContext) { requestContext.getResponse().setContentType("application/json");//响应json requestContext.setResponseStatusCode(status);//响应状态码 requestContext.setResponseBody("{\"message\":\"auth fail\"}"); requestContext.setSendZuulResponse(false);//这一句是说,当前过滤器到此返回,不会再往下走了、 } /** * 判断当前请求是否需要认证 * TODO:查数据库判断权限 * @param request * @return */ private boolean isNeedAuth(HttpServletRequest request) { return true; } }
实验
依次启动订单,认证,网关 三个微服务
在OAuth客户端配置的表里,配上网关的appId,appSecret,使其成为一个OAuth客户端。注意,一定要把client_secret配置正确,配置错误会一直报 HttpClientErrorException$Unauthorized: 401 null异常。
访问网关获取token:
访问网关,创建订单:
一切还算顺利。下面开始删掉订单服务里,关于安全的一些个代码:
订单服务里,删除oauth2的maven依赖,删除跟资源服务器相关的一切代码,只剩下如下干净的代码:
目前在其他微服务中获取用户信息的办法是,在网关的授权过滤器中,当一切条件都通过后,将用户信息,添加到Zuul的请求头里,在其他微服务,就可以从请求头中获取用户信息了,甚至可以穿进去一个json字符串,然后取的时候将json字符串转换为对象。(这种做法不好,后续文章介绍其他方法)
重复上边的实验步骤,依然可以从网关获取token,创建订单!
代码github:https://github.com/lhy1234/springcloud-security/tree/chapt-4-9-gateway02
欢迎关注个人公众号一起交流学习: