欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 明星 > [Java]微服务体系下的用户身份认证方案

[Java]微服务体系下的用户身份认证方案

2025/4/15 11:06:50 来源:https://blog.csdn.net/CSDN20221005/article/details/144005054  浏览:    关键词:[Java]微服务体系下的用户身份认证方案

需求分析

网关请求处理流程

网关登录校验的核心

  1. 如何在网关转发之前做登录校验?
  • 通过自定义过滤器, 请求到达微服务之前, 进行登录校验
  1. 网关如何将用户信息传递给微服务?
  • 通过在请求头中携带JWT令牌, 向后传递用户信息(网关内部的请求工具)
  1. 如何在微服务之间传递用户信息?
  • 通过在请求头中携带JWT令牌, 向后传递用户信息(请求工具OpenFeign)

自定义过滤器

网关过滤器有两种,分别是:

  1. GatewayFilter: 路由过滤器,作用于任意指定的路由; 默认不生效,要配置到路由后生效
  2. GlobalFilter: 全局过滤器,作用范围是所有路由; 声明后自动生效
  3. 两种过滤器的过滤方法签名完全一致:

解读全局过滤器的信息

定义全局过滤器: 两种过滤器的定义存在差异, 全局过滤器的定义简单一些, 实现GlobalFilter接口即可

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取请求ServerHttpRequest request = exchange.getRequest();// 过滤器业务处理System.out.println("GlobalFilter pre阶段执行了");HttpHeaders headers = request.getHeaders();System.out.println("headers = " + headers);// 反放行return chain.filter(exchange);}@Overridepublic int getOrder() {// 过滤器执行顺序, 值越小, 优先级越高return 0;}
}

自定义GatewavFilter是实现 AbstractGatewayFilterFactory 工厂,示例如下:

  1. 优势: 可以自由的指定作用的返回, 而且在配置时可以指定自定义参数, 非常灵活
  2. 劣势: 定义起来比较麻烦
  3. 定义无参的GatewavFilter过滤器

  1. 定义无参的GatewavFilter过滤器, 并设置执行顺序

  1. 定义有参的GatewavFilter过滤器

实现登录校验

需求: 在网关中基于过滤器实现登录校验功能

  • 黑马商城是基于IWT实现的登录校验,目前相关功能在hm-service模块

  1. 将JWT工具类拷贝到gateway模块

  • 拷贝过来的AuthProperties类会报错, 表示该类没有生效, 添加@Component注解就可以了

  1. 将工具方法拷贝到gateway模块

  1. 把JWT的一些配置复制到配置文件中
hm:jwt:location: classpath:hmall.jksalias: hmallpassword: hmall123tokenTTL: 30mauth:excludePaths:- /search/**- /users/login- /items/**- /hi
  1. 创建GlobalFilter过滤器, 实现登录校验
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {// 注入配置文件中的属性 (排除路径)private final AuthProperties authProperties;// 注入jwt工具类private final JwtTool jwtTool;// Spring提供的专门用于路径匹配的工具private final AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//1. 获取requestServerHttpRequest request = exchange.getRequest();//2. 判断是否需要做登录拦截String path = request.getPath().toString();if (isExclude(path)){return  chain.filter(exchange);}//3. 获取tokenString token = null;List<String> headers = request.getHeaders().get("authorization");if(headers != null && !headers.isEmpty()) {token = headers.get(0);}//4. 校验并解析tokenLong userId = null;try {userId = jwtTool.parseToken(token);} catch (UnauthorizedException e) {// 拦截,设置响应状态为401ServerHttpResponse response = exchange.getResponse();response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//5. 传递用户信息System.out.println("userId = " + userId);//6. 放行return chain.filter(exchange);}private boolean isExclude(String path) {for (String excludePath : authProperties.getExcludePaths()) {if (antPathMatcher.match(excludePath, path)) {return true;}}return false;}@Overridepublic int getOrder() {return 0;}
}
  1. 访问需要校验的接口

网关传递用户信息

网关传递用户信息到微服务的流程

  1. 网关在自定义过滤器中拿到请求头中的用户信息, 并进行校验, 没问题后, 把用户信息传递给后面的微服务
  2. 在微服务中定义拦截器, 拿到用户信息, 然后保存到ThreadLocal中, 后续业务就可以随时使用了

在网关的登录校验过滤器中, 把获取到的用户写入请求头

@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {... ...@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {... ...//5. 传递用户信息// System.out.println("userId = " + userId);String userInfo = userId.toString();ServerWebExchange swe = exchange.mutate().request(builder -> builder.header("user-info", userInfo)).build();//6. 放行return chain.filter(swe);}... ...
}
  1. 需求: 修改gateway模块中的登录校验拦截器,在校验成功后保存用户到下游请求的请求头中
  2. 提示: 要修改转发到微服务的请求,需要用到ServerWebExchange类提供的API
  3. 使用 exchange 上下文对象的mutate 方法对请求做修改
  4. mutate对象返回构建器对象builder, 使用builder中的request方法对请求做修改
  5. request方法会返回requestBuild, 可以使用lambda表达式, 对请求头或者其他部分做处理
  6. 定义的请求头的名字可以随便写, 但是取值时也要用这个名字, 建议定义为常量, 请求头的内容需要是字符串
  7. 请求修改完成后会返回新的ServerWebExchange, 放行时把最新的请求上下文传递过去, 修改就生效了
  8. 测试一下: 在购物车微服务中, 修改查询购物车的controller, 拿到请求头中用户信息
@Api(tags = "购物车相关接口")
@RestController
@RequestMapping("/carts")
@RequiredArgsConstructor
public class CartController {@ApiOperation("查询购物车列表")@GetMappingpublic List<CartVO> queryMyCarts(@RequestHeader(value = "user-info", required = false) String userInfo) {System.out.println("userInfo =" + userInfo);return cartService.queryMyCarts();}}

  1. 说明网关成功从token中解析出用户信息, 并且通过请求头传递给了下游微服务
  2. 在controller中通过注解形式获取用户信息, 比较麻烦

在hm-common中编写SpringMVC拦截器,获取登录用户

  1. 由于每个微服务都可能有获取登录用户的需求,因此我们直接在hm-common模块定义拦截器
  2. 这样微服务只需要引入依赖即可生效,无需重复编写
  3. 定义拦截器, 储存用户信息

public class UserInfoInterceptor implements HandlerInterceptor {public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.获取登录用户信息String userInfo = request.getHeader("user-info");// 2. 判断是否获取了用户信息(有些接口不需要登录就能访问, 所以可能获取不到)if (StrUtil.isNotBlank(userInfo)) {UserContext.setUser(Long.parseLong(userInfo));}// 3.放行return  true;}public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// controller执行完成后清理用户UserContext.removeUser();}
}
  • StrUtil.isNotBlan()时hutool工具包提供的判断字符串的方法, 可以判断null, 空字符串, 空格等多种情况, 比较严谨
  • UserContext工具类中的getUser()使用的非常多, 可以使用ctrl + alt + f7 查看使用的地方

  1. 把cart-service中写死的用户id改造为动态获取
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {@Overridepublic List<CartVO> queryMyCarts() {// 1.查询我的购物车列表// todo: UserContext.getUser() => 写死1 ,待优化List<Cart> carts = lambdaQuery().eq(Cart::getUserId,1L ).list();List<Cart> carts = lambdaQuery().eq(Cart::getUserId,UserContext.getUser()).list();... ...}
}
  1. 注册拦截器, 让拦截器生效
@Configuration
public class MvcConfig implements WebMvcConfigurer {public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
}
  1. 拦截器的配置是在hm-common模块下, 其他微服务默认是扫描不到的, 所以并不会生效, 所以要把配置类放到META-INF文件下的spring.factories中, 就能实现自动装配了

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.hmall.common.config.MyBatisConfig,\com.hmall.common.config.MvcConfig,\com.hmall.common.config.JsonConfig
  1. WebMvcConfigurer属于SpringConfigMvc包下, 网关底层不是SpringMvc那一套, 而是非阻塞的响应式编程技术, 基于WebFlux, 所以我们在网关中引入hm-common时, 拦截器的配置类会被加载, 就会报错
  2. 所以我们要让拦截器的配置类按需加载, SpringBoot的自动装配是可以带条件的, 让配置类在微服务中生效, 在网关中不生效, 判断条件就是是否存在SpringMvc, SpringMvc的核心是DispatcherServlet
@Configuration
@ConditionalOnClass(DispatcherServlet.class) // springboot自动配置的条件注解
public class MvcConfig implements WebMvcConfigurer {public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
}
  1. 测试一下: 动态拿到用户id, 查询购物车数据

OpenFeign传递用户

微服务项目中的很多业务要多个微服务共同合作完成,而这个过程中也需要传递登录用户信息,例如:

OpenFeign中提供了一个拦截器接口,所有由OpenFeign发起的请求都会先调用拦截器处理请求:

其中的RequestTemplate类中提供了一些方法可以让我们修改请求头:

解决方案: 使用OpenFeign调用其他微服务时, 在请求头中携带用户信息, 其他服务就可以拿到用户信息了

  1. 在hm-api引入common模块, 因为要使用ThreadLocal工具类
    <!-- common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency>
  1. 在hm-api中添加OpenFeign的拦截器接口
public class DefaultFeignConfig {@Beanpublic RequestInterceptor feignInterceptor() {return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate requestTemplate) {Long userId = UserContext.getUser();if (userId != null) {requestTemplate.header("user-info", userId.toString());}}};}
}
  • 注意: 交易服务的请求是从网关转发过来的, 所以交易服务是一定可以拿到请求头中的用户信息的
  1. DefaultFeignConfig这个配置生效的前提是要加在OpenFeign的启动类上
/**交易服务
*/
@MapperScan("com.hmall.trade.mapper")
@EnableFeignClients(basePackages = "com.hmall.api.client", defaultConfiguration = DefaultFeignConfig.class)
@SpringBootApplication
public class TradeApplication {public static void main(String[] args) {SpringApplication.run(TradeApplication.class, args);}
}
  1. 重启交易服务, 提交订单后, 会删除购物车中已经结算的商品

小结

为了解决微服务体系下的用户登录校验和用户信息传递, 使用了几种拦截器, 现在梳理一下

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词