Bootstrap

OAuth2基础篇

1. 简介

1.1 什么是OAuth协议?

OAuth协议为用户资源的授权提供了一个安全又简易的标准。与以往的授权方式不同之处是 OAuth的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth是安全的。 OAuth是 Open Authorization的简写

OAuth本身不存在一个标准的实现,后端开发者自己根据实际的需求和标准的规定实现。
其步骤一般如下:

  1. 第三方(客户端)要求用户给予授权
  2. 用户同意授权
  3. 根据上一步获得的授权,第三方向认证服务器请求令牌( token)
  4. 认证服务器对授权进行认证,确认无误后发放令牌
  5. 第方使用令牌向资源服务器请求资源
  6. 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源

1.2 OAuth2.0是为了解决什么问题?

任何身份认证,本质上都是基于对请求方的不信任所产生的。同时,请求方是信任被请求方的,例如用户请求服务时,会信任服务方。所以, 身份认证就是为了解决 身份的可信任问题。

在 OAuth2.0中,简单来说有三方:

  1. 用户(这里是指属于 服务方的用户)、
  2. 服务方(如微信、微博等)、
  3. 第三方应用(也就是所谓的客户端)

服务方不信任 用户,所以需要用户提供密码或其他可信凭据(用户需要用户密码登录)
服务方不信任 第三方应用,所以需要第三方提供自已交给它的凭据(如微信授权的 code,AppID等)
用户部分信任 第三方应用,所以用户愿意把自已在服务方里的某些服务交给第三方使用,但不愿意把自已在服务方的密码等交给第三方应用

1.3 OAuth2.0成员和授权基本流程

OAuth2.0成员:

  1. Resource Owner(资源拥有者:用户)
  2. Client (第三方接入平台:请求者)
  3. Resource Server (服务器资源:数据中心)
  4. Authorization Server (认证服务器)

OAuth2.0基本流程(步骤详解):

  1. Authorization Request, 第三方请求用户授权
  2. Authorization Grant,用户同意授权后,会从服务方获取一次性用户 授权凭据(如 code码)给第三方Authorization Grant
  3. 第三方会把 授权凭据以及服务方给它的的 身份凭据(如 AppId)一起交给服务方的向认证服务器申请 访问令牌Access Token,认证服务器核对授权凭据等信息,确认无误后,向第三方发送 访问令牌 Access Token等信息Access Token
  4. 通过这个 Access Token向 Resource Server索要数据Protected Resource
  5. 资源服务器使用令牌向认证服务器确认令牌的正确性,确认无误后提供资源

这样服务方,一可以确定第三方得到了用户对此次服务的授权(根据用户授权凭据),二可以确定第三方的身份是可以信任的(根据身份凭据),所以,最终的结果就是,第三方顺利地从服务方获取到了此次所请求的服务
从上面的流程中可以看出, OAuth2.0完整地解决了 用户、 服务方、 第方在某次服务时这者之间的信任问题

2. 第一波简单配置

2.1 核心依赖 集成父sprigboot内的版本

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

2.2 授权服务配置

package cn.wang.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 11:51
 * @description: OAuth2授权服务
 * <p>
 * 授权服务配置总结:
 * 授权服务配置分成大块,可以关联记忆。
 * 既然要完成认证,它首先得知道客户端信息从哪儿读取,因此要进行客户端详情配置。
 * 既然要颁发token,那必须得定义token的相关endpoint,以及token如何存取,
 * 以及客户端支持哪些类型的 token。
 * 既然暴露除了一些endpoint,那对这些endpoint可以定义一些安全上的约束等。
 */
@Configuration
@EnableAuthorizationServer  //开启授权服务
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    //授权码模式需要
    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;
    @Autowired
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    //密码模式需要,认证管理器
    @Autowired
    private AuthenticationManager  authenticationManager;

    //认证管理器


    //构造器
    public OAuth2AuthorizationServer() {
        super();
    }


    /**
     * 配置客户端(第方应用)详情,允许满足条件的客户端来访问,客户端详情会在这里进行初始化,
     * 可以写死也可以通过调取数据库来存储详情信息
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //ClientDetailsServiceConfigurer
        // 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService是一个抽象类)
        //ClientDetailsService负责查找ClientDetails,
        // 而ClientDetails几个重要的属性如下列表:
        //clientId:(必须的用来标识客户的Id。
        // secret:(需要值得信任的客户端)客户端安全码,如果有的话。
        //scope:用来限制客户端的访问范围,如果为空(默认的话,那么客户端拥全部的访问范围。
        // authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
        // authorities:此客户端可以使用的权限(基于Spring Security authorities。
        //客户端详情(Client Details能够在应用程序运行的时候进行更新,
        // 可以通过访问底层的存储服务(例如将客户 端详情存储在一个关系数据库的表中,就可以使用
        // JdbcClientDetailsService或者通过自己实现
        // ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口来进行管理。


//我们暂时使用内存方式存储客户端详情信息,配置如下:
        clients.inMemory()
                .withClient("orderId")  //配置客户端Id,假设我是订单客户端我来访问
                //客户端来访问的时候来着密码来,不然随便什么客户端都可以访问
                .secret(new BCryptPasswordEncoder().encode("abcd1234")) 
                .resourceIds("resource1") //需要访问的资源服务Id
                //允许客户端访问的授权类型
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                .scopes("aaa") //允许的授权范围,就是一个标识
                .autoApprove(false) //如果为true直接颁发令牌,为false返回授权页面
                .redirectUris("http://www.baidu.com"); //验证客户端没问题后回调地址
    }


    /**
     * 配置令牌访问端点(url)和令牌服务
     *
     * AuthorizationServerEndpointsConfigurer
     * 这个配置对象一个叫做 pathMapping() 的方法用来配置端点URL链接,
     * 它两个参数:
     * 第一个参数:String 类型的,这个端点URL的默认链接。
     * 第二个参数:String 类型的,你要进行替代的URL链接。
     * 以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping()方法的
     * 第一个参数: /oauth/authorize:授权端点。
     *            /oauth/token:令牌端点。
     *            /oauth/confirm_access:用户确认授权提交端点。
     *            /oauth/error:授权服务错误信息端点。
     *            /oauth/check_token:用于资源服务访问的令牌解析端点。
     *            /oauth/token_key:提供公密匙的端点,如果你使用JWT令牌的话。
     *
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager) //密码模式需要
                .authorizationCodeServices(authorizationCodeServices)//授权码模式需要
                .tokenServices(authorizationServerTokenServices) //令牌服务,都需要
                .allowedTokenEndpointRequestMethods(HttpMethod.POST); //允许post提交来访问令牌
    }

    /**
     * 配置客户端访问索要令牌端点(url)的安全策略(安全约束)
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                //tokenkey这个endpoint当使用JwtToken,
                // 资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。
                .tokenKeyAccess("permitAll()")
                //checkToken这个endpoint完全公开
                .checkTokenAccess("permitAll()")
                // 允许表单认证
                .allowFormAuthenticationForClients();//(3)
    }
}

2.3 配置授权码

package cn.wang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.InMemoryAuthorizationCodeServices;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 12:58
 * @description:
 */
@Configuration
public class OAuth2Code {

    //设置授权码模式的授权码如何 存取,暂时采用内存方式
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
       return new InMemoryAuthorizationCodeServices(); }
    }

2.4 配置令牌token

package cn.wang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 12:31
 * @description: AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,
 * 令牌可以被用来 加载身份信息,里面包含了这个令牌的相关权限。
 * 自己可以创建 AuthorizationServerTokenServices 这个接口的实现,
 * 则需要继承 DefaultTokenServices 这个类里面包含了一些有用实现,
 * 你可以使用它来修改令牌的格式和令牌的存储。
 * 默认的,当它尝试创建一个令牌的时 候,是使用随机值来进行填充的,
 * 除了持久化令牌是委托一个 TokenStore 接口来实现以外,
 * 这个类几乎帮你做了 所的事情。
 * 并且 TokenStore 这个接口一个默认的实现,它就是 InMemoryTokenStore ,
 * 如其命名,所的 令牌是被保存在了内存中。
 * 除了使用这个类以外,你还可以使用一些其他的预定义实现,
 * 下面几个版本,它们都 实现了TokenStore接口:
 * InMemoryTokenStore:这个版本的实现是被默认采用的,
 * 它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份,
 * 大多数的项目都可以使用这个版本的实现来进行 尝试,
 * 你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。
 * JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。
 * 使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,
 * 使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。
 * JwtTokenStore:这个版本的全称是 JSON Web Token(JWT,
 * 它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),
 * 但是它一个缺点,那就是撤销一个已经授 权令牌将会非常困难,
 * 所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token。
 * 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。
 * JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与
 * DefaultTokenServices 所扮演的角色是一样的。
 */
@Configuration
public class OAuth2Token {

    //这里暂时采用默认的放在内存里面
    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

}

2.5 配置令牌服务

package cn.wang.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 12:45
 * @description:
 */
@Configuration
public class OAuth2TokenService {

    @Autowired
    private TokenStore tokenStore;
    @Autowired
    private ClientDetailsService clientDetailsService;

    //授权token令牌服务
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices() {
        //自己可以创建 AuthorizationServerTokenServices 这个接口的实现,
        // 则需要继承 DefaultTokenServices
        // 这个类里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。
        DefaultTokenServices service = new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);
        service.setSupportRefreshToken(true); //是否支持刷新token
        service.setTokenStore(tokenStore);
        service.setAccessTokenValiditySeconds(7200); // 令牌默认效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认效期3天
        return service;
    }
}

2.6 配置WEB安全设置

package cn.wang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
 * @author Administrator
 * @version 1.0
 *
 * web安全设置
 **/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //认证管理器
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
//    //密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;

    }
}

2.6.1 授权码模式

在这里插入图片描述

如果直接访问令牌,缺少授权码会报错
localhost:8888/oauth/token?client_id=orderId&client_secret=abcd1234&grant_type=authorization_code&code=&redirect_uri=http://www.baidu.com

在这里插入图片描述
资源拥者(我)打开客户端,客户端要求资源拥有者(我)给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。(获取授权码)
localhost:8888/oauth/authorize?client_id=orderId&response_type=code&scope=aaa&redirect_uri=http://www.baidu.com
会跳转打认证服务器的登录页面,验证身份之后才会给授权码
在这里插入图片描述

下面模仿数据库,本来是需要到数据库里面去查询的

package cn.wang.login;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;


/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 15:47
 * @description:
 */
@Service
public class OAuth2LoginService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        //将来连接数据库根据账号查询用户信息
        if (s == null) {
            //如果用户查不到,返回null,由provider来抛出异常
            return null;
        }
        //根据用户的id查询用户的权限
        List<String> permissions = new ArrayList<>();
        permissions.add("aaa");
        permissions.add("bbb");
        //将permissions转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        //将userDto转成json
        UserDetails userDetails =
                User.withUsername("zhansan").password("$2a$10$vBAw2AmcaNB/eY96dxc3Yu3AZqq5QyUH/gmH3SPKFKmVwiUSxh8/a").authorities(permissionArray).build();
        return userDetails;
    }

    public static void main(String[] args) {
        BCryptPasswordEncoder bc = new BCryptPasswordEncoder();
        String encode = bc.encode("123");
        System.out.println("encode = " + encode);
    }
}

然后跳转到指定页面,后面的code就是授权码
在这里插入图片描述
在这里插入图片描述
但是令牌使用过第一次就失效了,刷新令牌是当上面的访问令牌失效了,可以使用刷新令牌再来获取

2.6.2 简化模式

在这里插入图片描述
资源拥者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会
附加客户端的身份信息。如:
localhost:8888/oauth/authorize?client_id=orderId&response_type=token&scope=aaa&redirect_uri=http://www.baidu.com
成功之后会跳转,令牌会出现在浏览器请求栏内
在这里插入图片描述
参数描述同授权码模式 ,注意response_type=token,说明是简化模式。

  1. 浏览器出现向授权服务器授权页面,之后将用户同意授权。
  2. 授权服务器将授权码将令牌(access_token以Hash的形式存放在重定向uri的fargment中发送给浏览
    器。

注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#作为 fragment 的开头,
其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要
知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。

一般来说,简化模式用于没有服务器端的第三方单页面应用,也就是只是页面与页面之间,因为没有服务器端就无法接收授权码。

2.6.3 密码模式

在这里插入图片描述

  1. 资源拥者将用户名、密码发送给客户端
  2. 客户端拿着资源拥者的用户名、密码向授权服务器请求令牌(access_token,请求如下:
    /uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123 ;
    参数列表如下:
    client_id:客户端准入标识。
    client_secret:客户端秘钥。
    grant_type:授权类型,填写password表示密码模式
    username:资源拥者用户名。
    password:资源拥者密码。
  3. 授权服务器将令牌(access_token发送给client
    这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

密码模式直接将账号密码作为参数请求令牌,存在泄漏账号密码的风险,只能是开发阶段内网使用
在这里插入图片描述

2.6.4 客户端模式

在这里插入图片描述

  1. 客户端向授权服务器发送自己的身份信息,并请求令牌(access_token
  2. 确认客户端身份无误后,将令牌(access_token发送给client,请求如下:
    localhost:8888/oauth/token?client_id=orderId&client_secret=abcd1234&grant_type=client_credentials

参数列表如下:
client_id:客户端准入标识。
client_secret:客户端秘钥。
grant_type:授权类型,填写client_credentials表示客户端模式

这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。
比如,合作方系统对接,拉取一组用户信息。

如图,没有刷新令牌,一般用于内部系统
在这里插入图片描述

2.7 添加资源服务Order

添加核心依赖

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

资源服务配置

package cn.wang.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 18:18
 * @description: 资源服务器配置
 * @EnableResourceServer 注解到一个 @Configuration 配置类上,
 * 并且必须使用 ResourceServerConfigurer 这个配置对象来进行配置
 * (可以择继承自 ResourceServerConfigurerAdapter 然后覆写其中的方法,
 * 参数就是这个 对象的实例
 */
@Configuration
@EnableResourceServer
public class ResourceService extends ResourceServerConfigurerAdapter {

    public static final String RESOURCE_ID = "resource1";

    @Autowired
    private ResourceServerTokenServices resourceServerTokenServices;


    /**
     * ResourceServerSecurityConfigurer中主要包括:
     * tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
     * tokenStore:TokenStore类的实例,指定令牌如何访问,与tokenServices配置可
     * resourceId:这个资源服务的ID,这个属性是可的,但是推荐设置并在授权服务中进行验证。
     * 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                .resourceId(RESOURCE_ID)  //资源服务Id
                .tokenServices(resourceServerTokenServices)
                .stateless(true);
    }


    /**
     * HttpSecurity配置这个与Spring Security类似:
     * 请求匹配器,用来设置需要进行保护的资源路径,
     * 默认的情况下是保护资源服务的全部路径。
     * 通过http.authorizeRequests()来设置受保护资源的访问规则
     * 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/**")
                //这里的scope和oauth2-uaa里面的配置一样
                .access("#oauth2.hasScope('aaa')")
                .and().csrf().disable()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);//不使用session
    }


}

配置远程验证token

package cn.wang.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

/**
 * @version V1.0.0
 * @author: WangQingLong
 * @date: 2020/11/7 18:35
 * @description: 远程认证token令牌服务
 */
@Configuration
public class ResourceTokenAuth {

    //资源服务令牌解析服务
    /**
     *ResourceServerTokenServices 是组成授权服务的另一半
     * 如果你的授权服务和资源服务在同一个应用程序上的 话,
     * 你可以使用 DefaultTokenServices ,这样的话,
     * 你就不用考虑关于实现所必要的接口的一致性问题。
     * 如果 你的资源服务器是分离开的,那么你就必须要确保能够有匹配授权服务提供的
     * ResourceServerTokenServices,它知道如何对令牌进行解码。
     * 令牌解析方法: 使用 DefaultTokenServices
     * 在资源服务器本地配置令牌存储、解码、解析方式
     * 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,
     * 每次都请求授权服务器端点 /oauth/check_token
     * 使用授权服务的 /oauth/check_token 端点
     * 你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问
     *
     */

    @Bean
    public ResourceServerTokenServices resourceServerTokenServices() {
        //使用远程服务请求授权服务器校验token,
        // 必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service = new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:8888/oauth/check_token");
        service.setClientId("orderId");
        service.setClientSecret("abcd1234");
        return service;
    }
}

测试
1.先测试是否能过令牌拿到资源:localhost:8888/oauth/check_token
在这里插入图片描述
再测试是否能根据配置正常访问资源服务
在这里插入图片描述
显示报错,没有授权不让访问
照oauth2.0协议要求,请求资源需要携带token且放在头部,如下:
token的参数名称为:Authorization,值为:Bearer token值
在这里插入图片描述
在这里插入图片描述
但是存在一个问题,上图中,只有拥有p1权限的才可以访问,但是如果咱们改成p2依然成功,
这是为什么呢,这是因为咱们没有配置SpringSecurity对方法的拦截,

这里需要区分一下,oauth2针对的是大范围的资源认证,springsecurity针对的是方法级别的认证
加上下面的

package cn.wang.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author Administrator
 * @version 1.0  如果需要对方法进行进行权限控制拦截,必须要配置这个
 **/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
//                .antMatchers("/r/r1").hasAuthority("p2")
//                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
        ;


    }
}

再试一试就发现不允许访问了
在这里插入图片描述

3. 基于第一波配置改造令牌

在这里插入图片描述
将上面的注掉,换成JWT令牌服务
在这里插入图片描述
在令牌服务里面对令牌进行JWT替换增强
在这里插入图片描述
修改完上面的UAA认证服务之后,修改资源服务的令牌远程服务
1.先将uaa里面的token配置粘贴到资源服务当中
在这里插入图片描述
2.将原来远程验证令牌服务注掉,这个配置类都可以不要了
在这里插入图片描述
在资源服务里面注掉原来的,导入Tokenserver
在这里插入图片描述
修改配置项,将tokenservices替换成tokenStore
在这里插入图片描述
测试
改造完之后调取服务发现,令牌变成了,这是因为把个人信息都放到jwt载荷当中了
在这里插入图片描述
验证一下,没问题,解析出来的令牌包含原来的数据
在这里插入图片描述
测试通过
在这里插入图片描述

4. 完善配置

之前我们的授权服务里面的客户端详情是写死在代码里面的
在这里插入图片描述
授权码也是放在内存
在这里插入图片描述
现在将两者改造,放到数据库里面去
1.导入依赖:

<!--如果使用com.mysql.cj.jdbc.Driver就需要引入8版本以上的,
不要使用springboot自带的5版本-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.15</version>
</dependency>

<!--OAuth2ClientDetails需要引入jdbc导入数据源-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

2.将客户端详情bean注入到spring容器方便使用

* @description: 由于要将客户端详情配置进数据库,客户端详情也是一个bean,
 * 所以特意新建一个类,注入客户端详情,方便区分
 */
@Configuration
public class OAuth2ClientDetails {

    @Autowired
    private PasswordEncoder passwordEncoder;

    //将客户端信息存储到数据库
    @Bean
    public ClientDetailsService clientDetailsService(DataSource dataSource) {
        ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        ((JdbcClientDetailsService) clientDetailsService).setPasswordEncoder(passwordEncoder);
        return clientDetailsService;
    }
}

在这里插入图片描述
3.修改之前的客户端详情配置,在认证服务器里面,将原来的注掉,注入属性
在这里插入图片描述
点进去看源码发现它已经自己是一个默认的字段的表的,
里面有相应的查询的方法,所以我们需要建立一个这样的表
在这里插入图片描述
改造授权码,将授权码存数据库
在这里插入图片描述
根据源码创建表
在这里插入图片描述
测试OK

5. 集成springcloud

网关整合 OAuth2.0 两种思路:

  1. 认证服务器生成jwt令牌, 所请求统一在网关层验证,判断权限等操作;
  2. 由各资源服务处理,网关只做请求转发。

我们用第一种。我们把API网关作为OAuth2.0的资源服务器角色,实现接入客户端权限拦截、令牌解析并转发当 前登录用户信息(jsonToken)给微服务,这样下游微服务就不需要关心令牌格式解析以及OAuth2.0相关机制了。

API网关在认证授权体系里主要负责两件事:

  1. 作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
  2. 令牌解析并转发当前登录用户信息(明文token给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:

  1. 用户授权拦截(看当前用户是否权访问该资源
  2. 将用户信息存储进当前线程上下文(利于后续业务逻辑随时获取当前用户信息

因为网关本身也是一种资源,我们把其他资源统一放到网关验证令牌并解析成明文转发给其他微服务,大概思路就是:
用户访问某一个服务,在网关认证解析令牌成明文放在请求头转发给服务,
服务从头里面拿到信息,验证相关信息和权限,这里指的是springsecurity对方法上的认证,资源服务大范围的认证在网关通过oauth2就已经完成了

5.1 配置

基于往常的Springcloud配置之外,在网关里面引入OAuth2启动依赖

1.因为要在网关解析令牌token所以配置tokenserver

@Configuration
public class OAuth2Token {

    //这里暂时采用默认的放在内存里面
//    @Bean
//    public TokenStore tokenStore() {
//        return new InMemoryTokenStore();
//    }

    /********************改造令牌变成JWT验证*******************/

    //签名随便取,但是这里设置多少,资源服务那边就多少,建议到时候将这个类拷贝过去
    private String SIGNING_KEY = "OAuth2-8888";

    //修改令牌存储方式
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    //令牌签名设置
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        对称秘钥,资源服务器使用该秘钥来验证
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

2.网关本身也是一种资源,加上我们准备采用统一在网关验证令牌的方式,所以所有资源服务都在网关层面配置且解析明文

/**
 * 因为网关本身也是一种资源服务,也需要web安全验证和授权,同时也为了方便把
 * 其他资源如订单order资源集中到这里,所以采用这种方式
 **/
@Configuration
public class ZuulResouceServerConfig {

    public static final String RESOURCE_ID = "resource1";


    //uaa资源服务配置
    @Configuration
    @EnableResourceServer
    //这几个类是由Spring创建的独立的配置对象,
    // 它们 会被Spring传入AuthorizationServerConfigurer中进行配置。
    public class UAAServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                 .antMatchers("/uaa/**").permitAll();
        }
    }


    //order资源
    //uaa资源服务配置
    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;

        @Override
        public void configure(ResourceServerSecurityConfigurer resources){
            resources.tokenStore(tokenStore).resourceId(RESOURCE_ID)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('aaa')");
        }
    }


    //配置其它的资源服务..


}

3.通过zuul拦截器,将jwt解析成明文转发给资源微服务

public class AuthFilter extends ZuulFilter {

    /**
     * 是否需要拦截
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     *拦截类型,在哪个位置拦截,pre在执行之前拦截
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 拦截顺序,数字越小越先执行
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public Object run() throws ZuulException {
        //获取当前请求的上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //从安全上下文中拿鉴权对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if(!(authentication instanceof OAuth2Authentication)){
            return null;
        }
        //强转为OAuth2对象
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        //取出用户身份信息
        String principal = userAuthentication.getName();

        //取出用户权限
        List<String> authorities = new ArrayList<>();

        //从userAuthentication取出权限,放在authorities
        userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));

        //拿到Oauth2请求数据
        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        //拿到请求列表内的参数
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if(userAuthentication!=null){
            jsonToken.put("principal",principal);
            jsonToken.put("authorities",authorities);
        }

        //把身份信息和权限信息放在json中,加入http的header中,转发给微服务
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));

        return null;
    }
}

4.将zuul拦截器注入spring容器使其生效

@Configuration
public class ZuulConfig {

    @Bean
    public AuthFilter preFileter() {
        return new AuthFilter();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

这样,明文就放在头部转发给具体的资源服务了,接下来在资源服务里面的拦截器从头部取出来拿到我们要的资源

5.在具体的资源服务中

@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            //解析出头中的token
        String token = httpServletRequest.getHeader("json-token");
        if(token!=null){
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            //将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);
            //用户身份信息
            UserDTO userDTO = new UserDTO();
            String clinetId = jsonObject.getString("client_id");
            String principal = jsonObject.getString("principal");
            userDTO.setClientId(clinetId);
            userDTO.setUserName(principal);
            //用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
            userDTO.setAuthorities(authorities);

            //将用户信息和权限填充 到用户身份token对象中
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //将authenticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);


        }
        //让过滤器链继续执行
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }
}

为了方便我们使用了一个实体类存放

@Data
public class UserDTO {

    /**
     * 用户id
     */
    private String clientId;
    /**
     * 用户名
     */
    private String userName;

    /**
     * 手机号
     */
    private String[] authorities;

在controller里面我们就可以拿到个人信息资源数据了

@RestController
public class ResourceTestController {

    @GetMapping(value = "/r/r1")
    @PreAuthorize("hasAnyAuthority('p1')")
    public String r1() {
        UserDTO user = (UserDTO) SecurityContextHolder
                .getContext()
                .getAuthentication()
                .getPrincipal();
        return String.format("用户名为【%s】,用户Id为【%s】,用户权限为【%s】", user.getUserName(),user.getClientId(),
                user.getAuthorities().toString());
    }
}

5.2 优化

我们发现,用户身份信息只有张三,但是很多时候我们不止这一个参数,可能除了用户名之外还有手机号啥的,而oauth2里面的用户只有这些属性,
所以我们会采用将username作为一个string类型标识,我们实际将一个对象josn化,然后放进去,到时候解析出来即可
在这里插入图片描述
在这里插入图片描述
只需要改几个地方,第一个地方是uaa用户信息那
在这里插入图片描述
第二个地方是资源服务的拦截器里面解析该属性时即可
在这里插入图片描述

;