SpringCloud Alibaba 2021微服务实战十六 整合Oauth2

SpringCloud  Oauth2

 

概念

为了保证服务的安全性,往往都会在接口调用时做权限校验。在分布式架构中我们会把复杂的业务拆成多个微服务,这样不得不在所有服务中都实现这样的权限校验逻辑,这样就会有很多代码和功能冗余。所以在微服务架构中一般会独立出一个单独的认证授权服务,供其他所有服务调用。

在SpringCloud体系中,我们只对网关层开放外网访问权限,其他后端微服务做网络隔离,所有外部请求必须要通过网关才能访问到后端服务。在网关层对请求进行转发时先校验用户权限,判断用户是否有权限访问。

我们一般使用Oauth2.0来实现对所有后端服务的统一认证授权。

gateway作为程序服务的入口。做鉴权应用,流程如下:

 


Oauth2简介
OAuth 2.0是用于授权的行业标准协议。OAuth 2.0致力于简化客户端开发人员,同时为Web应用程序,桌面应用程序,移动电话和客厅设备提供特定的授权流程。该规范及其扩展正在IETF OAuth工作组内开发。

OAuth 2.0 的标准是 RFC 6749 文件。该文件先解释了 OAuth 是什么。OAuth 的核心就是向第三方应用颁发令牌。然后,RFC 6749 接着写道:它定义了获得令牌的四种授权方式(authorization grant )。


简单一点的诉述就是发起一个认证请求,根据授权类型去认证服务器认证,如果成功就返回token,再使用token去访问资源服务器,token验证通过就返回被保护的资源。

 

OAuth2的4种模式

客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

  • 密码模式(resource owner password credentials): 用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

  • 客户端模式(client credentials): 指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

  • 授权码模式(authorization code): 授权码模式,是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

  • 简化模式(implicit): 不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

 

OAuth2的角色

  • 资源拥有者(Resource Owner) - 例如:用户Tom

  • 资源服务器(Resource Server) - 例如:微信

  • 授权服务器(Authorization Server) - 这里是微信,因为微信有相关数据

  • 客户端(Client) - 这里是某第三方App或某应用

Oauth2 Token内容简介

Token基本内容如下

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是Bearer类型或其它类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。

密码模式使用的例子

以某App登陆为例,用户请求获取授权信息

+-----------+                                     +-------------+
|           |       1-Request Authorization       |             |
|           |------------------------------------>|             |
|           |     grant_type&username&password    |             |--+
|           |                                     |Authorization|  | 2-Gen
|  Client   |                                     |Service      |  |   JWT
|           |       3-Response Authorization      |             |<-+
|           |<------------------------------------| Private Key |
|           |    access_token / refresh_token     |             |
|           |    token_type / expire_in / jti     |             |
+-----------+                                     +-------------+

Spring OAuth2 + JWT Token简介

本例中Spring OAuth2中Token结构简介

key备注
access_tokenJWT Access Token,过期时间,默认12小时
refresh_tokenJWT Refresh Token,过期时间,默认30天
expires_in过期时间,单位秒
token_typeBearer和Mac
scoperead和write

JWT(JSON Web Tokens)简介

JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。

  • 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
  • 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。

简短来说,用户请求时,将用户信息和授权范围序列化后放入一个JSON字符串,然后使用Base64进行编码,最终在授权服务器用私钥对这个字符串进行签名,得到一个JSON Web Token,我们可以像使用Access Token一样的直接使用它,假设其他所有的资源服务器都将持有一个RSA公钥。当资源服务器接收到这个在Http Header中存有Token的请求,资源服务器就可以拿到这个Token,并验证它是否使用正确的私钥签名(是否经过授权服务器签名,也就是验签)。验签通过,反序列化后就拿到OAuth 2的验证信息。

  • Jwt Token包含了使用.分隔的三部分

{Header 头部}.{Payload 负载}.{Signature 签名}

  • Header 头部

JWT包含了使用.分隔的三部分: 通常包含了两部分,token类型和采用的加密算法

{
   "alg": "HS256",
   "typ": "JWT"
 }
  • Payload 负载

Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据。

{
   "user_name": "admin", 
   "scope": [
       "read"
   ], 
   "organization": "admin", 
   "exp": 1531975621, 
   "authorities": [
       "ADMIN"
   ], 
   "jti": "23408d38-8cdc-4460-beac-24c76dc7629a", 
   "client_id": "test_client"
}
  • Signature 签名

使用Base64编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。

Jwt Token例子:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiXSwib3JnYW5pemF0aW9uIjoiYWRtaW4iLCJleHAiOjE1MzE5NzU2MjEsImF1dGhvcml0aWVzIjpbIkFETUlOIl0sImp0aSI6IjIzNDA4ZDM4LThjZGMtNDQ2MC1iZWFjLTI0Yzc2ZGM3NjI5YSIsImNsaWVudF9pZCI6InRlc3RfY2xpZW50In0.qawS1Z4j_h4vNx10GBC_Y_PHM1LLSQt64eniWLGzsJY

可到http://www.bejson.com/enc/base64 解码,注意分3部分分别解, 也可使用官网解码工具官网解码

表结构简介

Spring OAuth2表结构

表名简介默认地址
oauth_client_detailsclient持久化表本例子中dml初使化了test_client
oauth_client_token用户客户端存储从服务端获取的token未使用,本例中均为服务端
oauth_access_tokenaccess_token的持久表未使用,本例中使用了jwt,无需持久化到服务器中
oauth_refresh_tokenrefresh_token的持久化表本例中使用了jwt
oauth_approvals授权码模式授权信息持久化表用户授权记录
oauth_code授权码模式code持久化表code临时存放,code使用过就删除

具体表结构请参考spring-oauth-server 数据库表说明

用户角色资源等表结构

表名简介备注
users用户表使用应用的用户
groups组织表通过user_group_relation与users关联,多对多
position岗位表通过user_position_relation与users关联,多对多
roles角色表通过user_role_relation与users关联,多对多
menu菜单表通过role_menu_relation与roles关联,多对多
resource资源表通过role_resource_relation与roles关联,多对多

Oauth2认证服务

建立认证服务Auth-Service

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>com.alibaba.cloud</groupId>
		<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-oauth2</artifactId>
	</dependency>
	<!--database-->
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
	</dependency>
	<dependency>
		<groupId>com.baomidou</groupId>
		<artifactId>mybatis-plus-boot-starter</artifactId>
	</dependency>
	<dependency>
		<groupId>com.jianzh5.cloud</groupId>
		<artifactId>cloud-common</artifactId>
	</dependency>
</dependencies>

引入mysql主要是我们需要将oauth2.0的客户端信息以及token认证存入数据库。

建立相关数据表

主要是导入oauth2相关数据表以及用户表,在实际开发过程中用户权限这一套应该是基于RBAC进行设计,这里为了方便演示我就直接只做一个用户表。

(oauth2.0相关表结构

SET NAMES utf8;

DROP DATABASE IF EXISTS sc_auth;
CREATE DATABASE sc_auth DEFAULT CHARSET utf8mb4;
USE sc_auth;

-- access_token存储表
DROP TABLE IF EXISTS oauth_access_token;
CREATE TABLE oauth_access_token
(
    token_id          VARCHAR(256) COMMENT 'MD5加密的access_token的值',
    token             BLOB COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
    authentication_id VARCHAR(256) COMMENT 'MD5加密过的username,client_id,scope',
    user_name         VARCHAR(256) COMMENT '登录的用户名',
    client_id         VARCHAR(256) COMMENT '客户端ID',
    authentication    BLOB COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据',
    refresh_token     VARCHAR(256) COMMENT 'MD5加密果的refresh_token的值'
) COMMENT '访问令牌表';

-- refresh_token存储表
DROP TABLE IF EXISTS oauth_refresh_token;
CREATE TABLE oauth_refresh_token
(
    token_id       VARCHAR(256) COMMENT 'MD5加密过的refresh_token的值',
    token          BLOB COMMENT 'OAuth2RefreshToken.java对象序列化后的二进制数据',
    authentication BLOB COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据'
) COMMENT '更新令牌表';

-- 授权记录表
DROP TABLE IF EXISTS oauth_approvals;
CREATE TABLE oauth_approvals
(
    userid         VARCHAR(256) COMMENT '登录的用户名',
    clientid       VARCHAR(256) COMMENT '客户端ID',
    scope          VARCHAR(256) COMMENT '申请的权限',
    status         VARCHAR(10) COMMENT '状态(Approve或Deny)',
    expiresat      DATETIME COMMENT '过期时间',
    lastmodifiedat DATETIME COMMENT '最终修改时间'
) COMMENT '授权记录表';

-- 授权码表
DROP TABLE IF EXISTS oauth_code;
CREATE TABLE oauth_code
(
    code           VARCHAR(256) COMMENT '授权码(未加密)',
    authentication BLOB COMMENT 'AuthorizationRequestHolder.java对象序列化后的二进制数据'
) COMMENT '授权码表';

-- client用户表
DROP TABLE IF EXISTS oauth_client_details;
CREATE TABLE oauth_client_details
(
    client_id               VARCHAR(256) NOT NULL COMMENT '客户端ID',
    resource_ids            VARCHAR(256) COMMENT '资源ID集合,多个资源时用逗号(,)分隔',
    client_secret           VARCHAR(256) COMMENT '客户端密匙',
    scope                   VARCHAR(256) COMMENT '客户端申请的权限范围',
    authorized_grant_types  VARCHAR(256) COMMENT '客户端支持的grant_type',
    web_server_redirect_uri VARCHAR(256) COMMENT '重定向URI',
    authorities             VARCHAR(256) COMMENT '客户端所拥有的Spring Security的权限值,多个用逗号(,)分隔',
    access_token_validity   INTEGER COMMENT '访问令牌有效时间值(单位:秒)',
    refresh_token_validity  INTEGER COMMENT '更新令牌有效时间值(单位:秒)',
    additional_information  VARCHAR(4096) COMMENT '预留字段',
    autoapprove             VARCHAR(256) COMMENT '用户是否自动Approval操作',
    CONSTRAINT pk_oauth_client_details_client_id PRIMARY KEY (client_id)
) COMMENT '客户端信息';

-- 客户端授权令牌表
DROP TABLE IF EXISTS oauth_client_token;
CREATE TABLE oauth_client_token
(
    token_id          VARCHAR(256) COMMENT 'MD5加密的access_token值',
    token             BLOB COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
    authentication_id VARCHAR(256) COMMENT 'MD5加密过的username,client_id,scope',
    user_name         VARCHAR(256) COMMENT '登录的用户名',
    client_id         VARCHAR(256) COMMENT '客户端ID'
) COMMENT '客户端授权令牌表';

-- DML数据准备

INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove)
VALUES ('test_client', NULL, '$2a$10$2szDKjvKHJCWE6YQNznogOeQF3USZHmCYj1fG7YbfK.vnTgNKLzri', 'read', 'client_credentials,authorization_code,mobile,password,refresh_token', 'http://baidu.com', NULL, 7200, 108000, NULL, NULL);

 

给用户表添加数据:

INSERT INTO `user` VALUES ('1', '$2a$10$gExKdT3nkoFKfW1cFlqQUuFji3azHG.W4Pe3/WxHKANg3TpkSJRfW', 'zhangjian', 'ADMIN');

注意:在spring-security 5.x版本必须要注入密码实现器,我们使用了 BCryptPasswordEncoder 加密器,所以需要这里也需要对密码进行加密

添加client信息,使用 oauth_client_details 表:

INSERT INTO `oauth_client_details` VALUES ('app', 'app', '$2a$10$fG7ou8CNxDESVFLIM7LrneDmIpwbrxGM2W6.coGPddfQPyZxiqXE6', 'web', 'implicit,client_credentials,authorization_code,refresh_token,password', 'http://www.baidu.com', 'ROLE_USER', null, null, null, null);

同理也需要对client_secret字段进行加密

application.yml配置文件

spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: auth-service
  cloud:
    nacos:
      discovery:
        server-addr: xx.xx.xx.xx:8848/
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://xx.xx.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: xxxxxx
    driver-class-name: com.mysql.jdbc.Driver

server:
  port: 5000

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml

自定义认证服务器

只需要继承 AuthorizationServerConfigurerAdapter 并在开始处加上@EnableAuthorizationServer 注解即可

 

/**
 * <p>
 * <code>AuthorizationServerConfig</code>
 * </p>
 * Description:
 * 授权/认证服务器配置
 * @author javadaily
 * @date 2020/2/26 16:26
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private UserDetailServiceImpl userDetailService;

    // 认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    /**
     * access_token存储器
     * 这里存储在数据库,大家可以结合自己的业务场景考虑将access_token存入数据库还是redis
     */
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    /**
     * 从数据库读取clientDetails相关配置
     * 有InMemoryClientDetailsService 和 JdbcClientDetailsService 两种方式选择
     */
    @Bean
    public ClientDetailsService clientDetails() {
        return new JdbcClientDetailsService(dataSource);
    }

    /**
     * 注入密码加密实现器
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    /**
     * 认证服务器Endpoints配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //如果需要使用refresh_token模式则需要注入userDetailService
        endpoints.userDetailsService(userDetailService);
        endpoints.authenticationManager(this.authenticationManager);
        endpoints.tokenStore(tokenStore());
    }

    /**
     * 认证服务器相关接口权限管理
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients() //如果使用表单认证则需要加上
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

    /**
     * client存储方式,此处使用jdbc存储
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetails());
    }
}

自定义web安全配置类

/**
 * <p>
 * <code>WebSecurityConfig</code>
 * </p>
 * Description:
 * 自定义web安全配置类
 * @author javadaily
 * @date 2020/2/26 16:35
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    @Bean
    public UserDetailsService userDetailsService(){
        return new UserDetailServiceImpl();
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    /**
     * 认证管理
     * @return 认证管理对象
     * @throws Exception 认证异常信息
     */
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService())
                .passwordEncoder(passwordEncoder());

    }

    /**
     * http安全配置
     * @param http http安全对象
     * @throws Exception http安全异常信息
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and().httpBasic()
                .and().cors()
                .and().csrf().disable();
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(
                "/error",
                "/static/**",
                "/v2/api-docs/**",
                "/swagger-resources/**",
                "/webjars/**",
                "/favicon.ico"
            );
    }
}

自定义用户实现

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        //获取本地用户
        User user = userMapper.selectByUserName(userName);
        if(user != null){
            //返回oauth2的用户
            return new org.springframework.security.core.userdetails.User(
                    user.getUsername(),
                    user.getPassword(),
                    AuthorityUtils.createAuthorityList(user.getRole())) ;
        }else{
            throw  new UsernameNotFoundException("用户["+userName+"]不存在");
        }
    }
}

实现 UserDetailsService 接口并实现 loadUserByUsername 方法,这一部分大家根据自己的技术框架实现,Dao层我就不贴出来了。

对外提供获取当前用户接口

@RestController
@RequestMapping("user")
public class UserController {
    @Autowired
    public UserMapper userMapper;


    @GetMapping("getByName")
    public User getByName(){
        return userMapper.selectByUserName("zhangjian");
    }

    /**
     * 获取授权的用户信息
     * @param principal 当前用户
     * @return 授权信息
     */
    @GetMapping("current/get")
    public Principal user(Principal principal){
        return principal;
    }
}

资源服务器

Oauth2.0的认证服务器也资源服务器,我们在启动类上加入 @EnableResourceServer 注解即可

@SpringBootApplication
//对外开启暴露获取token的API接口
@EnableResourceServer
@EnableDiscoveryClient
public class AuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class, args);
    }
}

后端微服务改造

后端所有微服务都是资源服务器,所以我们需要对其进行改造,下面以account-service为例说明改造过程

  • 添加oauth2.0依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
  • 配置资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
                .antMatchers(
         "/v2/api-docs/**",
                    "/swagger-resources/**",
                    "/swagger-ui.html",
                    "/webjars/**"
                    ).permitAll()
                .anyRequest().authenticated()
                .and()
                //统一自定义异常
                .exceptionHandling()
                .and()
                .csrf().disable();
    }
}
  • 修改配置文件,配置身份获取地址
security:
  oauth2:
    resource:
      user-info-uri: http://localhost:5000/user/current/get
      id: account-service

测试

  • 我们直接访问account-service接口,会提示需要需要认证授权

 

  • 通过密码模式从认证服务器获取access_token

 

  • 在请求头上带上access_token重新访问account-service接口,接口正常响应

 

  • 通过debug模式发现每次访问后端服务时都会去认证资源器获取当前用户

 

  • 输入错误的access_token进行访问,提示access_token失效

 

总结

通过以上几步我们将后端服务加上了认证服务,必须要先进行认证才能正常访问后端服务。整个实现过程还是比较复杂的,建议大家都实践一下,理解其中相关配置的作用,也方便更深入理解Oauth2协议。

 

SpringCloud Gateway 集成Oauth2。

概念部分

在网关集成Oauth2.0后,我们的流程架构如上。主要逻辑如下:
1、客户端应用通过api网关请求认证服务器获取access_token http://localhost:8090/auth-service/oauth/token2、认证服务器返回access_token

{
  "access_token": "f938d0c1-9633-460d-acdd-f0693a6b5f4c",
  "token_type": "bearer",
  "refresh_token": "4baea735-3c0d-4dfd-b826-91c6772a0962",
  "expires_in": 43199,
  "scope": "web"
}

3、客户端携带access_token通过API网关访问后端服务

 

4、API网关收到access_token后通过 AuthenticationWebFilter 对access_token认证

5、API网关转发后端请求,后端服务请求Oauth2认证服务器获取当前用户

在前面文章中我们搭建好了单独的Oauth2认证授权服务,基本功能框架都实现了,这次主要是来实现第四条,SpringCloud 整合 Oauth2 后如何进行access_token过滤校验。

代码示例

引入组件

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>

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

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

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
</dependency>

主要引入跟oauth2相关的jar包,这里还需要引入数据库相关的jar包,因为我们的token是存在数据库中,要想在网关层校验token的有效性必须先从数据库取出token。

bootstrap.yml 配置修改

spring:
  application:
    name: cloud-gateway
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://xx.0.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: xxxxx
    password: xxxxxxx
    driver-class-name: com.mysql.jdbc.Driver

主要配置oauth2的数据库连接地址

自定义认证接口管理类

在webFlux环境下通过实现 ReactiveAuthenticationManager 接口 自定义认证接口管理,由于我们的token是存在jdbc中所以命名上就叫ReactiveJdbcAuthenticationManager

@Slf4j
public class ReactiveJdbcAuthenticationManager implements ReactiveAuthenticationManager {

    private TokenStore tokenStore;

    public JdbcAuthenticationManager(TokenStore tokenStore){
        this.tokenStore = tokenStore;
    }

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        return Mono.justOrEmpty(authentication)
                .filter(a -> a instanceof BearerTokenAuthenticationToken)
                .cast(BearerTokenAuthenticationToken.class)
                .map(BearerTokenAuthenticationToken::getToken)
                .flatMap((accessToken ->{
                    log.info("accessToken is :{}",accessToken);
                    OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
                    //根据access_token从数据库获取不到OAuth2AccessToken
                    if(oAuth2AccessToken == null){
                        return Mono.error(new InvalidTokenException("invalid access token,please check"));
                    }else if(oAuth2AccessToken.isExpired()){
                        return Mono.error(new InvalidTokenException("access token has expired,please reacquire token"));
                    }

                    OAuth2Authentication oAuth2Authentication =this.tokenStore.readAuthentication(accessToken);
                    if(oAuth2Authentication == null){
                        return Mono.error(new InvalidTokenException("Access Token 无效!"));
                    }else {
                        return Mono.just(oAuth2Authentication);
                    }
                })).cast(Authentication.class);
    }
}

网关层的安全配置

@Configuration
public class SecurityConfig {
    private static final String MAX_AGE = "18000L";
    @Autowired
    private DataSource dataSource;
    @Autowired
    private AccessManager accessManager;

    /**
     * 跨域配置
     */
    public WebFilter corsFilter() {
        return (ServerWebExchange ctx, WebFilterChain chain) -> {
            ServerHttpRequest request = ctx.getRequest();
            if (CorsUtils.isCorsRequest(request)) {
                HttpHeaders requestHeaders = request.getHeaders();
                ServerHttpResponse response = ctx.getResponse();
                HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
                HttpHeaders headers = response.getHeaders();
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
                headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
                if (requestMethod != null) {
                    headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
                }
                headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
                headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
                headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
                if (request.getMethod() == HttpMethod.OPTIONS) {
                    response.setStatusCode(HttpStatus.OK);
                    return Mono.empty();
                }
            }
            return chain.filter(ctx);
        };
    }

    @Bean
    SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
        //token管理器
        ReactiveAuthenticationManager tokenAuthenticationManager = new ReactiveJdbcAuthenticationManager(new JdbcTokenStore(dataSource));
        //认证过滤器
        AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);
        authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());

        http
                .httpBasic().disable()
                .csrf().disable()
                .authorizeExchange()
                .pathMatchers(HttpMethod.OPTIONS).permitAll()
                .anyExchange().access(accessManager)
                .and()
                // 跨域过滤器
                .addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
                //oauth2认证过滤器
                .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
        return http.build();
    }
}

这个类是SpringCloug Gateway 与 Oauth2整合的关键,通过构建认证过滤器 AuthenticationWebFilter 完成Oauth2.0的token校验。AuthenticationWebFilter 通过我们自定义的 ReactiveJdbcAuthenticationManager 完成token校验。我们在这里还加入了CORS过滤器,以及权限管理器 AccessManager

权限管理器

@Slf4j
@Component
public class AccessManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    private Set<String> permitAll = new ConcurrentHashSet<>();
    private static final AntPathMatcher antPathMatcher = new AntPathMatcher();


    public AccessManager (){
        permitAll.add("/");
        permitAll.add("/error");
        permitAll.add("/favicon.ico");
        permitAll.add("/**/v2/api-docs/**");
        permitAll.add("/**/swagger-resources/**");
        permitAll.add("/webjars/**");
        permitAll.add("/doc.html");
        permitAll.add("/swagger-ui.html");
        permitAll.add("/**/oauth/**");
        permitAll.add("/**/current/get");
    }

    /**
     * 实现权限验证判断
     */
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono, AuthorizationContext authorizationContext) {
        ServerWebExchange exchange = authorizationContext.getExchange();
        //请求资源
        String requestPath = exchange.getRequest().getURI().getPath();
        // 是否直接放行
        if (permitAll(requestPath)) {
            return Mono.just(new AuthorizationDecision(true));
        }

        return authenticationMono.map(auth -> {
            return new AuthorizationDecision(checkAuthorities(exchange, auth, requestPath));
        }).defaultIfEmpty(new AuthorizationDecision(false));

    }

    /**
     * 校验是否属于静态资源
     * @param requestPath 请求路径
     * @return
     */
    private boolean permitAll(String requestPath) {
        return permitAll.stream()
                .filter(r -> antPathMatcher.match(r, requestPath)).findFirst().isPresent();
    }

    //权限校验
    private boolean checkAuthorities(ServerWebExchange exchange, Authentication auth, String requestPath) {
        if(auth instanceof OAuth2Authentication){
            OAuth2Authentication athentication = (OAuth2Authentication) auth;
            String clientId = athentication.getOAuth2Request().getClientId();
            log.info("clientId is {}",clientId);
        }

        Object principal = auth.getPrincipal();
        log.info("用户信息:{}",principal.toString());
        return true;
    }
}

主要是过滤掉静态资源,将来一些接口权限校验也可以放在这里。

测试

  • 通过网关调用auth-service获取 access_token

 

  • 在Header上添加认证访问后端服务

 

  • 网关过滤器进行token校验

 

  • 权限管理器校验

 

  • 去认证服务器校验当前用户

 

  • 返回正常结果

 

  • 故意写错access_token,返回错误响应

 

  • 请求头上去掉access_token,直接返回401 Unauthorized

 

总结

通过以上几步我们将SpringCloud Gateway整合好了Oauth2.0,这样我们整个项目也基本完成了,后面几期再来对项目进行优化,欢迎持续关注。

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Cloud 2021.0.1是Spring Cloud框架的一个版本,它引入了许多新特性和改进。 OAuth2是一个开放的授权协议,用于保护Web资源。在使用Spring Cloud开发微服务架构时,可以使用OAuth2来实现身份验证和授权。 Spring Cloud 2021.0.1中的OAuth2模块提供了一套简单而强大的工具,用于在微服务架构中实现安全认证和资源授权。它提供了一种方式来管理和保护服务之间的通信,防止未经授权的访问。 使用OAuth2,我们可以定义资源服务器和授权服务器。资源服务器提供受保护的资源,而授权服务器负责验证用户的身份并授予访问令牌。这种方式使得微服务架构中的服务可以相互信任,并确保只有经过授权的服务可以访问受保护的资源。 Spring Cloud 2021.0.1的OAuth2模块还支持多种授权模式,包括密码模式、授权码模式、客户端模式和刷新令牌模式。这些模式适用于不同的场景和需求,可以根据实际情况选择合适的授权方式。 此外,Spring Cloud 2021.0.1还提供了一套可扩展的API,用于自定义身份验证和授权逻辑。我们可以根据自己的需求来实现自定义的授权策略,并轻松集成到Spring Cloud中。 综上所述,Spring Cloud 2021.0.1中的OAuth2模块为微服务架构提供了强大的安全认证和资源授权功能。它使得服务之间的通信更加可靠和安全,为微服务架构的开发和部署提供了便利。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值