CodeAshen's blog CodeAshen's blog
首页
  • Spring Framework

    • 《剖析Spring5核心原理》
    • 《Spring源码轻松学》
  • Spring Boot

    • Spring Boot 2.0深度实践
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
  • RabbitMQ
  • RocketMQ
  • Kafka
  • MySQL8.0详解
  • Redis从入门到高可用
  • Elastic Stack
  • 操作系统
  • 计算机网络
  • 数据结构与算法
  • 云原生
  • Devops
  • 前端
  • 实用工具
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
  • Reference
GitHub (opens new window)

CodeAshen

后端界的小学生
首页
  • Spring Framework

    • 《剖析Spring5核心原理》
    • 《Spring源码轻松学》
  • Spring Boot

    • Spring Boot 2.0深度实践
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
  • RabbitMQ
  • RocketMQ
  • Kafka
  • MySQL8.0详解
  • Redis从入门到高可用
  • Elastic Stack
  • 操作系统
  • 计算机网络
  • 数据结构与算法
  • 云原生
  • Devops
  • 前端
  • 实用工具
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
  • Reference
GitHub (opens new window)
  • 剖析Spring5核心原理

  • Spring源码轻松学

  • Spring Boot 2.0深度实践

  • Spring-Cloud

  • Spring-Cloud-Alibaba

    • 第01章-简介
    • 第02章-Nacos服务发现
    • 第03章-Ribbon负载均衡
    • 第04章-Feign声明式HTTP客户端
    • 第05章-Sentinel集群容错
    • 第06章-RocketMQ消息队列
    • 第07章-Spring Cloud Gateway网关
    • 第08章-认证授权
      • 8.1 有状态VS无状态
      • 8.2 微服务认证方案
        • 8.2.1 “处处安全”方案
        • 8.2.2 “外部无状态,内部有状态”方案
        • 8.2.3 “网关认证授权,内部裸奔”方案
        • 8.2.4 “内部裸奔”改进方案
        • 8.2.5 认证方案对比
      • 8.3 访问控制模型
      • 8.4 JWT
      • 8.5 微信登录认证
      • 8.6 登录状态校验
      • 8.7 Feign传递token
      • 8.9 RestTemplate传递token
    • 第09章-Nacos配置管理
    • 第10章-Sletuh调用链
  • Spring
  • Spring-Cloud-Alibaba
CodeAshen
2023-02-10
目录

第08章-认证授权

# 8.1 有状态VS无状态

image-20210304135850349

如图所示,当应用有多个实例的时候,将session存储到一个中央存储中去(Session Store),常常使用Redis或MemCache。

Tips

也可使用粘性会话,即:对相同IP的请求,NGINX总会转发到相同的Tomcat实例,这样就就无需图中的Session Store了。不过这种方式有很多缺点:比如用户断网重连,刷新页面,由于IP变了,NGINX会转发到其他Tomcat实例,而其他实例没有Session,于是就认为用户未登录。这让用户莫名其妙。 粘性会话不是本章重点,如果感兴趣可以百度一下(用得越来越少了)。

这种统一session管理的方式有些显著缺点,如果Session Store挂了,所有系统全部完蛋;如果Session Store需要迁移,则别的实例都需要改连接地址;如果Session Store达到了容量或性能瓶颈,就需要为其提供解决方案。

后来的发展趋势,特别是微服务流行起来之后,越来越多的提到无状态,所谓无状态是只服务器端不记录用户的登录状态,更直观的讲,服务器不再维护session了。

image-20210304140943938

无状态的玩法是这样的,服务端不存储用户的登录状态,而是在用户登录的时候颁发一个Token给用户,之后用户的每个请求都会带上这个Token,可以放在Header里传递,也可以放在URL参数里传递,服务端拿到Token校验Token的合法性时效性等。也可以在Token中传递一些不太敏感的用户信息,这样Token解析完服务器就能直接使用了。

这里讲的是解密Token直接拿到用户信息,事实上要看你项目的具体实现,有时候Token里不一定带有用户信息,而是利用Token到某个地方查询,才能获得用户信息。

有状态无状态优缺点对比:

优点 缺点
有状态 服务器端控制力强 存在中心点,鸡蛋在一个篮子里
迁移麻烦服务器端存储数据,加大了服务器端压力
无状态 去中心化
无存储,简单
任意扩容、缩容
服务器端控制力相对弱

# 8.2 微服务认证方案

# 8.2.1 “处处安全”方案

处处安全方案常用的协议是OAuth 2.0,OAuth2.0系列文章 (opens new window)

这种方案些的代表实现主要有:

  1. Spring Cloud Security

  2. Jboss Keycloak

    Keycloak功能强大,上手简单,维护方便,有图形化界面。但是是基于servlet模型的,所以无法和Spring Cloud Gateway配合使用。

处处安全优缺点:

  • 优点:安全性好
  • 缺点:实现成本比较高,由于存在多次Token交换和多次认证,带来了性能开销

参考:

  • OAuth2实现单点登录SSO (opens new window)

# 8.2.2 “外部无状态,内部有状态”方案

image-20210304143133254

这种方案中,网关不会存储Session,而是使用Token,而网关代理的微服务则使用了Session Store共享Session。

这种方案看起来非常怪异,其应用场景如下,在一个新老服务共存的庞大系统中,老服务使用网关传过来的JSESSIONID去Session Store中获取用户登录状态,新的微服务系统使用Token解密,这样可以一步步将传统架构重构成新的微服务架构。

image-20210304143358933

# 8.2.3 “网关认证授权,内部裸奔”方案

image-20210304143837056

请求在网关处做登录认证,登录成功颁发Token,之后的请求都携带Token,网关解密Token 网关可以将解密出user_id、username附加到请求header中,去请求后端微服务。这种方案下,网关说用户是谁,后端微服务就相信这是谁。

  • 优点:实现简单,性能好
  • 缺点:一旦网关被攻破就完了

# 8.2.4 “内部裸奔”改进方案

image-20210304151443873

请求经过网关去认证授权中心去登录,登录成功颁发Token,之后所有请求都携带该Token,网关并不操作Token,只是将Token传递给后端微服务,后端微服务解析Token来确定用户身份,微服务之间的调用也是同理。

优点:降低了网关的复杂度,网关不需要关心用户是谁了,不再揭秘解析Token,只做转发,并提高了一定的安全性 缺点:每个微服务都参与解密Token,这样知道密钥的人很多,泄密风险也相应变大

# 8.2.5 认证方案对比

方案 复杂度 安全性 性能 测试难度
处处安全 高 高 中等 难(一般做继承测试)
外部无状态,内部有状态 低 中 高 难(一般做继承测试)
内部裸奔 低 一般 高 简单(造Header即可实现接口测试)
内部裸奔改进版 低 高 高 中(造Token即可实现接口测试)

# 8.3 访问控制模型

  • Access Control List(ACL)
  • Role-based access control(RBAC)
  • Attribute-based access control(ABAC)
  • Rule-based access control
  • Time-based access control

RBAC访问控制模型:

image-20210304155613406

# 8.4 JWT

JWT全称Json web token,是一个开放标准(RFC 7519),用来在各方之间安全地传输信息。JWT可被验证和信任,因为它是数字签名的。

JWT组成:

组成 作用 内容实例
Header(头) 记录令牌类型、签名的算法等 {"alg":"HS256", "typ":"JWT"}
Payload(有效负载) 携带一些用户信息 {"userld":"1", "username":"damu"}
Signature(签名) 防止Token被篡改、确保安全性 计算出来的签名,一个字符串
  • Token算法:

    Token = Base64(Header).Base64(Payload).Base64(Signature)

    示例:aaaa.bbbbb.ccccc

  • 签名算法:

    Signature = Header指定的签名算法(Base64(header).Base64(payload),秘钥)

    秘钥:HS256("aaaa.bbbbb",秘钥)

参考:JWT工具类手记 (opens new window)

# 8.5 微信登录认证

在user-center中微信小程序登录认证代码,用到了微信开发的Java SDK(WxJava (opens new window)),使用步骤如下:

加依赖

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>4.0.0</version>
</dependency>
1
2
3
4
5

编写配置类

/**
 * 微信小程序相关Bean配置
 */
@Configuration
public class WxConfig {
    /**
     * 小程序配置
     */
    @Bean
    public WxMaConfig wxMaConfig() {
        WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
        config.setAppid("wx97xxxxxxxxxf181");
        config.setSecret("6dabxxxxxxxxxxxxxxxxxxxxxxxxd00c");
        return config;
    }
    
    /**
     * 小程序服务接口,提供各种api
     */
    @Bean
    public WxMaService wxMaService() {
        WxMaServiceImpl wxMaService = new WxMaServiceImpl();
        wxMaService.setWxMaConfig(wxMaConfig());
        return wxMaService;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

业务代码:

UserController

@Resource
private WxMaService wxMaService;
@Resource
private JwtOperator jwtOperator;

@PostMapping("/login")
public LoginRespDTO login(@RequestBody UserLoginDTO loginDTO) throws WxErrorException {
    // 微信小程序服务端校验是否已登录
    WxMaJscode2SessionResult result = wxMaService.getUserService().getSessionInfo(loginDTO.getCode());
    // 微信的OpenId,微信端唯一标识
    String openid = result.getOpenid();

    // String openid = "xxx";  // 注释上述代码,放开本行,接口调试用
    // 看用户是否注册,未注册就插入用户信息,注册了就颁发Token
    User user = userService.login(loginDTO, openid);

    // 颁发Token
    Map<String, Object> userInfo = new HashMap<>();
    userInfo.put("id", user.getId());
    userInfo.put("wxNickname", user.getWxNickname());
    userInfo.put("role", user.getRoles());
    String token = jwtOperator.generateToken(userInfo);
    log.info("用户{}登录成功,生成token={},有效期至{}", user.getWxNickname(), token, jwtOperator.getExpirationTime());

    return LoginRespDTO.builder()
        .user(UserRespDTO.builder()
              .id(user.getId())
              .avatarUrl(user.getAvatarUrl())
              .bonus(user.getBonus())
              .wxNickname(user.getWxNickname())
              .build())
        .token(JwtTokenRespDTO.builder()
               .expirationTime(jwtOperator.getExpirationTime().getTime())
               .token(token)
               .build())
        .build();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

UserService

public User login(UserLoginDTO loginDTO, String openId) {
    User user = userMapper.selectOne(
        User.builder().wxId(openId).build());

    if (user == null) {
        User userToSave = User.builder()
            .wxId(openId).wxNickname(loginDTO.getWxNickname()).avatarUrl(loginDTO.getAvatarUrl())
            .bonus(300).roles("user").createTime(new Date()).updateTime(new Date())
            .build();
        userMapper.insertSelective(userToSave);
        return userToSave;
    }

    return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

请求登录接口

# 8.6 登录状态校验

当请求进到服务端的时候,有以下几种方式时间实现登录状态的检查

  • Servlet过滤器
  • Spring MVC拦截器:可以方便的调用Spring MVC的api
  • Spring AOP:代码简洁,可插拔,注解切面,不需要校验的地方不写注解即可

加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1
2
3
4

写注解切面

@Slf4j
@Aspect
@Component
public class CheckLoginAspect {
    
    @Resource
    private JwtOperator jwtOperator;
    
    @Around("@annotation(com.lucifer.usercenter.auth.CheckLogin)")
    public Object checkLogin(ProceedingJoinPoint point) throws Throwable {
        // 1.从header中获取token
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("X-Token");

        // 2.校验token有效性和时效性
        if (StringUtils.isBlank(token) || !jwtOperator.validateToken(token)) {
            log.warn("token check failed, token={}", token);
            throw new RuntimeException("无效Token");
        }
        
        // 3.如果校验成功,就将解析token获取用户信息,存到request的attribute中
        Claims claims = jwtOperator.getClaimsFromToken(token);
        request.setAttribute("id", claims.get("id"));
        request.setAttribute("wxNickname", claims.get("wxNickname"));
        request.setAttribute("role", claims.get("role"));

        log.warn("token check passed, token={}", token);
        return point.proceed();
    }
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 8.7 Feign传递token

每个微服务请求进来的时候都会校验请求头里的token,但是微服务之间使用Feign互相调用的时候不会传递token,就导致下游微服务校验token的时候不通过。为了解决这个问题有以下两种方式

  • @RequestHeader注解
  • RequestInterceptor拦截器

使用@RequestHeader注解对代码侵入性比较大,而且每个接口都要加,这里采用为Feign配置RequestInterceptor拦截器方式

编写拦截器

/**
 * Feign拦截器,用于传递token
 */
public class TokenRelayRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 1.从header中获取token
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest request = attributes.getRequest();
        String token = request.getHeader("X-Token");
        
        // 2. 传递token
        if (!StringUtils.isBlank(token)) {
            requestTemplate.header("X-Token", token);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

配置拦截器

feign:
  client:
    config: 
      # 想要调用的微服务名称,改成default即对所有服务生效
      default:
        # 配置全面默认拦截器
        requestInterceptors:
          - com.lucifer.contentcenter.feignclient.interceptor.TokenRelayRequestInterceptor
1
2
3
4
5
6
7
8

参考:

  • [4.3 Feign配置](#4.3 Feign配置)
  • 官方文档 (opens new window)

# 8.9 RestTemplate传递token

RestTemplate传递token有以下几种方式

  • HttpEntity中封装header
  • Spring的ClientHttpRequestInterceptor

方式一:

public class TestRestTemplate {
    @Autowired
    private RestTemplate restTemplate;
    public void postObject(String token){
        String url="http://www.baidu.com";
        User user = new User();
        // 构造请求头
        HttpHeaders header = new HttpHeaders();
        header.add("X-token", token);
        // 创建httpEntity
        HttpEntity<User> httpEntity = new HttpEntity<>(user, header);
        // 发送请求
        JSONObject response = restTemplate.postForObject(url, httpEntity, JSONObject.class);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

请求方式很多,除了xxxForObject,还有exchange,都是利用将header封装在HttpEntity中的方式实现携带请求头

方式二:

编写拦截器:

/**
 * 拦截器,可向RestTemplate中注册拦截器
 */
public class RelayTokenClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        // 1.取出原始请求头中的token
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest httpServletRequest = attributes.getRequest();
        String token = httpServletRequest.getHeader("X-Token");
        // 2.设置header
        HttpHeaders headers = request.getHeaders();
        headers.add("X-Token", token);
        // 继续执行(后面还有拦截器的话继续,责任链模式)
        return execution.execute(request, body);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

RestTemplate上配置拦截器

@Bean
@LoadBalanced  //负载均衡注解
@SentinelRestTemplate  // Sentinel整合RestTemplate
public RestTemplate restTemplate() {
    RestTemplate template = new RestTemplate();
    // 配置拦截器链
    template.setInterceptors(Collections.singletonList(
        new RelayTokenClientHttpRequestInterceptor()
    ));
    return template;
}
1
2
3
4
5
6
7
8
9
10
11
编辑 (opens new window)
上次更新: 2023/06/04, 12:34:19
第07章-Spring Cloud Gateway网关
第09章-Nacos配置管理

← 第07章-Spring Cloud Gateway网关 第09章-Nacos配置管理→

最近更新
01
第01章-RabbitMQ导学
02-10
02
第02章-入门RabbitMQ核心概念
02-10
03
第03章-RabbitMQ高级特性
02-10
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CodeAshen | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式