本文共 11541 字,大约阅读时间需要 38 分钟。
gateway作为客户端访问微服务的统一入口,鉴权是一件绕不开的事情,在前后端完全分离的今天,通常都是采用jwt生成token,客户端每次请求的时候都会把token放在请求头,gateway做校验,token解密没有问题,gateway就会路由分发到对应的微服务.之前的两个章节已经完成了登录和注册,这个章节写
package com.ccm.server.user.controller;import com.auth0.jwt.JWTVerifyException;import com.ccm.common.exception.result.ResultSet;import com.ccm.server.user.constants.ServerUserProperties;import com.ccm.server.user.controller.req.UserLoginReq;import com.ccm.server.user.controller.req.UserRegisterReq;import com.ccm.server.user.service.UserService;import com.ccm.server.user.util.JwtUtil;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import io.swagger.annotations.ApiParam;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.validation.Valid;import java.io.IOException;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.security.SignatureException;@Api(tags = "用户控制层")@RestController@RequestMapping(value = "user")public class UserController { @Autowired private UserService userService; @Autowired private ServerUserProperties serverUserProperties; @ApiOperation(value = "注册") @PostMapping(value = "register") public ResultSet register(@Valid @RequestBody UserRegisterReq userRegisterReq) { userService.register(userRegisterReq.getUsername(),userRegisterReq.getPassword()); return ResultSet.success(); } @ApiOperation(value = "登录") @PostMapping(value = "login") public ResultSet login(@Valid @RequestBody UserLoginReq userLoginReq) throws IOException { String token = userService.login(userLoginReq.getUsername(),userLoginReq.getPassword()); return ResultSet.success(token); } @ApiOperation(value = "解析token") @GetMapping(value = "analysisToken") public ResultSetanalysisToken(@ApiParam(required = true,value = "token") @RequestParam(name = "token") String token) throws SignatureException, NoSuchAlgorithmException, JWTVerifyException, InvalidKeyException, IOException { Long userId = JwtUtil.unsign(token, Long.class, serverUserProperties.getJwtSecretKey()); return ResultSet.success(userId); }}
package com.ccm.gateway.filter;import com.alibaba.fastjson.JSONObject;import com.alibaba.fastjson.serializer.SerializerFeature;import com.alibaba.nacos.client.naming.utils.CollectionUtils;import com.ccm.common.exception.result.CodeEnum;import com.ccm.common.exception.result.ResultSet;import com.ccm.gateway.feign.ServerUserFeign;import lombok.Data;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.cloud.gateway.filter.GatewayFilter;import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;import org.springframework.core.io.buffer.DataBuffer;import org.springframework.http.MediaType;import org.springframework.http.server.reactive.ServerHttpRequest;import org.springframework.http.server.reactive.ServerHttpResponse;import org.springframework.stereotype.Component;import org.springframework.util.AntPathMatcher;import org.springframework.util.StringUtils;import org.springframework.web.server.ServerWebExchange;import reactor.core.publisher.Mono;import java.util.Arrays;import java.util.List;import java.util.Optional;/** * @Description 鉴权过滤器 * @Author ccm * @CreateTime 2020/08/07 9:29 */@Slf4j@Componentpublic class AuthFilter extends AbstractGatewayFilterFactory{ public AuthFilter() { super(AuthFilterProperties.class); } /** * 默认需要放行的地址 */ private static final List staticUriList = Arrays.asList("*.html","*.css","*.js","/v2/api-docs","*/v2/api-docs","/webjars/**"); /** * @Description 鉴权过滤器参数实体类映射 * @Author ccm * @CreateTime 2020/8/7 9:34 */ @Data public static class AuthFilterProperties { private List excludePatterns; //放行的接口路径 } @Autowired private AntPathMatcher antPathMatcher; @Autowired private ServerUserFeign serverUserFeign; @Override public GatewayFilter apply(AuthFilterProperties authFilterProperties) { return (exchange, chain) -> { //获取请求路径 ServerHttpRequest request = exchange.getRequest(); String uri = request.getURI().getPath(); log.info("鉴权过滤器接收到请求,uri={}",uri); //放行静态资源路径 for(String staticUri: staticUriList) { if(antPathMatcher.match(staticUri,uri)) { log.info("uri={},该路径为静态资源路径,放行",uri); return chain.filter(exchange); } } //放行配置了不需要做鉴权的路径 if(!CollectionUtils.isEmpty(authFilterProperties.getExcludePatterns())) { for(String excludePattern: authFilterProperties.getExcludePatterns()) { if(antPathMatcher.match(excludePattern,uri)) { log.info("uri={},该路径为配置了无需鉴权,放行",uri); return chain.filter(exchange); } } } //从请求头获取token String token = Optional.ofNullable(request.getHeaders().get("ccm-token")) .map(t -> t.get(0)) .orElse(null); if(StringUtils.isEmpty(token)) { //token为空,鉴权不通过,直接响应给客户端 return this.response(exchange,ResultSet.error(CodeEnum.NO_TOKEN,"未传入token","请先登录")); } //调用server-user服务的解析token的接口进行鉴权 ResultSet resultSetFeign = serverUserFeign.analysisToken(token); if(!CodeEnum.SUCCESS.getCode().equals(resultSetFeign.getCode())) { //鉴权失败,直接响应给客户端 return this.response(exchange,ResultSet.error(CodeEnum.ERROR_TOKEN,"token解析失败","登录信息错误")); } //走到这里,代表token有效,将token注入到请求头中,路由转发到微服务后,微服务接口就可以拿到该token对应的用户id ServerHttpRequest.Builder builder = request.mutate(); builder.headers(headers -> { headers.set("ccm-userId",resultSetFeign.getData().toString()); }); return chain.filter(exchange); }; } /** * @Description 响应给客户端结果集 * @Author ccm * @CreateTime 2020/8/30 14:53 * @Params [exchange, resultSet] * @Return reactor.core.publisher.Mono */ public Mono response(ServerWebExchange exchange, ResultSet resultSet) { ServerHttpResponse response = exchange.getResponse(); response.getHeaders().add("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE); String responseBodyString = JSONObject.toJSONString(resultSet, SerializerFeature.WriteMapNullValue); DataBuffer bodyDataBuffer = response.bufferFactory().wrap(responseBodyString.getBytes()); return response.writeWith(Mono.just(bodyDataBuffer)); }}
package com.ccm.gateway.feign;import com.ccm.common.exception.result.ResultSet;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;/** * @Description 调用server-user服务的feign层 * @Author ccm * @CreateTime 2020/08/07 9:27 */@FeignClient(name = "server-user")public interface ServerUserFeign { @GetMapping(value = "user/analysisToken") ResultSetanalysisToken(@RequestParam(name = "token") String token);}
server: port: 200 #服务端口spring: application: name: gateway #服务名称 cloud: nacos: discovery: server-addr: 47.96.131.185:8849 #nacos服务的注册和发现地址 #gateway组件相关配置 gateway: discovery: locator: enabled: true #开启基于服务的注册和发现的路由转发,默认轮询模式 routes: #路由配置 - id: server-user #路由名称,不配默认为UUID uri: lb://server-user #满足断言的路由到此服务 predicates: #为一个数组,每个规则为并且的关系 - Path=/api-user/** #断言表达式,如果args不写key的,会自动生成一个id,如下会生成一个xxx0的key,值为/foo/* filters: #请求路由转发前执行的filter,为数组 - StripPrefix=1 #缩写,和name=StripPrefix,args,参数=1是一个意思,该过滤器为路由转发过滤去 - name: AuthFilter args: excludePatterns: #不做权限校验的路径 - /user/register - /user/login - id: server-basic #路由名称,不配默认为UUID uri: lb://server-basic #满足断言的路由到此服务 predicates: #为一个数组,每个规则为并且的关系 - Path=/api-basic/** #断言表达式,如果args不写key的,会自动生成一个id,如下会生成一个xxx0的key,值为/foo/* filters: #请求路由转发前执行的filter,为数组 - StripPrefix=1 #缩写,和name=StripPrefix,args,参数=1是一个意思,该过滤器为路由转发过滤去
package com.ccm.server.user.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.builders.ParameterBuilder;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.schema.ModelRef;import springfox.documentation.service.ApiInfo;import springfox.documentation.service.Parameter;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;import java.util.ArrayList;import java.util.List;/** * @Description swagger配置 * @Author ccm * @CreateTime 2020/7/8 17:38 */@Configurationpublic class SwaggerConfig { @Bean public Docket createRestApi() { Listpars = new ArrayList (); ParameterBuilder ticketPar = new ParameterBuilder(); ticketPar.name("ccm-token").description("必要参数(白名单接口无需传递)") .modelRef(new ModelRef("string")).parameterType("header") .required(false).build(); //header中的ticket参数非必填,传空也可以 pars.add(ticketPar.build()); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.ccm.server.user.controller")) //swagger接口扫描包 .paths(PathSelectors.any()).build().globalOperationParameters(pars); } private ApiInfo apiInfo() { return new ApiInfoBuilder().version("1.0.0") .title("欢迎") .description("光临") .termsOfServiceUrl("www.baidu.com") .build(); }}
server-user服务中加入这个测试接口,可以看到请求头 有userId这个字段,在gateway中解密token后会将对应的用户id放入请求头,如果接口能够正确返回userId,证明测试成功.
package com.ccm.server.user.controller;import com.ccm.common.exception.result.ResultSet;import com.ccm.server.user.controller.req.UserRegisterReq;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import io.swagger.annotations.ApiParam;import org.springframework.web.bind.annotation.*;import javax.validation.Valid;@Api(tags = "gateway鉴权测试")@RestController@RequestMapping(value = "gatewayAuthTest")public class GatewayAuthTestController { @ApiOperation(value = "测试gateway是否把userId注入到了请求头") @GetMapping(value = "test01") public ResultSet test01(@ApiParam(hidden = true) @RequestHeader(name = "ccm-userId") String userId) { return ResultSet.success("传入的token用户id为:"+userId); }}
可以看到正确的返回了token对应的用户id
至此,完事!您的点赞、收藏、转发和关注是我持续创作的动力!
源码地址:
转载地址:http://hktli.baihongyu.com/