3、OAuth2

OAuth2

OAuth 2.0 是目前最流行的授权标准(协议),用来授权第三方应用,获取用户数据。

OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。

CAS的单点登录和OAuth2的区别

SSO :单点登录(Single sign-on)是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

CAS :中央认证服务(Central Authentication Service),一个基于Kerberos票据方式实现SSO单点登录的框架,为Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。

微服务的认证和授权思路

通常微服务的认证和授权思路有两种:

0786305bdb01bbbdcd93cad9c0130129.png

第二种结合了OAuth2体系,网关不仅仅承担流量转发功能,认证授权也是在网关层处理的,令牌会中继给下游服务。这种模式下需要搭建一个UAA(User Account And Authentication)服务。它非常灵活,它可以管理用户,也可以让受信任的客户端自己管理用户,它只负责对客户端进行认证(区别于用户认证)和对客户端进行授权。目前使用OAuth2对微服务进行安全体系建设的都使用这种方式。

44292b537f93ec5c3db3c873b181f731.png

oauth2分布式认证架构

image-20221003133911729

前后端分离架构设计

在这里插入图片描述

基本概念

oauth2包含以下角色:

客户端:本身不存储资源,需要通过资源拥有者授权去请求资源服务器的资源。

资源拥有者:通常为用户

服务类型:

授权(认证)服务器:用于服务提供商对资源拥有者的身份进行认证、对访问资源进行授权,认证成功颁发令牌(access_token),作为客户端访问资源服务器的凭据,配置认证服务器必须实现的endpoints:

资源服务器:存储资源的服务器,该资源服务需要实现OAuth2的过滤器(如果使用了GateWay作为服务统一出口,那么这个过滤器再gateway上声明就可以):

使用

1、新建UAA认证微服务,引入依赖

<!--    springboot    -->
<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>top.ygang</groupId>
    <artifactId>grady-young-common</artifactId>
    <version>1.0</version>
</dependency>

<!--    服务配置中心    -->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!-- spring-security依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-security</artifactId>
</dependency>

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

<!-- jwt依赖 -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

<!-- mybatis-plus依赖 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

<!-- 连接池,用于创建多个redis-template实例 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

<!-- mysql依赖 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- druid数据库连接池 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
</dependency>

2、创建授权服务配置

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    /**
     * 用来配置支持的客户端详情,在这里初始化客户端详情配置
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    }

    /**
     * 用来配置令牌访问端点和令牌服务,用来生成、颁发令牌
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    }

    /**
     * 对于令牌端点的安全约束
     * @param security
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    }
}

客户端详情配置(ClientDetailsServiceConfigurer),可以使用内存存储也可以使用JDBC数据库存储,ClientDetails重要的属性:

clientId:标识客户端ID

secret:客户端安全码(受信任的客户端才有)

scope:限制客户端访问范围,如果为空(默认),客户端可以访问所有资源

authorizedGrantTypes:客户端可以使用的授权类型

authorities:客户端的权限(基于spring security authorities)

/**
 * 用来配置支持的客户端详情,在这里初始化客户端详情配置
 * @param clients
 * @throws Exception
 */
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    // 使用内存方式配置
    clients.inMemory()
        .withClient("test") //客户端id
        .secret("abcdef-abcdef")//客户端密钥
        .resourceIds("r1") //客户端可访问的资源列表
        .authorizedGrantTypes( //客户端可用来申请令牌的方式
        "authorization_code",
        "password",
        "client_credentials",
        "implicit",
        "refresh_token")
        .scopes("all") //允许访问的范围
        .autoApprove(false) //跳转到授权页面
        .redirectUris("https://www.baidu.com"); //验证回调地址
}

管理令牌服务(AuthorizationServerTokenServices),接口定义了一些操作可以对令牌进行管理

定义TokenConfig

@Configuration
public class TokenConfig {

    /**
     * 配置令牌存储策略
     * @return
     */
    @Bean
    public TokenStore tokenStore(){
        // 内存方式,生成普通令牌
        return new InMemoryTokenStore();
    }
}

在上面写的授权服务配置AuthorizationServer中,配置令牌服务

@Autowired
private ClientDetailsService clientDetailsService;

@Autowired
private TokenStore tokenStore;

/**
 * 配置令牌服务
 * @return
 */
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices(){
    DefaultTokenServices services = new DefaultTokenServices();
    services.setClientDetailsService(clientDetailsService); // 配置客户端信息服务
    services.setSupportRefreshToken(true); //是否产生刷新令牌
    services.setTokenStore(tokenStore); //配置令牌存储策略
    services.setAccessTokenValiditySeconds(7200); //令牌有效期
    services.setRefreshTokenValiditySeconds(259200); //刷新令牌有效期
    return services;
}

配置令牌访问端点(AuthorizationServerEndpointsConfigurer),这个配置对象有方法pathMapping()可以替换默认端点,第一个参数是要替换的默认端点,第二个参数是自定义端点。默认端点有:

/oauth/authorize:授权端点

/oauth/token:令牌端点

/oauth/confirm_access:用户确认授权提交端点

/oauth/error:授权服务错误信息端点

/oauth/check_token:用于资源服务进行令牌解析、校验的端点

/oauth/token_key:提供公有密钥的端点

/**
 * 暂时使用采用内存方式作为授权码存储
 * @return
 */
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
    return new InMemoryAuthorizationCodeServices();
}

@Autowired
private AuthenticationManager authenticationManager;

@Autowired
private AuthorizationCodeServices authorizationCodeServices;

/**
 * 用来配置令牌访问端点和令牌服务,用来生成、颁发令牌
 * @param endpoints
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
        .authenticationManager(authenticationManager) // 用于密码模式或其他自定义的认证模式
        .authorizationCodeServices(authorizationCodeServices) // 授权码模式
        .tokenServices(authorizationServerTokenServices()) // 令牌管理服务
        .allowedTokenEndpointRequestMethods(HttpMethod.POST); // 允许post方式

}

令牌端点安全约束(AuthorizationServerSecurityConfigurer)

/**
 * 对于令牌端点的安全约束
 * @param security
 * @throws Exception
 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security
        .checkTokenAccess("permitAll()") //允许任何校验令牌的请求
        .tokenKeyAccess("permitAll()") //允许提供公钥
        .allowFormAuthenticationForClients(); //允许表单认证
}

3、配置SecurityConfig、userDetailsService,具体配置参考前面单体服务

3、简单测试各个模式

授权码模式(authorization_code)

授权码由第三方(认证服务器)颁发,可以使用授权码获取令牌,具体流程:

1、资源拥有者打开客户端,客户端要求资源拥有者授权,他将浏览器重定向到授权服务器,附带客户端信息

GET /oauth/authorize?client_id=test&response_type=code&scope=all&redirect_url=https://www.baidu.com

client_id:客户端id

response_type:授权模式,固定为code

scope:客户端权限

redirect_url:跳转url,申请授权码成功会跳转到此地址,后面会加上授权码code

2、认证账号密码

image-20221004151110397

3、登录成功,跳转授权页面,选择第一个Approve,进行授权

image-20221004151245222

4、跳转到之前的redirect_url,并且附带了code

image-20221004151423461

5、使用授权码申请令牌,一个授权码只能使用一次

POST /oauth/token
application/x-www-form-urlencoded
client_id 客户端id
client_secret 客户端密钥
grant_type 申请模式(授权码模式固定为authorization_code)
code 授权码
redirect_url 跳转地址,和申请授权码时一致

image-20221004152253137

简化模式(token)

和授权码模式第一步比较相似,只不过grant_type改为token,请求验证成功后,会直接将token以hash模式附带在redirect_url后面,请求地址:

GET /oauth/authorize?client_id=test&response_type=token&scope=all&redirect_url=https://www.baidu.com

得到结果:

image-20221004152814724

密码模式(password)

密码模式可能会泄漏账号密码给客户端,所以一般用在自己写的客户端

POST /oauth/token
application/x-www-form-urlencoded
client_id 客户端id
client_secret 客户端密钥
grant_type 申请模式(密码模式固定为password)
username 用户名
password 密码
redirect_url 跳转地址,和申请授权码时一致

image-20221004153405968

客户端模式(client_credentials)

客户端模式比密码模式还要简单,只需要客户端id、客户端密钥就可以申请token

POST /oauth/token
application/x-www-form-urlencoded
client_id 客户端id
client_secret 客户端密钥
grant_type 申请模式(客户端模式固定为client_credentials)
redirect_url 跳转地址,和申请授权码时一致

image-20221004153611519

集成jwt

1、在TokenConfig中配置JWT相关,使用对称加密,配置盐值

@Configuration
public class TokenConfig {
    
    /**
     * jwt令牌加密盐值
     */
    public static final String SIGNING_KEY = "sojff#OJSDF-9IOFH*124";

    /**
     * jwt令牌存储策略
     * @return
     */
    @Bean
    public JwtTokenStore jwtTokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * jwt令牌生成策略
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

2、在AuthorizationServer中配置令牌增强,使用jwt令牌,使用新的JwtTokenStore,替换之前的TokenStore

/**
 * 配置令牌服务
 * @return
 */
@Bean
public AuthorizationServerTokenServices authorizationServerTokenServices(){
    DefaultTokenServices services = new DefaultTokenServices();
    services.setClientDetailsService(clientDetailsService); // 配置客户端信息服务
    services.setSupportRefreshToken(true); //是否产生刷新令牌
    services.setTokenStore(jwtTokenStore); //配置令牌存储策略
    // 配置令牌增强,使用jwt
    TokenEnhancerChain chain = new TokenEnhancerChain();
    chain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
    services.setTokenEnhancer(chain);

    services.setAccessTokenValiditySeconds(7200); //令牌有效期
    services.setRefreshTokenValiditySeconds(259200); //刷新令牌有效期
    return services;
}

3、重新请求令牌,发现令牌已经成功使用jwt进行生成

image-20221004160420506

4、校验令牌,发现用户信息已经存储

image-20221004160436613

oauth2数据持久化

OAuth2.0的服务端和资源端都不是我们自己写的,都是springsecurity框架给我们写的,既然是springsecurity框架的,那么客户端的信息保存在数据库里面的时候,这个数据库的表结构就需要使用springsecurity框架定义的。

DROP TABLE IF EXISTS `oauth_client_details`;

CREATE TABLE `oauth_client_details` (
  `client_id` varchar(255) NOT NULL COMMENT '客户端id',
  `resource_ids` varchar(255) DEFAULT NULL COMMENT '客户端所能访问的资源id集合',
  `client_secret` varchar(255) DEFAULT NULL COMMENT '用于指定客户端(client)的访问密匙',
  `scope` varchar(255) DEFAULT NULL COMMENT '指定客户端申请的权限范围',
  `authorized_grant_types` varchar(255) DEFAULT NULL COMMENT '指定客户端支持的grant_type',
  `web_server_redirect_uri` varchar(255) DEFAULT NULL COMMENT '客户端的重定向URI,',
  `authorities` varchar(255) DEFAULT NULL COMMENT '指定客户端所拥有的Spring Security的权限值',
  `access_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的access_token的有效时间值(单位:秒),可选,默认的有效时间值12小时',
  `refresh_token_validity` int(11) DEFAULT NULL COMMENT '设定客户端的refresh_token的有效时间值',
  `additional_information` varchar(255) DEFAULT NULL COMMENT '预留的字段',
  `autoapprove` varchar(255) DEFAULT NULL COMMENT '设置用户是否自动Approval操作'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `oauth_access_token`;

CREATE TABLE `oauth_access_token` (
  `token_id` varchar(255) DEFAULT NULL,
  `token` longblob,
  `authentication_id` varchar(255) DEFAULT NULL,
  `user_name` varchar(255) DEFAULT NULL,
  `client_id` varchar(255) DEFAULT NULL,
  `authentication` longblob,
  `refresh_token` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `oauth_approvals`;

CREATE TABLE `oauth_approvals` (
  `userId` varchar(255) DEFAULT NULL,
  `clientId` varchar(255) DEFAULT NULL,
  `scope` varchar(255) DEFAULT NULL,
  `status` varchar(10) DEFAULT NULL,
  `expiresAt` datetime DEFAULT NULL,
  `lastModifiedAt` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `oauth_client_token`;

CREATE TABLE `oauth_client_token` (
  `token_id` varchar(255) DEFAULT NULL,
  `token` longblob,
  `authentication_id` varchar(255) DEFAULT NULL,
  `user_name` varchar(255) DEFAULT NULL,
  `client_id` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `oauth_code`;

CREATE TABLE `oauth_code` (
  `code` varchar(255) DEFAULT NULL,
  `authentication` varbinary(2550) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `oauth_refresh_token`;

CREATE TABLE `oauth_refresh_token` (
  `token_id` varchar(255) DEFAULT NULL,
  `token` longblob,
  `authentication` longblob
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

对于之前存在内存中的信息改为存储到数据库

@Autowired
private DataSource dataSource;

/**
 * 配置使用数据库作为授权码存储方式
 * @return
 */
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
    return new JdbcAuthorizationCodeServices(dataSource);
}

/**
 * 配置从数据库获得客户端详情
 * @return
 */
@Bean
public ClientDetailsService clientDetailsService(){
    JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
    jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
    return jdbcClientDetailsService;
}