Loading

51-OAuth2&JWT

微服务统一认证方案 Spring Cloud OAuth2 + JWT

1. 微服务架构下统一认证

1.1 认证场景

认证:验证用户的合法身份,比如输入用户名和密码,系统会在后台验证用户名和密码是否合法,合法的前提下,才能够进行后续的操作,访问受保护的资源。

分布式系统的每个服务都会有认证需求,如果每个服务都实现一套认证逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证的请求。

1.2 认证思路

(1)基于 Session 的认证方式

在分布式的环境下,基于 Session 的认证会出现一个问题,每个应用服务都需要在 Session 中存储用户身份信息,通过负载均衡将本地的请求分配到另一个应用服务需要将 Session 信息带过去,否则会重新认证。我们可以使用 Session 共享、Session 黏贴等方案。

Session方案也有缺点,比如基于 Cookie、移动端不能有效使用等。

(2)基于 token 的认证方式

基于 token 的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把 token 存在任意地方,并且可以实现 web 和 app 统一认证机制。

其缺点也很明显,token 由于自包含信息,因此一般数据量较大,而且每次请求都需要传递,因此比较占带宽。另外,token 的签名验签操作也会给 CPU 带来额外的处理负担。

2. OAuth2 开放授权协议/标准

2.1 OAuth2 介绍

OAuth(开放授权)是一个开放协议/标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。

结合“使用 QQ 登录拉勾”这个场景拆分理解上述这句话:

  • 用户:我们自己
  • 第三方应用:拉勾网
  • 另外的服务提供者:QQ

OAuth2 是 OAuth 协议的延续版本,但不兼容 OAuth1 即完全废弃了 OAuth1。

2.2 OAuth2 协议角色和流程

拉勾网要开发使用 QQ 登录这个功能的话,那么拉勾网是需要提前到 QQ 平台进行登记的:

  1. 拉勾网 — 登记 —> QQ 平台
  2. QQ 平台会颁发一些参数给拉勾网,后续上线进行授权登录的时候(刚才打开授权页面)需要携带这些参数
    • client_id :客户端 id(QQ 最终相当于一个认证授权服务器,拉勾网就相当于一个客户端了,所以会给一个客户端 id),相当于账号;
    • secret:相当于密码

  • 资源所有者(Resource Owner):可以理解为用户自己;
  • 客户端(Client):我们想登陆的网站或应用,比如拉勾网;
  • 认证服务器(Authorization Server):可以理解为微信或者 QQ;
  • 资源服务器(Resource Server):可以理解为微信或者 QQ。

2.3 什么情况下需要使用 OAuth2?

  • 【第三方授权登录的场景】比如,我们经常登录一些网站或者应用的时候,可以选择使用第三方授权登录的方式,比如:微信授权登录、QQ 授权登录、微博授权登录等,这是典型的 OAuth2 使用场景。
  • 【单点登录的场景】如果项目中有很多微服务或者公司内部有很多服务,可以专门做一个认证中心(充当认证平台角色),所有的服务都要到这个认证中心做认证,只做一次登录,就可以在多个授权范围内的服务中自由穿行。

2.4 OAuth2 的颁发 token 授权方式

  1. 授权码(authorization-code)
  2. 密码式(password)
  3. 隐藏式(implicit)
  4. 客户端凭证(client credentials)

授权码模式使用到了回调地址,微博、微信、QQ 等第三方登录就是这种模式。我们重点讲解接口对接中常使用的密码模式(提供用户名 + 密码换取 token)。

3. Spring Cloud OAuth2

3.1 简单说明

Spring Cloud OAuth2 是 Spring Cloud 体系对 OAuth2 协议的实现,可以用来做多个微服务的统一认证(验证身份合法性)授权(验证权限)。通过向 OAuth2 服务(统一认证授权服务)发送某个类型的 grant_type 进行集中认证和授权,从而获得 access_token(访问令牌),而这个 token 是受其他微服务信任的。

注意:使用 OAuth2 解决问题的本质是,引入了一个认证授权层,认证授权层连接了资源的拥有者,在授权层里面,资源的拥有者可以给第三方应用授权去访问我们的某些受保护资源。

3.2 构建微服务统一认证服务思路

【注意】在我们统一认证的场景中,Resource Server 其实就是我们的各种受保护的微服务,微服务中的各种 API 访问接口就是资源,发起 HTTP 请求的浏览器就是 Client 客户端(对应为第三方应用)。

3.3 搭建认证服务器

认证服务器(Authorization Server),负责颁发 token。

a. 基础构建

  • 新建微服务:cloud-oauth-server-9999
  • 配置文件无特别之处
  • 入口类特殊之处
  • 引入相关依赖
    <groupId>io.tree6x7</groupId>
    <artifactId>study_oauth2</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <!--spring boot 父启动器依赖-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
    </parent>
    
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.1.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.4.RELEASE</version>
        </dependency>
    </dependencies>
    

b. 相关配置

(1)SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 注册一个认证管理器对象到容器
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    /**
     * 密码编码对象(密码不进行加密处理)
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    /**
     * 处理用户名和密码验证事宜
     * 1)客户端传递username和password参数到认证服务器
     * 2)一般来说,username和password会存储在数据库中的用户表中
     * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中
        // 实例化一个用户对象(相当于数据表中的一条用户记录)
        UserDetails user = new User("admin", "123456", new ArrayList<>());
        auth.inMemoryAuthentication()
                .withUser(user).passwordEncoder(passwordEncoder);
    }
}

(2)OauthServerConfig


/**
 * @author 6x7
 * @date 2022/08/25 14:49
 * @description 核心配置类!
 * - 继承 AuthorizationServerConfigurerAdapter → 当前类为 Oauth2 的配置类
 * - 增加 @EnableAuthorizationServer → 开启认证服务器功能
 */
@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {


    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 客户端详细配置,如 client_id、secret。
     * 当前这个服务就如同 QQ 平台,现在你的网站作为客户端需要 QQ 平台进行登录授权认证等,
     * 需要提前到 QQ 平台注册,QQ 平台会给拉勾网颁发 client_id 等必要参数来标识客户端是谁。
     *
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                // 从内存中加载客户端
                .inMemory()
                // 添加一个 client 配置
                .withClient("client_cloud")
                // 指定客户端 secret
                .secret("tree6x7")
                // 指定客户端所能访问资源id
                .resourceIds("res1")
                // 认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,
                // 具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定
                .authorizedGrantTypes("password", "refresh_token")
                // 客户端的权限范围,此处配置为all,即全部
                .scopes("all");
    }

    /**
     * 认证服务器最终是以API接口的方式对外提供服务(校验合法性并生成令牌、校验令牌...)
     * 那么,以API接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行一些配置
     *
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 允许客户端表单认证
                .allowFormAuthenticationForClients()
                // 开启端口 /oauth/token_key 的访问权限
                .tokenKeyAccess("permitAll()")
                // 开启端口 /oauth/check_token 的访问权限
                .checkTokenAccess("permitAll()");
    }

    /**
     * 配置token令牌管理相关(token 此时就是一个字符串,当下的 token
     * 需要在服务端存储,而存储相关的配置,就是在这里配置的)
     *
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                // token生成细节的描述,如token有效期等
                .tokenServices(authorizationServerTokenServices())
                // 指定认证管理器,随后注入一个到当前类使用即可AuthenticationManager
                .authenticationManager(authenticationManager)
                // 请求类型
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
    }

    /**
     * 令牌存储对象 —— token 以什么形式存储
     *
     * @return
     */
    private TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

    private AuthorizationServerTokenServices authorizationServerTokenServices() {
        // 使用默认实现
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        // 是否启动令牌刷新
        tokenServices.setSupportRefreshToken(true);
        // 指定 token 的存储方式
        tokenServices.setTokenStore(tokenStore());
        // 设置令牌的有效时间
        tokenServices.setAccessTokenValiditySeconds(20);
        // 设置刷新令牌的有效时间
        tokenServices.setRefreshTokenValiditySeconds(259200);
        return tokenServices;
    }
}

c. API 测试

(1)生成 token

e.g. http://localhost:9999/oauth/token?client_secret=tree6x7&grant_type=password&username=admin&password=123456&client_id=client_cloud

获取 token 携带的参数:

  • client_id:客户端 id
  • client_secret:客户端 secret
  • grant_type:指定使用哪种颁发类型
  • username:用户名
  • password:密码

(2)校验 token

e.g. http://localhost:9999/oauth/check_token?token=7367dd81-3a27-420f-8df6-a17a97167610

(3)刷新 token

e.g. http://localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_cloud&client_secret=tree6x7&refresh_token=6799c456-216d-4890-9ee1-0c55ff6d5819

3.4 配置资源服务器

即希望请求是被认证过的微服务。

a. 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
</dependency>

b. 配置

ResourceServerConfig

@Configuration
@EnableResourceServer  // 开启资源服务器功能
@EnableWebSecurity     // 开启 web 访问安全
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

  /**
   * 该方法用于定义资源服务器向远程认证服务器发起请求,进行 token 校验等事宜
   * @param resources
   * @throws Exception
   */
  @Override
  public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    // 设置当前资源服务的资源 id
    resources.resourceId("res1");
    // 定义 token 服务对象(token 校验就应该靠 token 服务对象)
    RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
    // 校验端点/接口设置
    remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:9999/oauth/check_token");
    // 携带客户端 id 和客户端 secret
    remoteTokenServices.setClientId("client_cloud");
    remoteTokenServices.setClientSecret("tree6x7");
    // 设置到 resources!
    resources.tokenServices(remoteTokenServices);
  }


  /**
   * [场景] 一个服务中可能有很多资源(API接口)
   *   - 某一些API接口,需要先认证,才能访问
   *   - 某一些API接口,压根就不需要认证,本来就是对外开放的接口
   * 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证。
   *
   * @param http
   * @throws Exception
   */
  @Override
  public void configure(HttpSecurity http) throws Exception {
    http
      // 设置 session 的创建策略(根据需要创建即可)
      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
      .and()
      .authorizeRequests()
      .antMatchers("/test/**").authenticated()  // test 为前缀的请求需要认证
      .antMatchers("/demo/**").authenticated()  // demo 为前缀的请求需要认证
      .anyRequest().permitAll();                // 其他请求不认证
  }

}

4. 引入 JWT 进行改造

4.1 问题&解决方案

通过上边的测试我们发现,当资源服务和授权服务不在一起时资源服务使用 RemoteTokenServices 远程请求授权服务验证 token,如果访问量较大将会影响系统的性能。

解决上边问题:令牌采用 JWT 格式即可解决上边的问题,用户认证通过会得到一个 JWT 令牌,JWT 令牌中已经包括了用户相关的信息,客户端只需要携带 JWT 访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递 JSON 对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA 的公钥/私钥对来签名,防止被篡改。

4.2 JWT 令牌结构

JWT 令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz。

a. Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如 HMAC、SHA256 或 RSA),例如:

{
    "alg": "HS256",
    "typ": "JWT"
}

将上边的内容使用 Base64Url 编码,得到一个字符串,就是 JWT 令牌的第 1 部分。

b. Payload

第⼆部分是负载,内容也是一个 JSON 对象,它是存放有效信息的地方,它可以存放 JWT 提供的现成字段,比如:iss(签发者)、exp(过期时间戳)、sub(面向的用户)等,也可自定义字段。 此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

{
    "sub": "1234567890",
    "name": "John Doe",
    "iat": 1516239022
}

最后将第二部分负载使用 Base64Url 编码,得到一个字符串,就是 JWT 令牌的第 2 部分。

c. Signature

第三部分是签名,此部分用于防止 JWT 内容被篡改。 这个部分使用 Base64url 将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用 header 中声明的签名算法进行签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
  • base64UrlEncode(header):JWT 令牌的第 1 部分;
  • base64UrlEncode(payload):JWT 令牌的第 2 部分;
  • secret:签名所使用的密钥。

4.3 改造令牌存储机制

a. 统一认证授权中心

OauthServerConfig

private TokenStore tokenStore() {
    // return new InMemoryTokenStore();
    // => 使用 JWT 令牌
    return new JwtTokenStore(jwtAccessTokenConverter());
}

private final String SIGN_KEY = "cloud-oauth-jwt";

/**
 * => 返回jwt令牌转换器(帮助我们生成jwt令牌的)
 * 在这里,我们可以把签名密钥传递进去给转换器对象
 * @return
 */
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
    jwtAccessTokenConverter.setSigningKey(SIGN_KEY);
    jwtAccessTokenConverter.setVerifier(new MacSigner(SIGN_KEY));
    return jwtAccessTokenConverter;
}

private AuthorizationServerTokenServices authorizationServerTokenServices() {
    // 使用默认实现
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    // 是否启动令牌刷新
    tokenServices.setSupportRefreshToken(true);
    // 指定 token 的存储方式
    tokenServices.setTokenStore(tokenStore());
    // 设置令牌的有效时间
    tokenServices.setAccessTokenValiditySeconds(20);
    // 设置刷新令牌的有效时间
    tokenServices.setRefreshTokenValiditySeconds(259200);
    // => 针对 JWT 令牌的添加
    tokenServices.setTokenEnhancer(jwtAccessTokenConverter());
    return tokenServices;
}

b. 资源服务器

ResourceServerConfig

private TokenStore tokenStore() {
    // return new InMemoryTokenStore();
    // => 使用 JWT 令牌
    return new JwtTokenStore(jwtAccessTokenConverter());
}

private final String SIGN_KEY = "cloud-oauth-jwt";

/**
 * => 返回jwt令牌转换器(帮助我们生成jwt令牌的)
 * 在这里,我们可以把签名密钥传递进去给转换器对象
 * @return
 */
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
    jwtAccessTokenConverter.setSigningKey(SIGN_KEY);
    jwtAccessTokenConverter.setVerifier(new MacSigner(SIGN_KEY));
    return jwtAccessTokenConverter;
}

/**
 * 该方法用于定义资源服务器向远程认证服务器发起请求,进行 token 校验等事宜
 * @param resources
 * @throws Exception
 */
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
    /* 之前代码全部注掉 */

    // JWT 令牌改造
    resources.resourceId("res1").tokenStore(tokenStore()).stateless(true);
}

5. 引入数据库进行改造

5.1 验证客户端信息

(1)创建如下数据表并初始化数据(表名及字段保持固定);

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
    `client_id` varchar(48) NOT NULL,
    `resource_ids` varchar(256) DEFAULT NULL,
    `client_secret` varchar(256) DEFAULT NULL,
    `scope` varchar(256) DEFAULT NULL,
    `authorized_grant_types` varchar(256) DEFAULT NULL,
    `web_server_redirect_uri` varchar(256) DEFAULT NULL,
    `authorities` varchar(256) DEFAULT NULL,
    `access_token_validity` int(11) DEFAULT NULL,
    `refresh_token_validity` int(11) DEFAULT NULL,
    `additional_information` varchar(4096) DEFAULT NULL,
    `autoapprove` varchar(256) DEFAULT NULL,
    PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
BEGIN;
INSERT INTO `oauth_client_details` VALUES (
    'oauth2_client_tree1101', 'res1,res2', '123456789', 'all',
    'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL
);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

(2)数据库相关依赖、相关配置,略过;

(3)主配置类 OauthServerConfig 改造

@Autowired
private DataSource dataSource;

@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
  JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
  return jdbcClientDetailsService;
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  /* 从内存中加载客户端详情 */

  clients.withClientDetails(createJdbcClientDetailsService());
}

5.2 验证用户合法性

(1)创建数据表 users(表名不需固定),初始化数据;

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `username` char(10) DEFAULT NULL,
    `password` char(100) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of users
-- ----------------------------
BEGIN;
INSERT INTO `users` VALUES (1, 'liujiaqi', 'abcdef');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;

(2)操作数据表的 JPA 配置以及针对该表的操作的 Dao 接口此处省略;

(3)开发 UserDetailsService 接口的实现类,根据用户名从数据库加载用户信息;

@Service
public class JdbcUserDetailsService implements UserDetailsService {

  @Autowired
  private UsersRepository usersRepository;

  /**
   * 根据 username 查询出该用户的所有信息,封装成 UserDetails 类型的对象返回
   * @param username
   * @return
   * @throws UsernameNotFoundException
   */
  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Users users = usersRepository.findByUsername(username);
    return new User(users.getUsername(), users.getPassword(), new ArrayList<>());
  }
}

(4)SecurityConfig 使用用户自定义的用户详情查询组件;

@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;

/**
 * 处理用户名和密码验证事宜
 * 1)客户端传递username和password参数到认证服务器
 * 2)一般来说,username和password会存储在数据库中的用户表中
 * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(jdbcUserDetailsService).passwordEncoder(passwordEncoder);
}

6. 基于Oauth2的JWT令牌信息扩展

OAuth2 帮我们生成的 JWT 令牌载荷部分信息有限,关于用户信息只有一个 user_name,有些场景下我们希望放入一些扩展信息项,比如,之前我们经常向 session 中存入 userId,或者现在我希望在 JWT 的载荷部分存入当时请求令牌的客户端 IP,客户端携带令牌访问资源服务时,可以对比当前请求的客户端真实 IP 和令牌中存放的客户端 IP 是否匹配,不匹配拒绝请求,以此进一步提高安全性。那么如何在 OAuth2 环境下向 JWT 令牌中存入扩展信息?

6.1 认证服务器放入信息

生成 JWT 令牌时存入扩展信息

(1)继承 DefaultAccessTokenConverter 类,重写 convertAccessToken() 存入扩展信息;

@Component
public class MyAccessTokenConvertor extends DefaultAccessTokenConverter {
  @Override
  public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getRequest();
    String remoteAddr = request.getRemoteAddr();
    Map<String, String> stringMap = (Map<String, String>) super.convertAccessToken(token, authentication);
    stringMap.put("clientIp", remoteAddr);
    return stringMap;
  }
}

(2)改造 OauthServerConfig

@Autowired
private MyAccessTokenConvertor myAccessTokenConvertor;

/**
 * 返回jwt令牌转换器(帮助我们生成jwt令牌的)
 * 在这里,我们可以把签名密钥传递进去给转换器对象
 * @return
 */
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
    jwtAccessTokenConverter.setSigningKey(SIGN_KEY);
    jwtAccessTokenConverter.setVerifier(new MacSigner(SIGN_KEY));
    // => 配置转换器
    jwtAccessTokenConverter.setAccessTokenConverter(myAccessTokenConvertor);
    return jwtAccessTokenConverter;
}

6.2 资源服务器获取信息

资源服务器也需要自定义一个转换器类,继承 DefaultAccessTokenConverter,重写 extractAuthentication() 提取方法,把载荷信息设置到认证对象的 details 属性中。

@Component
public class MyAccessTokenConvertor extends DefaultAccessTokenConverter {
    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        OAuth2Authentication oAuth2Authentication = super.extractAuthentication(map);
        oAuth2Authentication.setDetails(map);
        return oAuth2Authentication;
    }
}

业务类比如 Controller 类中,可以通过 SecurityContextHolder.getContext().getAuthentication() 获取到认证对象,进一步获取到扩展信息。

获取到扩展信息后,就可以做其他的处理了,比如根据 userId 进一步处理,或者根据上述示例中存入的 clientIP 处理,或者其他都是可以的了~

7. 补充

关于 JWT 令牌我们需要注意:

  • JWT 令牌就是一种可以被验证的数据组织格式,它的玩法很灵活,我们这里是基于 Spring Cloud Oauth2 创建、校验 JWT 令牌的,我们也可以自己写工具类生成、校验 JWT 令牌;
  • JWT 令牌中不要存放过于敏感的信息,因为我们知道拿到令牌后,我们可以解码看到载荷部分的信息;
  • JWT 令牌每次请求都会携带,内容过多,会增加网络带宽占用;
  • 以 JWT 为例介绍单点登录的解决方案:
    • 用户访问其他系统,会在网关判断 token 是否有效;
    • 如果 token 无效则会返回 401(认证失败)前端跳转到登录页面;
    • 用户发送登录请求,返回浏览器一个 token,浏览器把 token 保存到 cookie;
    • 再去访问其他服务的时候,都需要携带 token,由网关统一验证后路由到目标服务。
posted @ 2022-04-10 16:48  tree6x7  阅读(49)  评论(0编辑  收藏  举报