A094_SpringSecurity_微服务授权_7.1版

目录

一.微服务授权方案

注意:本文章内容全部基于SpringSecurity.docx为基础。

1.概述
1.1.微服务授权的问题

微服务架构下,一个应用会被拆分成若干个微应用,每个微应用都需要对访问进行鉴权,每个微应用都需要明确当前访问用户以及其权限。尤其当访问来源不只是浏览器,还包括其他服务的调用时,单体应用架构下的鉴权方式就不是特别合适了。在为服务架构下,要考虑外部应用接入的场景、用户与服务的鉴权、服务与服务的鉴权等多种鉴权场景。David Borsos 在伦敦的微服务大会上提出了四种方案:

1.2.微服务授权方案

推荐文章 《常见的微服务授权方案》

  • 单点登录(CAS)
    这种方案意味着每个面向用户的服务都必须与认证服务交互,这会产生大量非常琐碎的网络流量和重复的工作,当动辄数十个微应用时,这种方案的弊端会更加明显
  • 分布式Session(会话)
    分布式会话方案原理主要是将关于用户认证的信息存储在共享存储中(Redis),且通常由用户会话作为 key 来实现的简单分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。在某些场景下,这种方案很不错,用户登录状态是不透明的。同时也是一个高可用且可扩展的解决方案。这种方案的缺点在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的复杂性了。
  • 客户端Token的认证方案
    令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每个请求上,为微服务提供用户身份验证,这种解决方案的安全性相对较好,但身份验证注销是一个大问题,缓解这种情况的方法可以使用短期令牌和频繁检查认证服务等。
  • 客户端 Token 与 API 网关结合
    这个方案意味着所有请求都通过网关,从而有效地隐藏了微服务。 在请求时,网关将原始用户令牌转换为内部会话 ID 令牌。在这种情况下,注销就不是问题,因为网关可以在注销时撤销用户的令牌。
2.微服务认证授权方案

微服务授权有两种方案一者是结合网关进行统一认证授权,二者是不使用网关统一认证授权(即认证和授权工作都交给资源服务)。

2.1.Token+zuul+SpringSecurity+Oauth2+JWT

结合网关授权方案:基于Token+网关+SpringSecurity+OAuth2+JWT认证流程图:
在这里插入图片描述
解释:
客户端 : web端,移动端,三方程序
认证服务:负责认证逻辑(登录)和颁发令牌(token)等
网关:负责token统一校验和统一授权
资源服务:负责授权(用户对资源的访问权限检查)和返回资源

2.2.Token+SpringSecurity+Oauth2+JWT

不使用网关授权:基于token+SpringSecurity+OAuth2+JWT认证流程图:
在这里插入图片描述
不使用网关其实就是把token的校验交给了资源服务器自己来处理,网关不做任何事情。

二.OAUTH2

1.Oauth2概述
1.1.什么是Oauth2

阮一峰 博客: http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的,oAuth是Open Authorization的简写,目前的版本是2.0版。

2.Oauth2的认证流程
1.1.认证流程图分析

Oauth2授权码模式
在这里插入图片描述

1.2.Oauth2相关概念
  • Third-party application
    第三方应用程序,本文中又称"客户端"(client),即栗子中的"云打印"。
  • HTTP service
    HTTP服务提供商,本文中简称"服务提供商",即上一节例子中的QQ。
  • Resource Owner
    资源所有者,本文中又称"用户"(user)。
  • User Agent
    用户代理,如浏览器,移动端等。
  • Authorization server
    认证服务器,即服务提供商专门用来处理认证的服务器QQ。
  • Resource server
    资源服务器,即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器QQ
3.Oauth2授权模式

http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
或见“理解OAuth 2.0 - 阮一峰的网络日志.pdf”
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。

3.1.授权码模式(authorization code)

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的认授权流程如下:
在这里插入图片描述
授权流程:

(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

下面是上面这些步骤所需要的参数。

A步骤中,客户端申请认证的URI,包含以下参数:
response_type:表示授权类型,必选项,此处的值固定为"code"
client_id:表示客户端的ID,必选项
redirect_uri:表示重定向URI,可选项
scope:表示申请的权限范围,可选项
state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
获取授权码如:
/authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1Host: server.example.com
D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:
grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
code:表示上一步获得的授权码,必选项。
redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
client_id:表示客户端ID,必选项。
获取令牌如:
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
3.2.简化模式(implicit)

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

3.3.密码模式(resource owner password credentials)

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
在这里插入图片描述

3.4.客户端模式(client credentials)

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

三.Spring Cloud OAUTH2

1.概述
1.1.SpringSecurity-Oauth2介绍

SpingSecurityOauth2实现了Oatuh2,SpingSecurityOauth2分为两个大块,分别为认证授权服务(Authorization Server)和资源服务(Resource server),认证授权服务一般负责执行认证逻辑(登录)和加载用户的权限(给用户授权),以及认证成功后的令牌颁发 ,而资源服务器一般指的是我们系统中的微服务(被访问的微服务),在资源服务器需要对用户的令牌(认证成功与否),以及授权(是不是有访问权限)做检查 。

1.2.Oauth2认证解决方案

这里我们需要准备四个服务,1.授权服务 2.资源服务,3.网关,4注册中心,一共4个服务。授权服务和资源服务的流程图:
在这里插入图片描述
在这里插入图片描述
我们这里以Oauth2授权码模式为例

2.环境准备

注意:没有SpringCloud基础的同学先去学SpringCloud
搭建基本项目结构如下

security-parent  //父工程
	security-auth-server      //统一认证微服务
	security-eureka-server    //注册中心
	security-resource-server  //资源微服务
	security-zuul-server      //网关
2.1.搭建父工程

管理SpringBoot和SpringCloud相关依赖,管理公共依赖,以及依赖版本统一管理
Pom.xml

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.5.RELEASE</version>
</parent>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Finchley.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
2.2 搭建EurekaServer注册中心

微服务注册中心服务

  • Pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>
  • 启动类
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class);
    }
}
  • 配置文件
server:
  port: 1000
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false #禁用注册中心向自己注册
    fetchRegistry: false  #不让注册中心获取服务的注册列表
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/
      #注册中心的注册地址 ,其他微服务需要向这个地址注册
2.3.搭建ZuulServer网关微服务

微服务统一访问入口,后面实现统一鉴权操作。

  • Pom.xml
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
  • 启动类
@SpringBootApplication
@EnableZuulProxy
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class);
    }
}
  • 配置文件
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/ #注册中心地址
  instance:
    prefer-ip-address: true #使用ip地址注册
    instance-id: zuul-server:2000  #指定服务的id
server:
  port: 2000
zuul:
  ignored-services: "*"   #禁止使用服务名字进行访问
  prefix: "/servers"  #统一的前缀
  routes: #配置路由,指定服务的访问路径
    resource1-server: "/resources/**"
spring:
  application:
    name: zuul-server
2.4.搭建AuthServer认证授权微服务

认证微服务,实现认证逻辑,用户授权,令牌颁发等

1.导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2.配置类

@SpringBootApplication
public class AuthServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class) ;
    }
}

3.yml配置

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/ #注册中心地址
  instance:
    prefer-ip-address: true #使用ip地址注册
    instance-id: auth-server:3000  #指定服务的id
server:
  port: 3000
spring:
  application:
    name: auth-server
2.5.搭建ResourceServer资源微服务

资源服务(用户,订单,支付等都是资源服务),需要对用户的请求进行鉴权成功后才能访问。

1.导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

2.配置类

@SpringBootApplication
public class Resource1ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(Resource1ServerApplication.class) ;
    }
}

3.yml配置

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/ #注册中心地址
  instance:
    prefer-ip-address: true #使用ip地址注册
    instance-id: resource1-server:4000  #指定服务的id
server:
  port: 4000
spring:
  application:
    name: resource1-server

启动测试http://localhost:1000/
在这里插入图片描述

3.认证服务AuthServer搭建

在搭建好的 security-auth-server工程 , 集成MyBatis,security和oauth2

3.1.集成MyBatis
  • 完成User,Role,Permission的domain,mapper映射器,Mapper.xml等相关组件的创建
  • Pom.xml : 需要集成Oauth2,Security,Mybatis , 增加如下依赖:
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
        <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.20</version>
    </dependency>
    <!-- mysql 数据库驱动. -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.1.1</version>
    </dependency>
</dependencies>
  • 主启动类
@SpringBootApplication
@MapperScan("cn.itsource.mapper")
public class AuthServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(AuthServerApplication.class) ;
    }
}
  • 配置文件:集成EurekaClient,Mybatis
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/ #注册中心地址
  instance:
    prefer-ip-address: true #使用ip地址注册
    instance-id: auth-server:3000  #指定服务的id
server:
  port: 3000
mybatis:
  mapper-locations: classpath:cn/itsource/mapper/*Mapper.xml
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    username: root
    password: admin
    url: jdbc:mysql:///auth-rbac
    driver-class-name: com.mysql.jdbc.Driver
  application:
    name: auth-server
3.2.定义UserDetailsService

复写loadUserByUsername,根据用户名从数据库中获取认证的用户对象,并且加载用户的权限信息,把用户的认证信息和权限信息封装成UserDetaials返回。
在前面的章节已经学习过UserDetailsService的配置这里不在赘述配置如下:

package cn.itsource.userdetails;
import cn.itsource.domain.Permission;
import cn.itsource.domain.User;
import cn.itsource.mapper.PermissionMapper;
import cn.itsource.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Component
public class UserDetailServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private PermissionMapper permissionMapper;

    /**
     * 加载数据库中的认证的用户的信息:用户名,密码,用户的权限列表
     * @param username: 该方法把username传入进来,
        我们通过username查询用户的信息(密码,权限列表等)
        然后封装成 UserDetails进行返回 ,交给security 。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //User是security内部的对象,UserDetails的实现类 ,
        //用来封装用户的基本信息(用户名,密码,权限列表)
        //public User(String username, String password, Collection<? extends GrantedAuthority> authorities)

userFromDB = userMapper.selectByUsername(username);
        if(null == userFromDB){
            throw new RuntimeException("无效的用户");
        }

        //模拟存储在数据库的用户的密码:123
        //String password = passwordEncoder.encode("123");
        String password = userFromDB.getPassword();

        //查询用户的权限
        List<Permission> permission = permissionMapper.selectPermissionsByUserId(userFromDB.getId());

        //用户的权限列表,暂时为空
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        permission.forEach(e->{
            System.out.println("用户:"+userFromDB.getUsername()+" 加载权限:"+e.getExpression());
            authorities.add(new SimpleGrantedAuthority(e.getExpression()));
        });

        org.springframework.security.core.userdetails.User  user = new org.springframework.security.core.userdetails.User (username,password, authorities);

        return user;
    }
}

认证服务在处理认证逻辑的时候会通过该userDetailService加载用户在数据库中的认证信息实现身份认证,同时在整个方法中也加载了用户的权限列表,后续当用户发起对资源服务的访问时,是否能够授权成功就跟用户的权限列表息息相关了。

3.3.WebSecurity-web安全配置

在前面的章节已经学习过WebSecurity的配置这里不在赘述,配置如下

//Security配置
@Configuration
@EnableWebSecurity(debug = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    //密码 编码器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    //授权规则配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable()   //屏蔽跨域防护
                .authorizeRequests()          //对请求做授权处理
                .antMatchers("/login").permitAll()  //登录路径放行
               // .antMatchers("/login.html").permitAll()//对登录页面跳转路径放行
                .anyRequest().authenticated() //其他路径都要拦截
                .and().formLogin()  //允许表单登录, 设置登陆页
                .successForwardUrl("/loginSuccess") // 设置登陆成功页(对应//controller),一定要有loginSuccess这个路径
                .and().logout().permitAll();    //登出
    }

    //配置认证管理器,授权模式为“poassword”时会用到
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

}

注意:loginSuccess需要对应一个controller

@RestController
public class LoginController {
    @RequestMapping("/loginSuccess")
    public String loginSuccess(){
        return "登錄成功";
    }
}

测试:http://localhost:3000/login

3.4.AuthorizationServer-授权服务配置
1.授权服务配置类-AuthorizationServerConfigurerAdapter

SpringSecurityOauth2提供了AuthorizationServerConfigurerAdapter适配器类来作为认证授权服务的配置,其中有三个方法源码如下:

public class AuthorizationServerConfigurerAdapter  {
    //客户端详情:配置客户端请求的参数
	public void configure(ClientDetailsServiceConfigurer clients)...	
	//授权服务断点:配置授权码和令牌的管理/存储方式
	public void configure(AuthorizationServerEndpointsConfigurer 	endpoints)...
    //授权服务安全配置:配置哪些路径放行(检查token的路径要放行)
	public void configure(AuthorizationServerSecurityConfigurer security) ...
}

作用分别如下:

  • ClientDetailsServiceConfigurer :用来配置客户端详情服务:如配置客户端id(client_id)资源id、客户端密钥(secrect)、授权方式、scope等,可以基于内存或jdbc。(可以理解为是对浏览器向授权服务器获取授权码或令牌时需要提交的参数配置)
  • AuthorizationServerEndpointsConfigurer:配置令牌的访问端点url和令牌服务,如配置如何管理授权码(内存或jdbc),如何管理令牌(存储方式,有效时间等等)
  • AuthorizationServerSecurityConfigurer: 用来配置令牌端点的安全约束,如配置对获取授权码,检查token等某些路径进行放行

授权服务配置分析
在这里插入图片描述
在这里插入图片描述
定义配置类

//授权服务配置
@Configuration
//开启授权服务配置
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
}
2.授权服务配置-客户端详情
//一.客户端详情配置=================================================================================
@Autowired
private DataSource dataSource;

@Autowired
private PasswordEncoder passwordEncoder;

//1.定义客户端详情服务,基于数据库,自动加载oauth_client_details表中的数据
@Bean
public ClientDetailsService  clientDetailsService(){
    JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
    //数据库的secret秘钥是密文
    jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
    return jdbcClientDetailsService;
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.withClientDetails(clientDetailsService());
}

ClientDetailsServiceConfigurer 是这对于客户端详情的配置,我们通过withClientDetails关联一个JdbcClientDetailsService,默认会去找数据库中的名字为 oauth_client_details 表中的数据作为客户端详情的配置,见 JdbcClientDetailsService类的源代码,所以我们需要在数据库执行以下sql创建表:并填充好数据
在这里插入图片描述
因为在jdbcClientDetailsService设置了setPasswordEncoder(passwordEncoder);,所以数据库中的client_secret需要是密文加密的,加密如:BCrypt.hashpw(“secret”, BCrypt.gensalt())
oauth_client_details解释:

字段名字段约束详细描述范例
client_id主键,必须唯一,不能为空用于唯一标识每一个客户端(client);注册时必须填写(也可以服务端自动生成),这个字段是必须的,实际应用也有叫app_keyOaH1heR2E4eGnBr87Br8FHaUFrA2Q0kE8HqZgpdg8Sw
resource_ids不能为空,用逗号分隔客户端能访问的资源id集合,注册客户端时,根据实际需要可选择资源id,也可以根据不同的额注册流程,赋予对应的额资源idorder-resource,pay-resource
client_secret必须填写注册填写或者服务端自动生成,实际应用也有叫app_secret, 必须要有前缀代表加密方式{bcrypt}gY/Hauph1tqvVWiH4atxteSH8sRX03IDXRIQi03DVTFGzKfz8ZtGi
scope不能为空,用逗号分隔指定client的权限范围,比如读写权限,比如移动端还是web端权限read,write / web,mobile
authorized_grant_types不能为空可选值 授权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔implicit",“client_credentials”,“password”, “authorization_code”, “refresh_token”
web_server_redirect_uri可为空客户端重定向uri,authorization_code和implicit需要该值进行校验,注册时填写,httt://baidu.com
authorities可为空指定用户的权限范围,如果授权的过程需要用户登陆,该字段不生效,implicit和client_credentials需要ROLE_ADMIN,ROLE_USER
access_token_validity可空设置access_token的有效时间(秒),默认(606012,12小时)3600
refresh_token_validity可空设置refresh_token有效期(秒),默认(606024*30, 30填)7200
additional_information可空附加数据,值必须是json格式{“key”, “value”}
autoapprovefalse/true/read/write默认false,适用于authorization_code模式,设置用户是否自动approval操作,设置true跳过用户确认授权操作页面,直接跳到redirect_urifalse

客户端请求授权码url如:

http://localhost:3000/oauth/authorize?client_id=webapp&response_type=code&redirect_uri=http://www.baidu.com 
3.授权服务配置-配置令牌和授权码
//二.授权服务端点配置==============================================================

@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
            //1.密码授权模式需要
            .authenticationManager(authenticationManager)
            //2.授权码模式服务
            .authorizationCodeServices(authorizationCodeServices())
            //3.配置令牌管理服务
            .tokenServices(tokenService())
            //允许post方式请求
            .allowedTokenEndpointRequestMethods(HttpMethod.POST);
}

//授权码的管理服务 ,默认读取 oauth_code表
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
    return new JdbcAuthorizationCodeServices(dataSource);
}

//令牌的管理服务
@Bean
public AuthorizationServerTokenServices tokenService(){
    //创建默认的令牌服务
    DefaultTokenServices services = new DefaultTokenServices();
    //指定客户端详情配置
    services.setClientDetailsService(clientDetailsService());
    //支持产生刷新token
    services.setSupportRefreshToken(true);
    //token存储方式
    services.setTokenStore(tokenStore());
    return services;
}
//基于内存的Token存储
@Bean
public TokenStore tokenStore(){
    return new InMemoryTokenStore();
}

AuthorizationServerEndpointsConfigurer是针对于授权服务的端点配置,主要是配置授权码和令牌该如何管理
API介绍:

  • AuthenticationManager
    认证管理器“password”模式会用到认证管理器
  • TokenStore : token存储方式
    该接口提供了三个默认实现:InMemoryTokenStore基于存储的token存储方案,
    JdbcTokenStore基于数据库的token存储方案,JwtToeknStore基于JWT的存储方案
  • AuthorizationCodeServices
    授权码服务,提供了InMemoryAuthorizationCodeServices基于内存和基于数据库 JdbcAuthorizationCodeServices的授权码存储方案,JdbcAuthorizationCodeServices 是基于数据库的存储方案,所以要导入授权码SQL脚本,JdbcAuthorizationCodeServices 默认读取数据库中的 oauth_code 表中的数据作为授权码的存储表
  • AuthorizationServerTokenServices
    该接口用来配置授权服务器令牌,如配置是否支持Token,Token的存储方式(内 存,jdbc,),token加密,token过期等
4.授权服务配置-端点安全配置
//三.授权服务安全配置===========================================================@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security
            //对应/oauth/check_token ,路径公开
            .checkTokenAccess("permitAll()")
            //允许客户端进行表单身份验证,使用表单认证申请令牌
            .allowFormAuthenticationForClients();
}

AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全策略,我们对/oauth/token_key端点和/oauth/check_token端点进行公开,目的是后续资源服务需要通过该端点进行远程校验Token。

配置总结
授权服务配置了三个东西

  • ClientDetailsServiceConfigurer :配置客户端信息,如同做微信登录的时候获取授权码和令牌时需要传入很多的参数,这些参数就是客户端详情配置的参数
  • AuthorizationServerEndpointsConfigurer:配置token相关端点,以及token如何存取,以及客户端支持哪些类型token
  • AuthorizationServerSecurityConfigurer:配置了端点就要对端点配置约束

Oauth2授权服务配置最佳实践

  • 创建配置类
package cn.itsource.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

import javax.sql.DataSource;

//授权服务配置
@Configuration
//开启授权服务配置
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    //1.客户端详情配置==============================
    //数据源
    @Autowired
    private DataSource dataSource ;
    //编码器
    @Autowired
    private PasswordEncoder passwordEncoder;
    //1.1.定义ClientDetailsService 客户端详情配置服务
    @Bean
    public ClientDetailsService clientDetailsService(){
        //JdbcClientDetailsService的作用是去数据库加载客户端配置,加载表 oauth_client_details
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    //1.2.配置客户端详情
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //ClientDetailsService:就是提供客户端详情配置的一个服务
        clients.withClientDetails(clientDetailsService());
    }

    //2.服务端点:授权码,令牌管理配置======================
  //2.1.认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;
    //2.2.定义 JdbcAuthorizationCodeServices 授权码的服务,基于数据库存储
    @Bean
    public AuthorizationCodeServices  authorizationCodeServices(){
        return new JdbcAuthorizationCodeServices(dataSource);
    }
    //2.3.定义AuthorizationServerTokenServices ,令牌的服务配置
    @Bean
    public AuthorizationServerTokenServices tokenService(){
        //创建默认的令牌服务
        DefaultTokenServices services = new DefaultTokenServices();
        //指定客户端详情配置
        services.setClientDetailsService(clientDetailsService());
        //支持产生刷新token
        services.setSupportRefreshToken(true);
        //token存储方式
        services.setTokenStore(tokenStore());
        return services;
    }
    //2.4.配置令牌的存储
    @Bean
    public TokenStore tokenStore(){
        return new InMemoryTokenStore();
    }
    //2.5.配置授权码和令牌端点服务
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
            //密码授权模式需要
            .authenticationManager(authenticationManager)
            //授权码模式服务
            .authorizationCodeServices(authorizationCodeServices())
            //配置令牌管理服务
            .tokenServices(tokenService())
            //允许post方式请求
            .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    //3.授权服务安全配置,url是否放行等==============================    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                //对应/oauth/token_key 公开
                .tokenKeyAccess("permitAll()")
                //对应/oauth/check_token ,路径公开
                .checkTokenAccess("permitAll()")
                //允许客户端进行表单身份验证,使用表单认证申请令牌
                .allowFormAuthenticationForClients();
    }
}
  • 创建客户端详情配置表SQL
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;`	
INSERT INTO `oauth_client_details` VALUES ('webapp', 'res1', '$2a$10$GPHeNpkKAUJDJcC2XafjkuTyh/P01s2ZoIu0/IsPs6WcXtnv8LNgm', 'all', 'client_credentials,password,authorization_code,refresh_token', 'http://www.baidu.com', null, '7200', '72000', null, 'true');
INSERT INTO `oauth_client_details` VALUES ('webapp2', 'res2', '$2a$10$GPHeNpkKAUJDJcC2XafjkuTyh/P01s2ZoIu0/IsPs6WcXtnv8LNgm', 'all', 'client_credentials,password,authorization_code,refresh_token', 'http://www.baidu.com', '', '7200', '72000', '', 'true');	
  • 创建授权码SQL
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code` (
`code` varchar(255) DEFAULT NULL COMMENT '授权码(未加密)',
`authentication` varbinary(5000) DEFAULT NULL COMMENT 'AuthorizationRequestHolder.java对象序列化后的二进制数据'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
3.5.授权测试
3.5.1.授权码方式获取Token

先进行登录
在这里插入图片描述

第一步,通过浏览器获取授权码,GET访问:
http://localhost:1100/oauth/authorize?client_id=webapp2&response_type=code&redirect_uri=http://www.baidu.com ,操作如下:
在这里插入图片描述
在这里插入图片描述

第二步使用Postmain获取令牌,Post访问:

Url: http://localhost:1100/oauth/token?
client_id=webapp2&client_secret=123&grant_type=authorization_code&code=gD8ZbB&redirect_uri=http://www.baidu.com ,操作如下:

在这里插入图片描述
可以看到这里已经获取到令牌,授权服务的配置暂时告一段落,后续我们就可以带着令牌访问资源服务
注意:内容格式为“x-www-form-urlencoded”

3.5.2.密码授权模式

在授权服务中我们配置了"password"密码模式,"authorization_code"授权码模式两种方式,接下来是测试“password”模式获取,将grant_type修改为“password” 添加username和password两个参数,去掉code参数
在这里插入图片描述

3.5.3.刷新token
http://localhost:3000/oauth/token?grant_type=refresh_token&refresh_token=刷新Token值&client_id=webapp&client_secret=secret
4.资源服务搭建

当客户端(web端,mobile移动端)带着Token向资源服务器发起请求获取资源,资源服务器需要对请求中的Token进行校验以及对资源进行授权,如果Token校验和授权都通过即可返回相应的数据给客户端。

4.1.项目修改
  • Pom.xml
    修改 security-resource-server工程,除了集成EurekaClient以外,还需要集成security以及Oauth2 , 在pom中导入如下依赖:
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

注意:其实spring-cloud-starter-oauth2依赖中已经导入了spring-boot-starter-security

  • 主配置类
//资源服务器配置
@SpringBootApplication
public class ResourceServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class);
    }
}
  • 配置文件
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/ #注册中心地址
  instance:
    prefer-ip-address: true #使用ip地址注册
    instance-id: resource-server:4000  #指定服务的id
server:
  port: 4000
spring:
  application:
    name: resources-server
4.2.Oauth2资源配置

在资源服务中我们需要解决的问题是,当请求进来,请求是需要携带token,我们需要配置资源服务如何对token进行校验和授权。在Oauth2中,对资源服务进行配置的类为:
ResourceServerConfigurerAdapter,该类的源码如下:

/**
 * @author Dave Syer
 */
public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
   @Override
   public void configure(ResourceServerSecurityConfigurer resources)
 throws Exception { }

   @Override
   public void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests().anyRequest().authenticated();
   }
}

注意,上面是源码,不要乱拷贝,API介绍:

  • ResourceServerConfigurerAdapter : 资源服务配置适配器类,用于实现资源服务器的安全访问配置,通过继承该类并结合@EnableResourceServer注解开启资源服务配置。
  • ResourceServerSecurityConfigurer : 资源服务器安全配置器,如配置token校验规则。

oauth2资源服务配置最佳实践

package cn.itsource.config;
import org.springframework.context.annotation.Bean;
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.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
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.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;

//资源服务配置
@Configuration
//开启资源服务配置
@EnableResourceServer
//开启方法授权
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
    //1.资源服务安全配置

    //1.1资源服务令牌验证服务,通过远程校验令牌
    @Bean
    public ResourceServerTokenServices resourceServerTokenServices(){
        //使用远程服务请求授权服务器校验token , 即:资源服务和授权服务器不在一个主机
        RemoteTokenServices services = new RemoteTokenServices();
        //授权服务地址 , 当浏览器访问某个资源时就会调用该远程授权服务地址去校验token
        //要求请求中必须携带token
        services.setCheckTokenEndpointUrl("http://localhost:3000/oauth/check_token");
        //客户端id,对应认证服务的客户端详情配置的clientId
        services.setClientId("webapp");
        //密钥,对应认证服务的客户端详情配置的clientId
        services.setClientSecret("123");
        return services;
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //我的资源名称是什么
        resources.resourceId("res1");
        //用来校验,解析Token的服务
        resources.tokenServices(resourceServerTokenServices());

    }

    //2.资源服务的资源的授权配置,比如那些资源放行,那些资源需要什么权限等等
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //校验scope必须为all , 对应认证服务的客户端详情配置的clientId
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                //关闭跨域伪造检查
                .and().csrf().disable()
                //把session设置为无状态,意思是使用了token,那么session不再做数据的记录
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}

这里通过 ResourceServerSecurityConfigurer 指定了 ResourceServerTokenServices ,即指定了资源服务令牌验证方案,然后使用了 RemoteTokenServices 远程校验方案,当请求到达资源服务器,资源服务会带着请求中的token,向认证服务器发起远程校验(/oauth/check_token)

  • ResourceServerSecurityConfigurer :指定了资源的ID,这里需要跟认证服务中配置的资源ID一致见:AuthorizationServerConfig.configure配置的resourceIds
  • 通过tokenServices指定令牌校验服务
  • ResourceServerTokenServices :令牌校验服务配置

注意:这里在配置类上开启了@EnableGlobalMethodSecurity(prePostEnabled = true),因为我这里使用方法授权的方式指明我们的controller方法需要什么样的权限才能访问,当然也可以使用web授权的方式(回顾web授权和方法授权)

4.3.编写资源controller
@RestController
public class ResourceController {

    @RequestMapping("/employee/list")
    //这是一个测试方法,如果资源服务器对token校验通过就能够访问该资源
    @PreAuthorize("hasAnyAuthority('employee:list')")
    public String list(){
        return "您的token验证通过,已经访问到真正的资源 employee:list";
    }

    @RequestMapping("/employee/add")
    //这是一个测试方法,如果资源服务器对token校验通过就能够访问该资源
    @PreAuthorize("hasAnyAuthority('employee:add')")
    public String add(){
        return "您的token验证通过,已经访问到真正的资源 employee:add";
    }
}

该Controller作为测试返回数据,当客户端请求“oauth-resource”过来,如果Token校验成功,将会看到““您的token验证通过,已经访问到真正的资源””

4.4.测试

向资源服务器发起请求,请求头携带:Authorization=Bearer token值 ,如下:
在这里插入图片描述
这里我们已经成功的访问到真正的资源 , 请求资源之前,资源服务器会发送远程请求到授权服务器验证token的合法性,并且根据当前token获取权限列表,然后在进行授权,如果权限列表拥有资源(controller的方法)所需要的权限,即可访问成功 。
错误示范:
如果Token是无效的会出现如下信息:
在这里插入图片描述
如果Token中的权限不包含资源所需要的权限会出现如下信息:
在这里插入图片描述
思考:那是不是每个资源服务(微服务)都需要做如上的配置呢?

5.小结

我们基于Oauth2配置授权服务器和资源服务器完成了请求授权并获取资源的操作,整个流程如下:
在这里插入图片描述

四.SpringSecurity-OAUTH2+JWT令牌

1.认识JWT
1.1.为什么要用JWT令牌

根据前面的案例,我们每次访问资源都要通过资源服务器远程调用认证服务器进行token的校验和授权才能访问到资源。但是如果我们的访问比较频繁,并发比较高,那么这种权限校验方式无疑比较消耗性能。而JWT就是用来解决远程校验令牌的问题。

1.2.什么是JWT

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。客户端身份经过服务器验证通过后,会生成带有签名的 JSON 对象并将它返回给客户端。客户端在收到这个 JSON 对象后存储起来。
在以后的请求中客户端将 JSON 对象连同请求内容一起发送给服务器,服务器收到请求后通过 JSON 对象标识用户,如果验证不通过则不返回请求的数据。

总结:使用JWT生产的Token是安全的,可以理解成就是在我们之前的Token基础上做了加密处理,实现了数据的安全传输 , 也实现了资源服务器对Token的自校验

1.3.JWT特点

基于JSON,方便解析
可以在令牌中定义内容,方便扩展
非对称加密算法即数字签名,JWT防篡改
资源服务使用JWT可以不依赖认证服务即可完成授权

1.4.JWT组成

https://www.jianshu.com/p/99a458c62aa4
JWT包含三部分组成

  • 头部 (header): JSON格式,描述JWT的最基本的信息,如签名算法等效果如下:
    {“type”:“JWT”,“alg”:“HS256”} 这里指明了签名算法是HS256算法。在使用过程中会对该 JSON进行BASE64编码如:yJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  • 载荷(playload) :JSON格式,包含了需要的数据内容,也需要BASE64编码。包含三部分
    • 标准中注册的声明(建议但不强制使用)
      iss: jwt签发者
      sub: jwt所面向的用户, zs
      aud: 接收jwt的一方
      exp: jwt的过期时间,这个过期时间必须要大于签发时间
      nbf: 定义在什么时间之前,该jwt都是不可用的.
      iat: jwt的签发时间
      jti: jwt的唯一身份标识,主要用来作为一次性token。
    • 公共的声明 :公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
    • 私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

定义一个payload: 如 {“sub”:“1234”,“name”:“xxx”,“admin”:true} 进行Base64编码后得到JWT第二部分如 eyJzdWIiOiIxMjM0NTY3ODkbmFtZSI6IkpvaG4gRG9lIiwiYW…

  • 签名 (signature): 通过指定的算法生成哈希,以确保数据不会被篡改。jwt的第三部分是一个签证信息,通过指定的算法生成哈希,以确保数据不会被篡改,这个签证信息由三部分组成:head(base64编码后的);playload(base64编码后的);secret(秘钥)
    签名后的密文构成JWT第三部分如:ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXr

最后JWT构建的内容编码后的字符串
“eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NTc5MDQxODF9.ThecMfgYjtoys3JX7dpx3hu6pUm0piZ0tXXreFU_u3Y”

2.授权服务配置JWT

修改认证服务 security-auth-server

2.1.基于JWT的TokenStore配置

修改认证服务指定把Token存储方案由InMemoryTokenStore修改为JwtTokenStore,修改 AuthorizationServerConfig配置类,定义如下:

  @Bean
  public TokenStore tokenStore(){
  	return new JwtTokenStore(jwtAccessTokenConverter());
  }

JwtTokenStore是以JWT方式存储令牌 , JwtTokenStore需要一个 令牌转换器对象JwtAccessTokenConverter(JWT令牌校验工具) ,该对象把JWT编码的令牌值和OAuth身份验证信息进行互相转换,定义如下:

//设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
    private final String sign_key  = "123";

    //JWT令牌校验工具
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = 
        new JwtAccessTokenConverter();
        //设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
        jwtAccessTokenConverter.setSigningKey(sign_key);
        return jwtAccessTokenConverter;
    }

JwtAccessTokenConverter负责把JWT编码的令牌值和OAuth身份验证信息进行互相转换 , 并且JWT提供了签名机制让数据传输跟安全,所有这里的转换器需要设置前面的密钥:SigningKey ,这里简单使用对称密钥(支持非对称)设置为 “123” 。
注意:授权服务的签名密钥要跟资源服务的签名密钥一致。

2.2.令牌管理服务修改

因为现在使用了JWT模式的令牌存储方案,那么在令牌管理服务AuthorizationServerTokenServices的配置中需要指定JwtAccessTokenConverter , 因为 AuthorizationServerTokenServices的作用是创建,获取,刷新Token,既然我们的TokenStore使用了JWT方案并指定了token转换器,那么我们的AuthorizationServerTokenServices也要指定相同的token转换器,修改 AuthorizationServerTokenServices 的定义如下:

//配置令牌管理服务
    @Bean
    public AuthorizationServerTokenServices authorizationServerTokenServices(){

        //创建默认的令牌服务
        DefaultTokenServices services = new DefaultTokenServices();
        //设置客户端信息服务
        services.setClientDetailsService(clientDetailsService);
        //支持token刷新
        services.setSupportRefreshToken(true);
        //token存储方式
        services.setTokenStore(tokenStore);

        //设置token增强 - 设置token转换器
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();  
 	    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
        services.setTokenEnhancer(tokenEnhancerChain);  //jwtAccessTokenConverter()

        //token有效时间2小时
        services.setAccessTokenValiditySeconds(72000);
        //属性令牌默认有效时间
        services.setRefreshTokenValiditySeconds(200000);
        return services;
    }
2.3.测试

经过如上步骤,我们已经把token修改为JWT方案,重新走一遍测试流程,
获取授权码

http://localhost:3000/oauth/authorize?client_id=webapp&response_type=code&redirect_uri=http://www.baidu.com

获取token

http://localhost:3000/oauth/token?client_id=webapp&client_secret=secret&grant_type=authorization_code&code=授权码&redirect_uri=http://www.baidu.com

在这里插入图片描述
校验token

http://localhost:3000/oauth/check_token?token=token值

访问资源 ,请求头带参数: “Authorization= Bearer token的值“

http://localhost:4000/oauth-resource
3.资源服务配置JWT

我们之前的资源服务使用的远程校验方式去校验token(非JWT令牌)的正确性,以及token对应认证信息(用户名,权限等)都是远程获取的,而现在我们的令牌是在认证服务方使用JWT方式生成的,并且使用了签名机制,而JWT令牌的特点就是所有的认证信息包含在令牌中,所以这里我们要修改资源服务校验令牌的方式,不再需要远程校验,修改如下

3.1.定义JwtAccessTokenConverter

修改资源服务 security-resource-server ,在资源服务也定义一份和授权服务一样的 TokenStore和JwtAccessTokenConverter ,在资源服务的配置类ResourceServerConfig中加如下代码:

//修改JWT令牌
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
    private final String sign_key  = "123";

    //JWT令牌校验工具
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
        jwtAccessTokenConverter.setSigningKey(sign_key);
        return jwtAccessTokenConverter;
    }

这里的签名密钥,tokenStore,令牌转换器和认证服务都是一样的。

3.2.修改ResourceServerSecurityConfigurer
//资源服务器安全性配置
@Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //资源ID
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore())
                //验证令牌的服务,令牌验证通过才允许获取资源
                //.tokenServices(resourceServerTokenServices())
                //无状态
                .stateless(true);
    }

注意:这里吧ResourceServerSecurityConfigurer配置中的
.tokenServices(resourceServerTokenServices())注释掉,意思是不在使用远程的令牌校验方式 ,而是使用.tokenStore(tokenStore())的校验 - 客户端自己校验

3.3.测试

重新走一遍流程即可

五.微服务之间的授权

1.概述
1.1.微服务之间为什么需要授权

当客户端先资源服务发起请求,请求头携带Token,在资源服务(A)需要校验Token进行访问授权,但是我们一个请求往往是需要多个微服务一起完成,如果A服务调用B服务调用C服务,那么A服务校验Token之后,B服务需不需要也校验Token呢?答案是当然的,因为每个微服务都是独立的都需要做好权限校验工作?那么如何实现服务之间的授权?

1.2.解决方案
  • 方案一:服务之间临时
    解决一就是在A服务调用B服务的时候是通过Feign实现调用,底层也是Http协议,那么我们就可以使用Feign请求拦截器拦截到Feign的请求,给请求头添加Token。

如果只是服务之间授权,没有用户上下文的参与,我们可以在Feign的拦截器中使用“客户端模式”从认证服务获取Token,然后把Token添加到请求服务B的请求头中,这样一来,B服务就可以对请求中的Token进行校验从而进行授权。如图:
在这里插入图片描述
但是这种方案也有一定的问题,就是消费者服务需要额外发起请求向认证服务获取Token,并且需要对Token做缓存和定时刷新来保证Token的重用和保证Token失效,这样一来就增加了额外的网络开销和代码量,如果请求有用户参与,那么就不能采用从认证服务重新获取Token,而是需要将客户端带过来的Token通过请求头带给后续请求的服务,因为我们需要通过客户端的Token校验当前用户的权限。

  • 方案二:
    方案二采用直接把客户端提交给资源服务的Token继续向下游资源服务提交,那么久需要在消费者服务A向提供者服务器B发起请求(Feign)的时候进行拦截(Feign的拦截器),在拦截器中获取请求头中的Token添加到Fiegn的请求头中,这样一来Token就由A服务传递给了B服务,B服务就可以对Token进行校验了。如图:
    在这里插入图片描述
    方式二减少了向认证服务器获取令牌的网络开销,从代码量和性能上来说,方式二都略胜一筹,我们选择方式二。
2.服务提供者
2.1.搭建项目

服务提供者相当于上面所说的“服务B”,我们将security-resources-server作为服务消费者,创建项目security-resources-server-2作为服务提供者,服务提供和的搭建方式同security-resources-server也需要集成“Oauth2的资源配置”,并且暴露资源访问接口让security-resources-server远程调用。

  • 创建新项目:Pom.xml
<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>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
  • 配置类
//资源服务器配置
@SpringBootApplication
public class ResourceServerApplication2 {
    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication2.class);
    }
}
  • 配置文件
eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:1000/eureka/ #注册中心地址
  instance:
    prefer-ip-address: true #使用ip地址注册
    instance-id: resource-server2:5000  #指定服务的id
server:
  port: 5000
spring:
  application:
    name: resources-server2
2.2.Oauth2资源配置

其实每个资源服务的配置大多一样,所以可以抽取公共的资源配置模块,这里为了不增加复杂度就没有抽取了,而是把security-resources-server的资源配置复制过来。

//资源服务配置
@Configuration
@EnableResourceServer   //开启资源服务配置
@EnableGlobalMethodSecurity(prePostEnabled = true)  //开启方法授权
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //配置资源id ,跟AuthorizationServerConfig.configure配置的resourceIds一样
    public static final String RESOURCE_ID = "res1";

    //设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
    private final String sign_key  = "123";
    //资源服务器安全性配置
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        //资源ID
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore())
                //验证令牌的服务,令牌验证通过才允许获取资源
                //.tokenServices(resourceServerTokenServices())
                //无状态,仅在这些(RESOURCE_ID对应的资源)资源上使用允许使用token校验
                .stateless(true);
    }

    //修改JWT令牌,基于JWT的验证方案----------------------------
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //JWT令牌校验工具
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //设置JWT签名密钥。它可以是简单的MAC密钥,也可以是RSA密钥
        jwtAccessTokenConverter.setSigningKey(sign_key);
        return jwtAccessTokenConverter;
    }

    //SpringSecurity的相关配置
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                //校验scope必须为all , 对应认证服务的客户端详情配置的clientId
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                //关闭跨域伪造检查
                .and().csrf().disable()
                //把session设置为无状态,意思是使用了token,那么session不再做数据的记录
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
2.3.编写Controller
@RestController
public class Resource2Controller {
    @RequestMapping("/oauth-resource2")
    //这是一个测试方法,如果资源服务器对token校验通过就能够访问该资源
    @PreAuthorize("hasAnyAuthority('dept:list')")
    public String resources(){
        return "oauth-resource2:您的token验证通过,已经访问到真正的资源";
    }
}

这里指定了“oauth-resource2”资源需要“dept:list”才能访问。

3.服务消费者
3.1.导入依赖

我们修改security-resources-server服务,使用Feign去调用security-resources-server2服务实现服务之间的通信,所以这里需要集成Feign,修改security-resources-server如下:

  • Pom.xml 加入Feign的依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.2.集成Feign
  • 配置类开启Feign - @EnableFeignClients
@SpringBootApplication
@EnableFeignClients
public class ResourceServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceServerApplication.class);
    }
}
  • 编写Feign客户端接口 : 调用security-resources-server2
//调用 security-resources-server-2服务的Feign接口
@FeignClient("resources-server2")
public interface Resource2FeignClient {
    //这是一个测试方法,如果资源服务器对token校验通过就能够访问该资源
    @GetMapping("/oauth-resource2")
    public String resources();
}
3.3.服务通信

修改ResourceController,使用Resource2FeignClient 实现服务远程调用

@RestController
public class ResourceController {
    @Autowired
    private Resource2FeignClient resource2FeignClient ;
    @RequestMapping("/oauth-resource")
    //这是一个测试方法,如果资源服务器对token校验通过就能够访问该资源
    @PreAuthorize("hasAnyAuthority('employee:list')")
    public String resources(){
        return resource2FeignClient.resources();
        //return "您的token验证通过,已经访问到真正的资源";
    }
}
3.4.测试通信

走一遍测试流程,最终效果如下:
在这里插入图片描述
这里出现了没有访问权限,我们的请求线路是
Postmain -> resources-server(/oauth-resource) -> resources-server-2(/oauth-resource2)
其实我们的Token中是包含了 resources-server(/oauth-resource)说需要的权限
“employee:list”,出现无选项访问是在resources-server调用resources-server2的时候,由于resources-server2集成了oauth2的资源服务器配置,而resources-server并没有传递Token给resources-server2,所以出现了访问失败。下面我们就来实现服务之间的授权。

4.拦截器配置

根据我们之前分析的服务授权方案,我们需要在消费者向提供者发起请求前,使用Feign的拦截器从请求头中获取Token,然后带着Token向提供者发起请求,但是此种情况在微服务中大量存在,所以我们这决定讲上述业务抽取成公共的模块,消费者服务进行依赖即可。

4.1.创建模块

创建新的模块security-feign-oauth2-common,导入依赖

  • Pom.xml
<dependencies>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
4.2.定义Feign的拦截器

我们需要在资源服务提供者方定义Feign拦截器,在拦截器中像认证服务发送请求获取Token(client_credentials客户端模式),请求头添加Token,然后再把Token添加到请求头中,另外我们在这里做了Token缓存,定时30分钟刷新Token。另外我们这里获取token使用的是“client_credentials”客户端模式,因为服务和服务之间是绝对信任的,所以无需复杂的授权码模式。

//Fiegn拦截器
public class DefaultOAuth2FeignRequestInterceptor implements RequestInterceptor {

    //请求头中的token
    private final String AUTHORIZATION_HEADER = "Authorization";
    
    @Override
    public void apply(RequestTemplate requestTemplate) {

        //1.获取请求对象
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = servletRequestAttributes.getRequest();

        //2.从请求头获取到令牌
        String authorization = request.getHeader(AUTHORIZATION_HEADER);

        //3.把令牌添加到Fiegn的请求头
        requestTemplate.header(AUTHORIZATION_HEADER,authorization);
    }
}
4.3.Feign拦截器配置

需要把我们自定义的Feign拦截器类配置成Bean,需要的参数从配置文件中加载。

//Feign的拦截器配置
@Configuration
public class FeignInterceptorConfig {
    @Bean
    public RequestInterceptor oAuth2RequestInterceptor() {
        return new DefaultOAuth2FeignRequestInterceptor();
    }
}
4.4.资源服务依赖拦截器模块

修改服务消费者“security-resources-server”,依赖拦截器模块

<dependency>
    <groupId>cn.itsource</groupId>
    <artifactId>security-feign-interceptor-token</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
4.5.测试服务之间授权

发起请求调用,查看提供者服务是否能够降权成功,并且访问到资源。

5.Hystrix导致Token转发失败
5.1.Hystrix引发的问题

上面的案例中我们的“security-resources-server”消费者服务并没有集成Hystrix,如果我们集成了Hystrix那么在Feign的拦截器中是没办法获取到请求头中的信息的,这是因为Hystrix默认的隔离策略是线程池,每个请都会被分配到一个新的线程,导致请对象无法获取,解决方案有两种,一是使用信号量隔离,在配置文件中加入如下配置:
“hystrix.command.default.execution.isolation.strategy=SEMAPHORE”,解决方案二是修改Hystrix的隔离策略,我们这里使用第二种方式,因为使用信号量隔离会让请求变成单线程执行,官方也不推荐。
在这里插入图片描述
解决方案:
在这里插入图片描述

5.2.集成Hystrix

修改工程“security-resources-server”消费者服务,我们让Feign集成Hystrix

  • 开启Hystrix
feign:
  hystrix:
    enabled: true
5.3.Feign接口指定Fallback

修改 ResourceFeignClient 接口,指定fallback实现托底

//调用 security-resources-server-2服务的Feign接口
@FeignClient(value = "resources-server2",fallback = ResourceFeignClientFallback.class)
public interface ResourceFeignClient {

    //这是一个测试方法,如果资源服务器对token校验通过就能够访问该资源
    @GetMapping("/oauth-resource2")
    public String resources();
}
5.4.编写降级类
@Component
public class ResourceFeignClientFallback implements Resource2FeignClient {
    @Override
    public String resources() {
        return "资源服务 resources-server2 不可用";
    }
}
5.5 测试Hystrix导入转发失败

重新走一遍测试流程,发现返回的结果为“资源服务 resources-server2 不可用”,即返回了托底数据,这是因为Hystrix的隔离策略导致了Feign拦截器状态请求失败,如果在拦截器中去打印authorization(Token) 你看到的将是空值。

5.6.定义Hystrix隔离策略

在模块“security-feign-interceptor-token”拦截器模块中定义隔离策略类,该类的作用是:将现有的并发策略作为新并发策略的成员变量,在新并发策略中,返回现有并发策略的线程池、Queue;

@Configuration
public class FeignHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {

    private HystrixConcurrencyStrategy hystrixConcurrencyStrategy;

    public FeignHystrixConcurrencyStrategy() {
        try {
            this.hystrixConcurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
            if (this.hystrixConcurrencyStrategy instanceof FeignHystrixConcurrencyStrategy) {
                return;
            }
            HystrixCommandExecutionHook commandExecutionHook =
                    HystrixPlugins.getInstance().getCommandExecutionHook();
            HystrixEventNotifier eventNotifier = HystrixPlugins.getInstance().getEventNotifier();
            HystrixMetricsPublisher metricsPublisher = HystrixPlugins.getInstance().getMetricsPublisher();
            HystrixPropertiesStrategy propertiesStrategy =
                    HystrixPlugins.getInstance().getPropertiesStrategy();

            HystrixPlugins.reset();
            HystrixPlugins instance = HystrixPlugins.getInstance();
            instance.registerConcurrencyStrategy(this);
            instance.registerCommandExecutionHook(commandExecutionHook);
            instance.registerEventNotifier(eventNotifier);
            instance.registerMetricsPublisher(metricsPublisher);
            instance.registerPropertiesStrategy(propertiesStrategy);
        } catch (Exception e) {
            System.out.println("策略注册失败");
        }
    }

    @Override
    public <T> Callable<T> wrapCallable(Callable<T> callable) {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        return new WrappedCallable<>(callable, requestAttributes);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixProperty<Integer> corePoolSize,
                                            HystrixProperty<Integer> maximumPoolSize,
                                            HystrixProperty<Integer> keepAliveTime,
                                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, corePoolSize, maximumPoolSize, keepAliveTime,
                unit, workQueue);
    }

    @Override
    public ThreadPoolExecutor getThreadPool(HystrixThreadPoolKey threadPoolKey,
                                            HystrixThreadPoolProperties threadPoolProperties) {
        return this.hystrixConcurrencyStrategy.getThreadPool(threadPoolKey, threadPoolProperties);
    }

    @Override
    public BlockingQueue<Runnable> getBlockingQueue(int maxQueueSize) {
        return this.hystrixConcurrencyStrategy.getBlockingQueue(maxQueueSize);
    }

    @Override
    public <T> HystrixRequestVariable<T> getRequestVariable(HystrixRequestVariableLifecycle<T> rv) {
        return this.hystrixConcurrencyStrategy.getRequestVariable(rv);
    }

    static class WrappedCallable<T> implements Callable<T> {
        private final Callable<T> target;
        private final RequestAttributes requestAttributes;

        public WrappedCallable(Callable<T> target, RequestAttributes requestAttributes) {
            this.target = target;
            this.requestAttributes = requestAttributes;
        }

        @Override
        public T call() throws Exception {
            try {
                RequestContextHolder.setRequestAttributes(requestAttributes);
                return target.call();
            } finally {
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}
5.7.测试

发起请求调用,查看提供者服务是否能够降权成功,并且访问到资源。

六.获取用户上下文

1.UserDetailsService返回用户信息
//4.把用户信息封装成UserDetails返回
return new User(user.getUserInfoTOJSON(),user.getPassword(),authorities);
2.在资源服务接收Token中的用户信息
package cn.itsource.hrm.web.filter;
import cn.itsource.hrm.constants.Oauth2Constants;
import cn.itsource.hrm.holder.UserContextHolder;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;

//接收密文的Token,解析Token中的用户信息
@Component
public class AcceptToken extends OncePerRequestFilter {

    @Autowired
    private TokenStore tokenStore;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取密文Token
        String authorization = request.getHeader(Oauth2Constants.TOKEN_NAME_IN_HEADER);
        if(StringUtils.hasLength(authorization)){
            authorization = authorization.replace("Bearer ","");
            //解析Token得到OAuth2AccessToken
            OAuth2AccessToken oAuth2AccessToken = tokenStore.readAccessToken(authorization);
            //取出Token中的用户信息
            String userJson = oAuth2AccessToken.getAdditionalInformation().get("user_name").toString();
            //封装成Map
            Map<String,Object> map = JSON.parseObject(userJson, Map.class);
            //把用户信息封装成一个UserContext,设置到UserContextHolder和线程绑定
            UserContextHolder.setUser(Long.valueOf(map.get("id").toString()), map.get("username").toString());
        }
        filterChain.doFilter(request, response);
    }
}
3.用户上下文工具
public class UserContextHolder {
    private static ThreadLocal<UserContext> userContextThreadLocal = new ThreadLocal<>();

    public static void setUser(Long id,String username){
        userContextThreadLocal.set(new UserContext(id,username));
    }

    public static UserContext getUser(){
        return userContextThreadLocal.get();
    }
}
public class UserContext{
    private Long userId;
    private String username;

    public UserContext(Long userId , String username){
        this.userId = userId ;
        this.username = username;
    }
    public UserContext(){}

    public Long getUserId(){
        return userId;
    }
    public String getUsername(){
        return username;
    }
}
4.获取用户信息
UserContextHolder.getUser();
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值