Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >微服务的用户认证与授权杂谈(下)

微服务的用户认证与授权杂谈(下)

作者头像
端碗吹水
发布于 2020-09-23 02:17:29
发布于 2020-09-23 02:17:29
74100
代码可运行
举报
运行总次数:0
代码可运行

TOC


AOP实现登录状态检查

微服务的用户认证与授权杂谈(上)一文中简单介绍了微服务下常见的几种认证授权方案,并且使用JWT编写了一个极简demo来模拟Token的颁发及校验。而本文的目的主要是延续上文来补充几个要点,例如Token如何在多个微服务间进行传递,以及如何利用AOP实现登录态和权限的统一校验。

为了让登录态的检查逻辑能够通用,我们一般会选择使用过滤器、拦截器以及AOP等手段来实现这个功能。而本小节主要是介绍使用AOP实现登录状态检查,因为利用AOP同样可以拦截受保护的资源访问请求,在对资源访问前先做一些必要的检查。

首先需要在项目中添加AOP的依赖:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

定义一个注解,用于标识哪些方法在被访问之前需要进行登录态的检查。具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zj.node.usercenter.auth;

/**
 * 被该注解标记的方法都需要检查登录状态
 *
 * @author 01
 * @date 2019-09-08
 **/
public @interface CheckLogin {
}

编写一个切面,实现登录态检查的具体逻辑,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zj.node.usercenter.auth;

import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 登录态检查切面类
 *
 * @author 01
 * @date 2019-09-08
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class CheckLoginAspect {

    private static final String TOKEN_NAME = "X-Token";

    private final JwtOperator jwtOperator;

    /**
     * 在执行@CheckLogin注解标识的方法之前都会先执行此方法
     */
    @Around("@annotation(com.zj.node.usercenter.auth.CheckLogin)")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 从header中获取Token
        String token = request.getHeader(TOKEN_NAME);

        // 校验Token是否合法
        Boolean isValid = jwtOperator.validateToken(token);
        if (BooleanUtils.isFalse(isValid)) {
            log.warn("登录态校验不通过,无效的Token:{}", token);
            // 抛出自定义异常
            throw new SecurityException("Token不合法!");
        }

        // 校验通过,可以设置用户信息到request里
        Claims claims = jwtOperator.getClaimsFromToken(token);
        log.info("登录态校验通过,用户名:{}", claims.get("userName"));
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("userName", claims.get("userName"));
        request.setAttribute("role", claims.get("role"));

        return joinPoint.proceed();
    }
}

然后编写两个接口用于模拟受保护的资源和获取token。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;
    private final JwtOperator jwtOperator;

    /**
     * 需要校验登录态后才能访问的资源
     */
    @CheckLogin
    @GetMapping("/{id}")
    public User findById(@PathVariable Integer id) {
        log.info("get request. id is {}", id);
        return userService.findById(id);
    }

    /**
     * 模拟生成token
     *
     * @return token
     */
    @GetMapping("gen-token")
    public String genToken() {
        Map<String, Object> userInfo = new HashMap<>();
        userInfo.put("id", 1);
        userInfo.put("userName", "小眀");
        userInfo.put("role", "user");

        return jwtOperator.generateToken(userInfo);
    }
}

最后我们来进行一个简单的测试,看看访问受保护的资源时是否会先执行切面方法来检查登录态。首先启动项目获取token:

在访问受保护的资源时在header中带上token:

访问成功,此时控制台输出如下:

Tips:

这里之所以没有使用过滤器或拦截器来实现登录态的校验,而是采用了AOP,这是因为使用AOP写出来的代码比较干净并且可以利用自定义注解实现可插拔的效果,例如访问某个资源不用进行登录态检查了,那么只需要把@CheckLogin注解给去掉即可。另外就是AOP属于比较重要的基础知识,也是在面试中经常被问到的知识点,通过这个实际的应用例子,可以让我们对AOP的使用技巧有一定的了解。 当然也可以选择过滤器或拦截器来实现,没有说哪种方式就是最好的,毕竟这三种方式都有各自的特性和优缺点,需要根据具体的业务场景来选择。


Feign实现Token传递

微服务架构中通常会使用Feign来调用其他微服务所提供的接口,若该接口需要对登录态进行检查的话,那么就得传递当前客户端请求所携带的Token。而默认情况下Feign在请求其他服务的接口时,是不会携带任何额外信息的,所以此时我们就得考虑如何在微服务之间传递Token。

让Feign实现Token的传递还是比较简单的,主要有两种方式,第一种是使用Spring MVC的@RequestHeader注解。如下示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@FeignClient(name = "order-center")
public interface OrderCenterService {

    @GetMapping("/orders/{id}")
    OrderDTO findById(@PathVariable Integer id,
                      @RequestHeader("X-Token") String token);
}

Controller里的方法也需要使用这个注解来从header中获取Token,然后传递给Feign。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
@RequiredArgsConstructor
public class TestController {

    private final OrderCenterService orderCenterService;

    @GetMapping("/{id}")
    public OrderDTO findById(@PathVariable("id") Integer id,
                            @RequestHeader("X-Token") String token) {
        return orderCenterService.findById(id, token);
    }
}

从上面这个例子可以看出,使用@RequestHeader注解的优点就是简单直观,而缺点也很明显。当只有一两个接口需要传递Token时,这种方式还是可行的,但如果有很多个远程接口需要传递Token的话,那么每个方法都得加上这个注解,显然会增加很多重复的工作。

所以第二种传递Token的方式更为通用,这种方式是通过实现一个Feign的请求拦截器,然后在拦截器中获取当前客户端请求所携带的Token并添加到Feign的请求header中,以此实现Token的传递。如下示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zj.node.contentcenter.feignclient.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 请求拦截器,实现在服务间传递Token
 *
 * @author 01
 * @date 2019-09-08
 **/
public class TokenRelayRequestInterceptor implements RequestInterceptor {

    private static final String TOKEN_NAME = "X-Token";

    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 获取当前的request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 从header中获取Token
        String token = request.getHeader(TOKEN_NAME);

        // 传递token
        requestTemplate.header(TOKEN_NAME,token);
    }
}

然后需要在配置文件中,配置该请求拦截器的包名路径,不然不会生效。如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 定义feign相关配置
feign:
  client:
    config:
      # default即表示为全局配置
      default:
        requestInterceptor:
          - com.zj.node.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor

RestTemplate实现Token传递

除了Feign以外,部分情况下有可能会使用RestTemplate来请求其他服务的接口,所以本小节也介绍一下,在使用RestTemplate的情况下如何实现Token的传递。

RestTemplate也有两种方式可以实现Token的传递,第一种方式是请求时使用exchange()方法,因为该方法可以接收header。如下示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@RestController
@RequiredArgsConstructor
public class TestController {

    private final RestTemplate restTemplate;

    @GetMapping("/{id}")
    public OrderDTO findById(@PathVariable("id") Integer id,
                            @RequestHeader("X-Token") String token) {
        // 传递token                    
        HttpHeaders headers = new HttpHeaders();
        headers.add("X-Token", token);

        return restTemplate.exchange(
                "http://order-center/orders/{id}",
                HttpMethod.GET,
                new HttpEntity<>(headers),
                OrderDTO.class,
                id).getBody();
    }
}

另一种则是实现ClientHttpRequestInterceptor接口,该接口是RestTemplate的拦截器接口,与Feign的拦截器类似,都是用来实现通用逻辑的。具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class TokenRelayRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final String TOKEN_NAME = "X-Token";

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        // 获取当前的request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest servletRequest = attributes.getRequest();
        // 从header中获取Token
        String token = servletRequest.getHeader(TOKEN_NAME);

        // 传递Token
        request.getHeaders().add(TOKEN_NAME,token);
        return execution.execute(request, body);
    }
}

最后需要将实现的拦截器注册到RestTemplate中让其生效,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Configuration
public class BeanConfig {

    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setInterceptors(Collections.singletonList(
                new TokenRelayRequestInterceptor()
        ));

        return restTemplate;
    }
}

AOP实现用户权限验证

在第一小节中我们介绍了如何使用AOP实现登录态检查,除此之外某些受保护的资源可能需要用户拥有特定的权限才能够访问,那么我们就得在该资源被访问之前做权限校验。权限校验功能同样也可以使用过滤器、拦截器或AOP来实现,和之前一样本小节采用AOP作为示例。

这里也不做太复杂的校验逻辑,主要是判断用户是否是某个角色即可。所以首先定义一个注解,该注解有一个value,用于标识受保护的资源需要用户为哪个角色才允许访问。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zj.node.usercenter.auth;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * 被该注解标记的方法都需要检查用户权限
 *
 * @author 01
 * @date 2019-09-08
 **/
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckAuthorization {

    /**
     * 允许访问的角色名称
     */
    String value();
}

然后定义一个切面,用于实现具体的权限校验逻辑。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package com.zj.node.usercenter.auth;

import com.zj.node.usercenter.util.JwtOperator;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * 权限验证切面类
 *
 * @author 01
 * @date 2019-09-08
 **/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AuthAspect {

    private static final String TOKEN_NAME = "X-Token";

    private final JwtOperator jwtOperator;

    /**
     * 在执行@CheckAuthorization注解标识的方法之前都会先执行此方法
     */
    @Around("@annotation(com.zj.node.usercenter.auth.CheckAuthorization)")
    public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取request对象
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 从header中获取Token
        String token = request.getHeader(TOKEN_NAME);

        // 校验Token是否合法
        Boolean isValid = jwtOperator.validateToken(token);
        if (BooleanUtils.isFalse(isValid)) {
            log.warn("登录态校验不通过,无效的Token:{}", token);
            // 抛出自定义异常
            throw new SecurityException("Token不合法!");
        }

        Claims claims = jwtOperator.getClaimsFromToken(token);
        String role = (String) claims.get("role");
        log.info("登录态校验通过,用户名:{}", claims.get("userName"));

        // 验证用户角色名称是否与受保护资源所定义的角色名称匹配
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckAuthorization annotation = signature.getMethod()
                .getAnnotation(CheckAuthorization.class);
        if (!annotation.value().equals(role)) {
            log.warn("权限校验不通过!当前用户角色:{} 允许访问的用户角色:{}",
                    role, annotation.value());
            // 抛出自定义异常
            throw new SecurityException("权限校验不通过,无权访问该资源!");
        }

        log.info("权限验证通过");
        // 设置用户信息到request里
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("userName", claims.get("userName"));
        request.setAttribute("role", claims.get("role"));

        return joinPoint.proceed();
    }
}

使用的时候只需要加上该注解并且设置角色名称即可,如下示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * 需要校验登录态及权限后才能访问的资源
 */
@GetMapping("/{id}")
@CheckAuthorization("admin")
public User findById(@PathVariable Integer id) {
    log.info("get request. id is {}", id);
    return userService.findById(id);
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019/09/08 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
微服务的用户认证与授权杂谈(上)
几乎绝大部分的应用都需要实现认证与授权,例如用户使用账户密码登录就是一个认证过程,认证登录成功后系统才会允许用户访问其账户下的相关资源,这就是所谓的授权。而复杂点的情况就是用户会有角色概念,每个角色所拥有的权限不同,给用户赋予某个角色的过程也是一个授权过程。
端碗吹水
2020/09/23
2.1K0
微服务的用户认证与授权杂谈(上)
微服务[学成在线] day18:基于oauth2实现RBAC认证授权、微服务间认证实现
2、前端携带 token 请求用户中心服务获取jwt令牌,前端获取到jwt令牌解析,并存储在sessionStorage
LCyee
2020/08/05
3.4K0
微服务[学成在线] day18:基于oauth2实现RBAC认证授权、微服务间认证实现
022.自定义注解
注解是Jdk1.5新增新技术。很多框架为了简化代码,都会提供有些注解。 可以理解为插件,是代码级别的插件,在类的方法上写:@XXX,就是在代码上插入了一个插件。 注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用。
qubianzhong
2018/12/14
4260
Spring Cloud Alibaba - 15 微服务之间使用Feign实现参数的透传
https://github.com/yangshangwei/SpringCloudAlibabMaster
小小工匠
2022/02/04
7900
Spring Cloud Alibaba - 15 微服务之间使用Feign实现参数的透传
SaaS-基于JWT的API鉴权
如果我们每个方法都去写一段代码,冗余度太高,不利于维护,那如何做使我们的代码看起来更清爽呢?我们可以将这段代码放入拦截器去实现
cwl_java
2020/01/02
1.1K0
权限管理与Shiro入门(六)
用户1289394
2023/10/03
1530
权限管理与Shiro入门(六)
说说SpringBoot以及微服务中的几种鉴权方式
这个方法是利用Spring-AOP的机制,进行鉴权,可以使用execution进行鉴权
Karos
2023/12/15
1.2K0
说说SpringBoot以及微服务中的几种鉴权方式
各个微服务认证授权的处理方法
1.通过认证授权模块进行登录 在这里我们默认登录都是可以正常获取token令牌的,也就是都是登录成功的 2.微服务网关(SpringGateway) 在该微服务中我们通过拦截器链接请求,通过该请求是排除过滤的 uri 地址(例如:登录请求/auth/login),则放行该请求,否则则进行token认证,在这里我们有2种认证方式: 2.1 网关统一认证授权(本次不介绍该模式) 该模式需要使用redis进行缓存所有的认证路径和所有路径所需要的角色权限信息,最后在网关中统一鉴权,其他微服务不进行鉴权处理。
Reset
2022/12/27
8610
使用 Feign 实现微服务之间的认证和授权
在微服务架构中,认证和授权是保障系统安全和可靠性的重要手段。使用Feign实现微服务之间的认证和授权,可以有效地提高系统的安全性和可维护性。
堕落飞鸟
2023/04/08
4K2
Java实现JWT的Token认证机制[通俗易懂]
JWT是基于token的身份认证的方案。 json web token全称。可以保证安全传输的前提下传送一些基本的信息,以减轻对外部存储的依赖,减少了分布式组件的依赖,减少了硬件的资源。 可实现无状态、分布式的Web应用授权,jwt的安全特性保证了token的不可伪造和不可篡改。 本质上是一个独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以及您可以存储任何其他信息(自包含)。任何人都可以轻松读取和解析,并使用密钥来验证真实性。
全栈程序员站长
2022/08/04
5.2K0
Token认证
文章目录 1. 认证机制 1.1. 常见的几种认证机制 1.1.1. HTTP Basic Auth 1.1.2. OAuth(开放授权) 1.1.3. Cookie/Session 认证机制 1.1.4. 基于 Token 的认证机制 1.1.5. 有状态服务和无状态服务 1.2. 基于JWT(JSON WEB TOKEN)的Token认证机制实现 1.2.1. 头部(Header) 1.2.2. 载荷(Payload) 1.2.3. 签名(Signature) 1.3. JJWT 1.3.1. 添加
爱撒谎的男孩
2019/12/31
2.2K0
微服务通信密码,OpenFeign如何实现透明、高效的接口调用与协同
在微服务架构的世界里,服务间的顺畅通信至关重要。OpenFeign,作为Spring Cloud生态系统中的一颗璀璨明珠,以其声明式的HTTP客户端特性,极大地简化了微服务间的交互。本文将带您深入探索OpenFeign的核心原理,并结合实际案例,剖析其在日常开发中的应用场景,助力您在微服务通信中更加得心应手。
星辰大海的精灵
2024/04/11
5410
跟我学Springboot开发后端管理系统8:Matrxi-Web权限设计实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hlz5kP4-1590667445450)(https://ws3.sinaimg.cn/large/813fb3c3gy1g73tw6y311j20fu0cemxg.jpg)]
方志朋
2022/01/06
6600
java小技能:JWT(json web token)认证实现
引言 应用场景: 登录授权,它相比原先的session、cookie来说,更快更安全,跨域也不再是问题。 传递数据 I. 预备知识 1.1 关键字去空格处理 错误代码 (keyword+"").
公众号iOS逆向
2022/12/19
1.4K0
java小技能:JWT(json web token)认证实现
java架构之路-(微服务专题)feign的基本使用和nacos的配置中心
  上次我们说了ribbon的基本使用,包括里面的内部算法,算法的细粒度配置,还有我们自己如何实现我们自己的算法,主要还是一些基本使用的知识,还不会使用ribbon的小伙伴可以回去看一下上一篇博客。
小菜的不能再菜
2020/02/25
9270
java架构之路-(微服务专题)feign的基本使用和nacos的配置中心
微服务服务间调用组件Feign使用介绍、原理、优化技巧
Feign是一个声明式的Web Service客户端。它让微服务之间的调用变得更简单。Feign具有可插拔式的注解支持,包括Feign 注解和JAX-RS注解。Feign还支持可插拔的编码器和解码器。Spring Cloud增加了对Spring MVC注解的支持,并且也支持Spring WebFlux。
青山师
2023/10/17
10K0
SpringCloud 微服务实战笔记
这是很早以前在我的博客上写的关于 SpringCloud 的一些实战笔记,现在我把这些实战笔记集合起来贴到这里,可能会对一些刚刚接触 SpringCloud 微服务的小伙伴有帮助。
张乘辉
2019/11/11
6790
SpringCloud 微服务实战笔记
1. feign的使用及原理
  Feign 的英文表意为“假装,伪装,变形”, 是一个http请求调用的轻量级框架,可以以Java接口注解的方式调用Http请求,而不用像Java中通过封装HTTP请求报文的方式直接调用。Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。
用户7798898
2020/09/27
2.3K0
1. feign的使用及原理
Spring Cloud Feign如何实现JWT令牌中继以传递认证信息
令牌中继(Token Relay)是比较正式的说法,说白了就是让Token令牌在服务间传递下去以保证资源服务器能够正确地对调用方进行鉴权。
码农小胖哥
2021/11/02
1.6K0
架构设计:微服务模式下,实现灰度发布模式
请求通过8001服务,在灰度规则中,会读取下次请求的服务列表,根据版本号参数规则,选中路由的服务。
知了一笑
2020/11/24
9010
推荐阅读
相关推荐
微服务的用户认证与授权杂谈(上)
更多 >
领券
💥开发者 MCP广场重磅上线!
精选全网热门MCP server,让你的AI更好用 🚀
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验