第二章 Spring Security Oauth2认证授权

学习目标:

  • 了解认证授权基本概念
  • 了解RBAC权限数据模型
  • 了解常见的认证方式
  • SpringSecurity安全框架入门
  • 基于Spring Security Oauth2实现分布式系统认证(授权码模式,密码模式)
  • 基于Spring Security Oauth2实现分布式系统授权

.基本概念

​ 这两个术语通常在安全性方面相互结合使用,尤其是在获得用户对系统的访问权限时。两者都是非常重要的主题,这两个术语在概念上是非常不同的。虽然它们经常放在一起使用,但它们彼此完全不同。

​ 身份验证意味着确认你自己的身份,而授权意味着授予对系统的访问权限。简单来说,身份验证是验证你的身份的过程,而授权是验证你是否有权访问的过程。

什么是认证

​ 系统为什么要有认证?-让系统知道是谁?

​ 认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

**认证:**身份验证是验证用户凭据,如用户名和密码,系统通过登录密码验证用户身份。身份验证通常通过用户名和密码完成。如何身份合法可以继续访问,不合法则拒绝访问。常见的身份认证方式有:用户名密码登录,二维码登录,手机短信登录,指纹识别,扫脸识别等认证方式。

例如,当你将ATM卡输入ATM机时,机器会要求你输入你的密码。输入密码正确后,银行会确认你的身份证明该卡真正属于你,并且你是该卡的合法所有者。通过验证你的密码,银行实际上会验证你的身份,这称为身份验证。它只是确定你是谁,没有别的。 

什么是授权

​ 为什么要授权:

​ 认证是为了保护身份的合法性,授权则是为了更细粒度的对因数资源(数据)进行划分,授权实在认证通过的前提下发生的。控制不同的用户能够访问不同的资源。

​ 授权发生在系统成功验证你的身份后,最终会授予你访问资源的完全权限。简单来说,授权决定了你访问系统的能力以及达到的程度。验证成功后,系统验证你的身份后,即可授权你访问系统资源。

**授权:**授权是用户认证后根据用户的权限控制用户访问资源的过程,用于资源的访问权限则正常访问,没有权限则拒绝访问。

​ 例如:使用ATM进行转账,如果是银行卡是二类卡每日转账金额不得超过1万元。

权限数据模型

​ 如何进行授权即如何对用户访问资源进行控制,首先需要学习授权相关的数据模型。 授权可简单理解为Who对What(which)进行How操作,包括如下:

Who,即主体(Subject),主体一般是指用户,也可以是程序,需要访问系统中的资源。 What,即资源 (Resource),如系统菜单、页面、按钮、代码方法、系统商品信息、系统订单信息等。系统菜单、页面、按 钮、代码方法都属于系统功能资源,对于web系统每个功能资源通常对应一个URL;系统商品信息、系统订单信息 都属于实体资源(数据资源),实体资源由资源类型和资源实例组成,比如商品信息为资源类型,商品编号 为001 的商品为资源实例。 How,权限/许可(Permission),规定了用户对资源的操作许可,权限离开资源没有意义, 如用户查询权限、用户添加权限、某个代码方法的调用权限、编号为001的用户的修改权限等,通过权限可知用户 对哪些资源都有哪些操作许可。

主体、资源、权限关系如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 主体、资源、权限相关的数据模型如下:
    • 主体(用户id、账号、密码、…)
    • 资源(资源id、资源名称、访问地址、…)
    • 权限(权限id、权限标识、权限名称、资源id、…)

主体(用户)、资源、权限关系如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通常企业开发中将资源和权限表合并为一张权限表,如下:

资源(资源id、资源名称、访问地址、…)

权限(权限id、权限标识、权限名称、资源id、…)

合并为:

权限(权限id、权限标识、权限名称、资源名称、资源访问地址、…)

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

RBAC数据模型

​ 如何实现授权?业界通常基于RBAC模型(基于角色的访问控制Role/Resource-Based Access Control)实现授权。 RBAC认为授权实际就是who,what,how三者之间的关系,即who对what进行how的操作。Who,权限的拥用者或主体(如Principal、User、Group、Role、Actor等等);what,权限针对的对象或资源(Resource、Class) ;How,具体的权限(Privilege,正向授权与负向授权)。简单一点说吧就是,我们通过给角色授权,然后将附有权利的角色施加到某个用户身上,这样用户就可以实施相应的权利了。通过中间角色的身份,是权限管理更加灵活:角色的权利可以灵活改变,用户的角色的身份可以随着场所的不同而发生改变等。这样这套RBAC就几乎可以运用到所有的权限管理的模块上了。

基于角色的访问控制

​ RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

根据上图中的判断逻辑,授权代码可表示如下:

if(主体.hasRole("总经理角色id")){
    //查询工资 
}else{
    //权限不足
}

如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断用户的角色是否是 总经理或部门经理”,修改代码如下:

if(主体.hasRole("总经理角色id") || 主体.hasRole("部门经理角色id")){ 
	//查询工资 
}else{
    //权限不足
}

根据上边的例子发现,当需要修改角色的权限时就需要修改授权的相关代码,系统可扩展性差。

基于资源的访问控制

​ RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

根据上图中的判断,授权代码可以表示为:

//该方法调用,必须要求当前用户用户用户查询工资权限(角色关联的权限)
public UserSalary querySalary(Long UserId){
    //获取当前用户角色
    //获取用户包含角色中权限列表
    if(主体.hasPermission("查询工资权限标识")){ 
        查询工资 
    }  
}

优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也不需要修授权代码,系统可扩展性强。

常见的认证方式

登录认证几乎是任何一个系统的标配,web 系统、APP、PC 客户端等都需要注册、登录、授权。

应用场景

​ 目标:以我们九点钟移动办公为例,首先需要在移动APP端注册一个账号。拥有了账号(系统用户)之后,我们需要输入用户名(比如手机号或邮箱)、密码完成登录过程。之后如果在一段时间内再次打开APP,是不需要输入用户名和密码的,只有在连续长时间不登录的情况下(例如一个月没登录过)访问APP,需要再次需要输入用户名和密码。如果使用频率很频繁,通常是一年都不用再输一次密码。

提炼出来整个过程大概就是如下几步:

  • 首次使用,需要通过邮箱或手机号注册
  • 注册完成后,需要提供用户名和密码完成登录
  • 下次再使用,通常不会再次输入用户名和密码即可直接进入系统并使用其功能(除非连续长时间未使用)

基于JWT的Token认证

​ JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

认证过程

  1. 依然是用户登录系统;
  2. 服务端验证,将认证信息通过指定的算法(例如对称HS256,非对称加密RS256)进行加密,例如对用户名和用户所属角色进行加密,加密私钥是保存在服务器端的,将加密后的结果发送给客户端,加密的字符串格式为两个"." 分隔的字符串 Token,分别对应头部载荷签名,头部和载荷都可以通过 base64 解码出来,签名部分不可以;
  3. 客户端拿到返回的 Token,存储到 local storage,Cookie中 或本地数据库;
  4. 下次客户端再次发起请求,将 Token 附加到 header 中;
  5. 服务端获取 header 中的 Token ,通过相同的算法对 Token 中的用户名和所属角色进行相同的加密验证,如果验证结果相同,则说明这个请求是正常的,没有被篡改。这个过程可以完全不涉及到查询 Redis 或其他存储;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

优点

  • 使用 json 作为数据传输,有广泛的通用型,并且体积小,便于传输;
  • 不需要在服务器端保存相关信息;
  • jwt 载荷部分可以存储业务相关的信息(非敏感的),例如用户信息、角色等;

缺点

每次发请求必须携带Jwt令牌,客户端需要 编码实现(放在请求头中:Authorization 值: Bearer Jwt令牌);令牌一旦签发决定有效期(刷新令牌)

Cookie-Session 认证

​ 早期互联网以 web 为主,客户端是浏览器,所以 Cookie-Session 方式最那时候最常用的方式,直到现在,一些 web 网站依然用这种方式做认证。

认证过程大致如下:

  1. 用户输入用户名、密码或者用短信验证码方式登录系统;
  2. 服务端验证后,创建一个 Session 信息(一般存用户信息),并且将 SessionID 存到 cookie,发送回浏览器;
  3. 下次客户端再发起请求,自动带上 cookie 信息,服务端通过 cookie 获取 Session 信息进行校验;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

弊端

  • 只能在 web 场景下使用,如果是 APP 中,不能使用 cookie 的情况下就不能用了;
  • 即使能在 web 场景下使用,也要考虑跨域问题,因为 cookie 不能跨域;
  • cookie 存在 CSRF(跨站请求伪造)的风险;
  • 如果是分布式服务,需要考虑 Session 同步问题; 解决方案:tomcat集群(Session自动复制);springSession

OAuth 认证

​ OAuth (开放授权标准)认证比较常见的功能有第三方登录:微信登录、微博登录、qq登录等,简单来说就是利用这些比较权威的网站或应用开放的 API 来实现用户登录,用户可以不用在你的网站或应用上注册账号,直接用已有的微信、微博、qq 等账号登录。

这一样一来,即省了用户注册的时间,又简化了你的系统的账号体系。从而既可以提高用户注册率可以节省开发时间,同时,安全性也有了保障。

维基百科对它的解释摘要如下:

OAuth允许用户提供一个令牌(access_token),而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。

假设我们开发了一个电商平台,并集成了微信登录,以这个场景为例,说一下 OAuth 的工作原理。讲之前需要了解其中涉及到的几个角色:

  • 用户:用户
  • 客户端:用户使用的 APP 端或 web 端应用
  • 认证(授权)服务器:负责发放token的服务,以及想要访问受保护资源的客户端都需要向认证服务器注册信息
  • 资源服务器:受保护的API资源,例如微信,微博基本信息

接下来开始在我们的电商平台web端实现微信登录功能。微信网页授权是授权码模式(authorization code)的 OAuth 授权模式。

  1. 我们电商平台的用户过来登录,常用场景是点击“微信登录”按钮;
  2. 接下来,用户终端将用户引导到微信授权页面-手机扫码;
  3. 用户同意授权,应用服务器重定向到之前设置好的 redirect_uri (应用服务器所在的地址),并附带上授权码(code);
  4. 应用服务器用上一步获取的 code 向微信授权服务器发送请求,获取 access_token,也就是上面说的令牌;
  5. 之后应用服务器用上一步获取的 access_token 去请求微信授权服务器获取用户的基本信息,例如头像、昵称,生日,所属地区等;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Spring Security

Spring Security介绍

​ Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。由于它 是Spring生态系统中的一员,因此它伴随着整个Spring生态系统不断修正、升级,在spring boot项目中加入spring security更是十分简单,使用Spring Security 减少了为企业系统安全控制编写大量重复代码的工作。

SpringBoot集成Security

两件事:

  • 认证(验证用户信息-用户名密码是否合法)
  • 授权(提供角色权限)
  • 验权(验证当前用户是否有权限访问资源)

创建maven工程

新建maven工程security_springboot:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itheima</groupId>
    <artifactId>security_springboot</artifactId>
    <version>1.0-SNAPSHOT</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies><!-- 以下是>spring boot依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 以下是>spring security依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

启动类

package com.itheima;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

配置文件

server:
  port: 8080
spring:
  application:
    name: security_springboot

Controller

package com.itheima.security.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class TestController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

    @GetMapping("/r/r1")
    public String r1() {
        return "r1";
    }

    @GetMapping("/r/r2")
    public String r2() {
        return "r2";
    }
}

启动项目发现,自动跳转到登录页面:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Security框架提供默认用户名:user 默认密码:每次启动应用生成密码在控制台输出 。

继续优化方向:

  • 不再使用框架提供默认用户user------>从数据库动态获取用户信息|内存中定义用户信息(分配角色权限)

  • 授权控制,控制资源访问,哪些请求/方法 用户具有什么权限才能访问

安全配置类

spring security提供了用户名密码登录、退出、会话管理等认证功能,只需要配置(拷贝即可)即可使用。安全配置的内容包括:用户信息密码编码器安全拦截机制

package com.itheima.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

/**
 * 安全配置信息
 *
 * @EnableWebSecurity 开启security配置,进入注解源代码中 有注释说明如何使用
 */
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * 1.配置Bcrypt加密对象,用于对用户密码进行加密
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    /*
        2.在内存中定义一些用户信息 UserDetailsService(用户名,密码(密码需要进行加密),角色权限)
        security框架中角色跟权限 写法一样  要求角色ROLE_
     */
    @Bean
    public UserDetailsService userDetailsService() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("jack").password(passwordEncoder().encode("jack")).authorities("p1", "p", "ROLE_ADMIN").build());
        manager.createUser(User.withUsername("rose").password(passwordEncoder().encode("rose")).authorities("p2", "p", "ROLE_USER").build());
        return manager;
    }

    /**
     * 3.配置资源的访问规则(资源有什么权限才能访问资源)
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()   //开启表单认证
                .and()
                //本质上 拦截到请求后 判断当前用户是否有某个权限-过滤器
                .authorizeRequests()
                .antMatchers("/hello").access("hasAuthority('p') and hasRole('ROLE_ADMIN')") // /hello接口地址 必须有p权限才能访问
                .antMatchers("/r/r1").access("hasAuthority('p1')")
                .antMatchers("/r/r2").access("hasAuthority('p2')")
                .anyRequest().authenticated();  //剩余的其他请求 认证后 方可访问
    }
}

在**userDetailsService()**方法中,我们返回了一个UserDetailsService给spring容器,Spring Security会使用它来

获取用户信息。我们暂时使用InMemoryUserDetailsManager实现类,并在其中分别创建了jack、rose两个用

户,并设置密码和权限。

而在configure(HttpSecurity http)方法中,我们通过HttpSecurity设置了安全拦截规则,其中包含了以下内容:

  1. url匹配/r/r1的资源,经过认证后并且需要有“p1”才能访问才能访问。
  2. 其他url认证通过后可以访问。
  3. 支持form表单认证,认证成功后转向/index。

Security认证授权原理

结构总览

​ Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。 当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为 org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此 类,下图是Spring Security过虑器链结构图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时 这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的 ,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器 (AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

spring Security功能的实现主要是由一系列过滤器链相互配合完成。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

下面介绍过滤器链中主要的几个过滤器及其作用:

  • SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截 器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给 SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好 的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;

  • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密 码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,这些都可以根据需求做相关改变;

  • FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;

  • ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常: AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。

认证流程

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

让我们仔细分析认证过程:

  1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。
  2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证
  3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息。身份信息,细节信息,但密码通常会被移除) Authentication 实例。
  4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过 SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它 的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个 List 列表,存放多种认证方式,最终实际的认证工作是由 AuthenticationProvider完成的。咱们知道web表单的对应的AuthenticationProvider实现类为 DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终 AuthenticationProvider将UserDetails填充至Authentication。

认证核心组件的大体关系如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

授权流程

​ Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。Spring Security使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

Spring Security的授权流程如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

分析授权流程:

  1. 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子类拦截。

  2. 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类 DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限 Collection 。

    http..authorizeRequests()
                    .antMatchers("/r/r1").hasAuthority("p1")
    
  3. 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资 源,否则将禁止访问。 AccessDecisionManager(访问决策管理器)的核心接口如下:

    public interface AccessDecisionManager { 
        /** * 通过传递的参数来决定用户是否有访问对应受保护资源的权限 */
        void decide(Authentication authentication , Object object, Collection<ConfigAttribute> configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException; //略.. 
    }
    
  4. 这里着重说明一下decide的参数: authentication:要访问资源的访问者的身份 object:要访问的受保护资源,web请求对应FilterInvocation confifigAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。 decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

  5. AccessDecisionManager 采用投票的方式来确定是否能够访问受保护资源。AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

    public interface AccessDecisionVoter<S> { 
        int ACCESS_GRANTED = 1; 
        int ACCESS_ABSTAIN = 0; 
        int ACCESS_DENIED =1; 
        boolean supports(ConfigAttribute var1); 
        boolean supports(Class<?> var1); 
        int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3); 
    }
    

    vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意, ACCESS_DENIED表示拒绝,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前 Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。

  6. Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是 AffirmativeBased、ConsensusBasedUnanimousBased。 Spring security默认使用的是AffirmativeBased。

  7. AffirmativeBased的逻辑是:

    1. 只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;
    2. 如果全部弃权也表示通过;
    3. 如果没有一个投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

分布式系统认证

分布式认证需求

​ 分布式系统的每个服务都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式 系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统 内部服务提供认证,对第三方系统也要提供认证。分布式认证的需求总结如下:

统一认证授权

​ 提供独立的认证服务,统一处理认证授权。 无论是不同类型的用户,还是不同种类的客户端(web端,H5、APP),均采用一致的认证、权限、会话机制,实现 统一认证授权。要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别 等认证方式,并可以非常灵活的切换。

应用接入认证

​ 应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分API给接入第三方使用,一方应用(内部 系统服 务)和三方应用(第三方应用)均采用统一机制接入。

分布式认证方案

​ 基于token的认证方式,服务端不用存储认证数据,易维护扩展性强, 客户端可以把token 存在任意地方,并且可 以实现web和app统一认证机制。其缺点也很明显,token由于自包含信息,因此一般数据量较大,而且每次请求 都需要传递,因此比较占带宽。另外,token的签名验签操作也会给cpu带来额外的处理负担。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基于token的认证方式,它的优点是:

  • 适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。
  • token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0、JWT等。
  • 一般情况服务端无需存储会话信息,减轻了服务端的压力。

分布式系统认证技术方案见下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

流程描述:

(1)用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。

(2)认证服务(UAA)调用系统微服务验证该用户的身份是否合法,并获取用户权限信息。

(3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法。

(4)若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权

限。

(5)后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。

(6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。

(7)如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。

(8)微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事:

​ 1,用户授权拦截(看当前用户是否有权访问该资源)

​ 2,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时 获取当前用户信息)

SpringSecurityOauth简介

OAuth2.0介绍

SpringSecurityOauth内部认证 授权 底层调用是Security安全框架

​ OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们(用户)存储在另外的服务器上用户的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth1.0协议的延续版本,但不向后兼容OAuth 1.0,即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服 务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。 Oauth协议目前发展到2.0版本,1.0版本过于复杂,2.0版本已得到广泛应用。

参考:https://baike.baidu.com/item/oAuth/7153134?fr=aladdin

Oauth协议:https://tools.ietf.org/html/rfc6749

下边分析一个Oauth2认证的例子,通过例子去理解OAuth2.0协议的认证流程,本例子是黑马程序员网站使用微信 认证的过程,这个过程的简要描述如下:

​ 用户借助微信认证登录黑马程序员网站,用户就不用单独在黑马程序员注册用户,怎么样算认证成功吗?黑马程序 员网站需要成功从微信获取用户的身份信息则认为用户认证成功,那如何从微信获取用户的身份信息?用户信息的 拥有者是用户本人,微信需要经过用户的同意方可为黑马程序员网站生成令牌,黑马程序员网站拿此令牌方可从微 信获取用户的信息。

1、客户端请求第三方授权

用户进入黑马程序的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

点击“微信”出现一个二维码,此时用户扫描二维码,开始给黑马程序员授权。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2、资源拥有者同意给客户端授权

资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证, 验证通过后,微 信会询问用户是否给授权黑马程序员访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会 颁发一个授权码,并重定向到黑马程序员的网站。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、客户端获取到授权码,请求认证服务器申请令牌

此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码。

4、认证服务器向客户端响应令牌

微信认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。

此交互过程用户看不到,当客户端拿到令牌后,用户在黑马程序员看到已经登录成功。

5、客户端请求资源服务器的资源

客户端携带令牌访问资源服务器的资源。

黑马程序员网站携带令牌请求访问微信服务器获取用户的基本信息。

6、资源服务器返回受保护资源

资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。

以上认证授权的业务时序图如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过上边的例子我们大概了解了OAauth2.0的认证过程,下边我们看OAuth2.0认证流程:

引自OAauth2.0协议rfc6749 https://tools.ietf.org/html/rfc6749

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

OAauth2.0包括以下角色:

1、客户端

本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏

览器端)、微信客户端等。

2、资源拥有者

通常为用户,也可以是应用程序,即该资源的拥有者。

3、授权服务器(也称认证服务器)

用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌

(access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。

4、资源服务器

存储资源的服务器,本例子为微信存储的用户信息。

现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会

给准入的接入方一个身份,用于接入时的凭据:

client_id:客户端标识 client_secret:客户端秘钥

因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者客户端

Spring Security Oauth介绍

​ Spring-Security-OAuth是对OAuth2的一种实现,并且跟我们之前学习的Spring Security相辅相成,与Spring

Cloud体系的集成也非常便利,接下来,我们需要对它进行学习,最终使用它来实现我们设计的分布式认证授权解

决方案。

​ OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource

Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用

同一个授权服务的多个资源服务。

授权服务 (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌

的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:

AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。

TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。

资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴

权等,下面的过滤器用于实现 OAuth 2.0 资源服务:

OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

认证流程如下:

1、客户端请求Authorization Server授权服务进行认证。

2、认证通过后由Authorization Server颁发令牌。

3、客户端携带令牌Token请求资源服务。

4、资源服务校验令牌的合法性,合法即返回资源信息。

认证服务器开发

创建认证微服务

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

引入相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>nineclock</artifactId>
        <groupId>com.itheima</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>nc_auth</artifactId>

    <dependencies>
         <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</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-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-jwt</artifactId>
            <version>1.0.10.RELEASE</version>
        </dependency>
    </dependencies>


    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

提供启动类

package com.itheima;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

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

配置文件

server:
  port: 8082
spring:
  application:
    name: auth-service
  main:
    allow-bean-definition-overriding: true
  cloud:
    consul:
      port: 8500 #指定注册中心端口,默认为8500
      host: localhost #指定注册中心IP地址
      discovery:
        service-name: auth-service #注册到consul注册中心的微服务名称
      locator:
        lower-case-service-id: true
        enabled: true
      register: true
      prefer-ip-address: true #这个必须配
      tags: version=1.2
      instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}
      healthCheckInterval: 15s
      health-check-url: http://${spring.cloud.client.ip-address}:${server.port}/actuator/health
logging:
  level:
    com.itheima: debug

网关中配置认证微服务路由规则

打开nine_clock_gataway微服务配置文件增加以下内容:

spring:
  cloud:
    gateway:
      routes:
        # 认证微服务
        - id: auth-service #微服务名称
          uri: lb://auth-service #即auth-service服务的负载均衡地址
          predicates:     #predicates用于匹配HTTP请求的不同属性
            - Path=/auth/** #匹配到的URL地址
          filters:
            - StripPrefix=1 #在转发之前将/auth 去掉

授权服务器配置

Oauth2根据不同的场景提供了不同的四种认证模式:详细见:https://www.jianshu.com/p/84a4b4a1e833

  • 授权码模式(authorization code)-常见场景第三方登录场景,提供第三方应用访问资源(OSS)
  • 简化模式(implicit)
  • 密码模式(resource owner password credentials)-会给平台信任的程序提供认证(APP,PC端提供认证)
  • 客户端模式(client credentials)

EnableAuthorizationServer

​ 新建配置类:AuthorizationServer 。可以用 @EnableAuthorizationServer(注解来启动OAuth2.0授权服务机制) 注解并继承AuthorizationServerConfifigurerAdapter来配置OAuth2.0 授权服务器:

@Configuration 
@EnableAuthorizationServer 	
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    //略... 
}

AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,它们 会被Spring传入AuthorizationServerConfigurer中进行配置。

  • ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息

  • AuthorizationServerEndpointsConfigurer: 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)

  • AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.

配置客户端详情服务

ClientDetailsServiceConfigurer 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService), ClientDetailsService负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表:

  • clientId:(必须的)用来标识客户的Id(理解为第三方应用账户)
  • secret:(需要值得信任的客户端)客户端安全码,如果有的话。
  • scope:用来限制客户端的访问范围,可选值(read,write,all)如果为空(默认)的话,那么客户端拥有全部的访问范围。
  • authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。 可选值( authorization_codepassword,implicit,client_credentials,refresh_token
  • authorities:此客户端可以使用的权限(基于Spring Security authorities)。
  • autoApproveScopes: 设置是否自动授权
  • redirectUris 授权码模式中重定向地址

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户 端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。 我们暂时使用内存方式存储客户端详情信息,配置如下:

package com.itheima.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 注册bcrypt加密对象
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Bean
    public UserDetailsService userDetailsService(){
        //在内存中提供自定义的用户名 密码
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        //在内存中自定义用户名称:jack 密码:jack 权限:p1
        manager.createUser(User.withUsername("jack").password(passwordEncoder().encode("jack")).authorities("p1").build());
        manager.createUser(User.withUsername("rose").password(passwordEncoder().encode("rose")).authorities("p2").build());
        return manager;
    }

    /**
     * 配置客户端详情:移动端、web端APPID及秘钥
     * @param clients
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //客户端信息暂存储在内存中,也可改为security提供的表存储
        //授权码模式下需要的客户端信息如下
        clients.inMemory()
                .withClient("third_client")  //客户端标识
                .secret(passwordEncoder().encode("third_secret"))  //客户端的秘钥
                .authorizedGrantTypes("authorization_code")
                .scopes("all")
                .redirectUris("http://www.baidu.com")  //设置回调地址
            //密码模式需要的客户端信息
            .and()
                .withClient("app_client")  //客户端标识
                .secret(passwordEncoder().encode("app_secret"))  //客户端的秘钥
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("all") //设置回调地址
            .and()
                .withClient("pc_client")
                .secret(passwordEncoder().encode("pc_secret"))
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("all");
    }
}

安全配置类

package com.itheima.auth.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;

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

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

授权码模式测试

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

1、初次访问认证服务器任何请求,都会被拦截到,重定向到security默认提供的登录页面:http://localhost:8082/login

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2、使用配置类中内存中定义的用户名密码进行登录

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

3、登录成功后,地址栏会跳转到上次访问项目的url

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

4、资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:浏览器使用GET方式提交:

http://localhost:8082/oauth/authorize?client_id=third_client&response_type=code&scope=all&redirect_uri=http://www.baidu.com

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

5、点击允许访问,查看是否会调转到指定的回调地址,以及地址中是否携带授权码

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

6、打开postMan工具进行测试,使用返回的授权码进行认证

注意:这里需要设置认证头信息,授权码模式是采用 basic auth 授权方式验证客户端请求,Authorization 请求头对应的值是 (basic base64编码) 忽略括号,其中 base64编码是将 用户名:密码 这种格式进行处理生成的,postman 里面可以设置授权信息。帮助我们生成 base64编码,并且自动在 header 中添加 Authorization。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

采用POST方式提交:
URL:http://localhost:8082/oauth/token
参数名称 参数值 说明
grant_type authorization_code 授权码模式
redirect_uri http://www.baidu.com client中重定向地址
code xxxxx 重定向地址后授权码参数

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

**注意:**后期如果有第三方网站集成九点钟项目实现第三方登录,只需要在其他应用中发起请求获取授权码code,进而获取到访问令牌access_token,然后携带访问令牌获取受保护资源(例如:获取用户基本信息接口),这三个请求在浏览器内部发起,用户是感知不到。

密码模式

​ 密码模式中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端(APP),但是客户端不得储存密码,存的是认证后返回access_token。应用客户端一般都是受信任的(都是由一家公司开发的)

安全配置类

密码模式下需要在spring容器中配置认证管理器AuthenticationManager对象

package com.itheima.auth.config;

import org.springframework.beans.factory.annotation.Autowired;
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.password.PasswordEncoder;

@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    /**
     * 用户认证时需要的认证管理和用户信息来源
     * @return
     * @throws Exception
     */
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/actuator/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin();
    }
}
令牌访问端点配置

AuthorizationServerEndpointsConfigurer这个对象的实例可以完成令牌服务以及令牌endpoint配置

令牌管理

​ AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来加载身份信息,里面包含了这个令牌的相关权限。 自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类, 里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了 所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了TokenStore接口:

  • InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

定义TokenConfig,官方提供的TokenStore实现类有以下:

  • InMemoryTokenStore将OAuth2AccessToken保存在内存(默认)
  • JdbcTokenStore:将OAuth2AccessToken保存在数据库
  • JwkTokenStore:将OAuth2AccessToken保存到JSON Web Key
  • JwtTokenStore:将OAuth2AccessToken保存到JSON Web Token
  • RedisTokenStore:将OAuth2AccessToken保存到Redis
    package com.itheima.auth.config;

    import jdk.nashorn.internal.parser.Token;
    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;

    @Configuration
    public class TokenConfig {

        /**
         * 使用InMemoryTokenStore,内存中生成一个普通的令牌。
         * @return
         */
        @Bean
        public TokenStore tokenStore(){
            return new InMemoryTokenStore();
        }
    }

在AuthorizationServer中定义AuthorizationServerTokenServices :

@Autowired
private TokenStore tokenStore;


@Bean
public AuthorizationServerTokenServices tokenService() {
    DefaultTokenServices service = new DefaultTokenServices();
    service.setSupportRefreshToken(true);
    service.setTokenStore(tokenStore);
    service.setAccessTokenValiditySeconds(7200); // 令牌有效期2小时 默认12小时
    service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天 默认30天
    return service;
}

TokenService核心属性解析:

属性字段 作用
refreshTokenValiditySeconds refresh_token 的有效时长 (秒), 默认 30 天
accessTokenValiditySeconds access_token 的有效时长 (秒), 默认 12 小时
supportRefreshToken 是否支持 refresh token, 默认为 false
reuseRefreshToken 是否复用 refresh_token, 默认为 true (如果为 false, 每次请求刷新都会删除旧的 refresh_token, 创建新的 refresh_token)
tokenStore token 储存器 (持久化容器)
clientDetailsService 提供 client 详情的服务 (clientDetails 可持久化到数据库中或直接放在内存里)
accessTokenEnhancer token 增强器, 可以通过实现 TokenEnhancer 以存放 additional information
authenticationManager Authentication 管理器, 起到填充完整 Authentication的作用
令牌访问端点配置

AuthorizationServerEndpointsConfifigurer这个配置对象有一个叫做pathMapping()方法用来配置端点URL链接地址,包含两个参数:

  • 参数一 String类型 端点URL 默认链接
  • 参数二String类型 要进行替换的URL链接

以上参数都将以"/"字符串开头,框架的默认URL链接如下,可以作为pathMapping方法的第一个参数:

  • /oauth/authorize:授权端点
  • /oauth/token:令牌端点
  • /oauth/confirm_access:用户确认授权提交端点
  • /oauth/error:授权服务错误信息端点
  • /oauth/check_token: 用户资源服务器访问令牌解析端点
  • /oauth/token_key:提供公钥端点,如果采用JWT令牌
@Autowired
private AuthenticationManager authenticationManager;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
    endpoints.authenticationManager(authenticationManager)
            .tokenServices(tokenService());
}

令牌端点的安全约束

/**
 * **令牌端点的安全约束**
 * @param security
 */
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
    security.allowFormAuthenticationForClients();  //支持密码模式下表单登录
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

(1)资源拥有者将用户名、密码发送给客户端

(2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token)

采用POST方式提交:
http://localhost:8082/oauth/token
参数名称 参数值 说明
client_id app_client 客户端ID
client_secret app_secret 客户端秘钥
grant_type password 授权模式
username jack 用户名
password jack 密码

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

配置JWT令牌服务

在nc_auth中配置jwt令牌服务,即可实现生成jwt格式的令牌。

1、TokenConfig

package com.itheima.auth.config;

import jdk.nashorn.internal.runtime.FindProperty;
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;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * 令牌存储配置
 */
@Configuration
public class TokenStoreConfig {

    //对称加密采用秘钥
    private static final String secret = "itcast_auth";


    /**
     * 配置JWT产生方式,以及秘钥
     * 创建JWT令牌 保证令牌安全性,不可伪造:产生令牌加密方式 1.对称加密 2.非对称加密
     * 采用对称加密生成 JWT令牌
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter(){
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //配置对称加密原始秘钥 该秘钥值 不能泄露
        //jwtAccessTokenConverter.setKeyPair();//非对称加密
        jwtAccessTokenConverter.setSigningKey(secret);
        return jwtAccessTokenConverter;
    }



    /**
     * 令牌存储策略
     * @return
     */
    @Bean
    public TokenStore tokenStore(){
        //暂时将服务器端生成token存入内存中-默认机制 UUID令牌 没有可读性,没有含义-服务器端是不是依然存储信息(有状态认证)
        //return new InMemoryTokenStore();
        //后续我们将令牌 改为JWT 服务器端不再存储令牌 而是交给客户端存储
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

}

2、定义JWT令牌服务

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;


/**
 * 配置令牌服务相关信息
 */
@Bean
public AuthorizationServerTokenServices tokenServices() {
    DefaultTokenServices tokenServices = new DefaultTokenServices();
    //指定令牌产生加强对象(改为jwt令牌)
    tokenServices.setTokenEnhancer(jwtAccessTokenConverter);
    //给令牌进行定制
    //令牌如何在服务端存储-默认在内存中存储
    tokenServices.setTokenStore(tokenStore);
    //令牌access_token有效期
    tokenServices.setAccessTokenValiditySeconds(604800);
    //刷新令牌有“效期
    tokenServices.setSupportRefreshToken(true);
    tokenServices.setRefreshTokenValiditySeconds(777600);
    return tokenServices;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

资源服务器开发

​ 需求:我们访问系统微服务提供的查询用户列表(资源)restApi需要当前用户必须有指定的权限或者角色才能访问。比如拥有"ROLE_ADMIN"的角色才能访问。

添加依赖

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

资源服务器配置

tokenStore配置

​ 前面也介绍了,资源服务器由于需要验证并解析令牌,往往可以通过在授权服务器暴露check_token的Endpoint来完成,而我们在授权服务器使用的是对称加密的jwt,因此知道密钥即可,资源服务与授权服务本就是对称设计,那我们把授权服务的TokenConfig类拷贝过来即可 。

package com.itheima.auth.config;

import jdk.nashorn.internal.parser.Token;
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;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "itcast_auth";

    /**
     * 使用InMemoryTokenStore,生成一个普通的令牌。
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        //JWT令牌需要修改为JwtTokenStore
        return new JwtTokenStore(accessTokenConverter());
    }

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

资源服务器配置

  • 要访问资源服务器受保护的资源需要携带令牌(从授权服务器认证后获得)
  • 客户端往往同时也是一个资源服务器,各个服务之间的通信(访问需要权限的资源)时需携带访问令牌
  • 资源服务器通过 @EnableResourceServer 注解来开启一个 OAuth2AuthenticationProcessingFilter 类型的过滤器
  • 通过继承 ResourceServerConfigurerAdapter 类来配置资源服务器
package com.itheima.sys.config;

import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSecurity;
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.TokenStore;

import javax.servlet.http.HttpServletResponse;

@Configuration
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  //用户对某个控制层的方法是否具有访问权限
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().exceptionHandling()
                .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
                .and()
                    .authorizeRequests()
                    .antMatchers("/test").permitAll()
                    .antMatchers("/actuator/**").permitAll()  //匿名访问
             		   //控制权限方式一:采用过滤器 拦截请求

                     //配置其他的rest接口 安全策略
                    //.antMatchers("/test/user").hasAuthority("p1") //访问保存用户接口必须有p1权限
                .anyRequest().authenticated()  //剩余其他的请求 需要认真后方可一访问
                .and()
                .httpBasic();
    }
}

控制层资源设置访问权限

找到之前编写好的TestController在方法上使用security提供的相关注解,来控制访问权限;

采用注解方式校验权限:必须在资源服务器配置类上加注解:

@Configuration
@EnableResourceServer //开启资源服务器配置
@EnableGlobalMethodSecurity(prePostEnabled = true) //调用方法之前,对当前访问方法权限进行判断
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@PreAuthorize:用来在方法调用前检查权限
@PostAuthorize:用来在方法调用后检查权限
/**
 * 测试  查询用户所有的数据
 * @return
 */
@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('p1')")//如果要访问该资源 当前登录用户必须有ROLE_ADMIN角色 或 p1 权限才能访问
@PostMapping("/test/user")
public Result saveUser(@RequestBody UserDomain user){
    //如果年龄为空,则抛出异常,返回400状态码,返回错误提示消息
    if (user.getAge() == null) {
        //throw new RuntimeException("用户年龄为必填项!");
        throw new NcException(ResponseEnum.ERROR);
    }
    //return new Result(true, 200, "保存成功", userDominService.saveUser(user));
    return Result.success("保存成功!", userDominService.saveUser(user));
}

按照之前的方式,在Postman中直接访问接口进行测试,发现返回以下结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

通过结果发现资源服务器提供的接口已经受保护,无法直接匿名进行访问。如果要访问受保护的接口,需要在请求头中增加一个授权头Authorization,值为Bearer token值,可以复制用户jack认证后,认证服务器签发的token值。再次进行请求:

               //配置其他的rest接口 安全策略
                //.antMatchers("/test/user").hasAuthority("p1") //访问保存用户接口必须有p1权限
            .anyRequest().authenticated()  //剩余其他的请求 需要认真后方可一访问
            .and()
            .httpBasic();
}

}


### 控制层资源设置访问权限

找到之前编写好的TestController在方法上使用security提供的相关注解,来控制访问权限;

采用注解方式校验权限:必须在资源服务器配置类上加注解:

```java
@Configuration
@EnableResourceServer //开启资源服务器配置
@EnableGlobalMethodSecurity(prePostEnabled = true) //调用方法之前,对当前访问方法权限进行判断
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@PreAuthorize:用来在方法调用前检查权限
@PostAuthorize:用来在方法调用后检查权限
/**
 * 测试  查询用户所有的数据
 * @return
 */
@PreAuthorize("hasAuthority('ROLE_ADMIN') or hasAuthority('p1')")//如果要访问该资源 当前登录用户必须有ROLE_ADMIN角色 或 p1 权限才能访问
@PostMapping("/test/user")
public Result saveUser(@RequestBody UserDomain user){
    //如果年龄为空,则抛出异常,返回400状态码,返回错误提示消息
    if (user.getAge() == null) {
        //throw new RuntimeException("用户年龄为必填项!");
        throw new NcException(ResponseEnum.ERROR);
    }
    //return new Result(true, 200, "保存成功", userDominService.saveUser(user));
    return Result.success("保存成功!", userDominService.saveUser(user));
}

按照之前的方式,在Postman中直接访问接口进行测试,发现返回以下结果:

[外链图片转存中…(img-9rRgJruE-1779775476017)]

通过结果发现资源服务器提供的接口已经受保护,无法直接匿名进行访问。如果要访问受保护的接口,需要在请求头中增加一个授权头Authorization,值为Bearer token值,可以复制用户jack认证后,认证服务器签发的token值。再次进行请求:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐