17.Oauth2-微服务认证

news2024/11/27 8:38:31

1.Oauth2

OAuth 2.0授权框架支持第三方支持访问有限的HTTP服务,通过在资源所有者和HTTP服务之间进行一个批准交互来代表资源者去访问这些资源,或者通过允许第三方应用程序以自己的名义获取访问权限。

image-20220506121118022

为了方便理解,可以想象OAuth2.0就是在用户资源和第三方应用之间的一个中间层,它把资源和第三方应用隔开,使得第三方应用无法直接访问资源,从而起到保护资源的作用。

为了访问这种受保护的资源,第三方应用(客户端)在访问的时候需要提供凭证。即,需要告诉OAuth2.0你是谁你要做什么。

用户可以将用户名和密码告诉第三方应用,让第三方应用直接以你的名义去访问,也可以授权第三方应用去访问。

例如,微信公众平台开发,在微信公众平台开发过程中当我们访问某个页面,页面可能弹出一个提示框应用需要获取我们的个人信息问是否允许,点确认其实就是授权第三方应用获取我们在微信公众平台的个人信息,这里微信网页授权就是使用的OAuth2.0。

  • 第三方应用程序(Third-party application): 又称之为客户端(client),我们自己开发的各种客户端,对我们自己的项目来说,QQ、微信、支付宝等是第三方应用程序。

  • HTTP 服务提供商(HTTP service): 我们开发的项目以及 QQ、微信、支付宝、钉钉等都可以称之为“服务提供商”。

  • 资源所有者(Resource Owner): 又称之为用户(user),拥有账号密码的人。

  • 用户代理(User Agent): 用来访问资源,比如浏览器,代替用户去访问这些资源。

  • 认证服务器(Authorization server): 即服务提供商专门用来处理认证的服务器,主要就是实现登录、授权功能。

  • 资源服务器(Resource server): 即服务提供商存放用户生成的资源的服务器,比如电商中的商品模块、订单模块等,是用来处理具体业务的服务器。

        OAuth2.0协议流程描述了四种角色之间的交互过程,如下图所示。

image-20220506121307806

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

令牌(token)与密码(password)的作用是一样的,都可以进入系统,但是有三点差异。

  • 令牌是短期的,到期会自动失效,用户自己无法修改。密码一般长期有效,用户不修改,就不会发生变化。

  • 令牌可以被数据所有者撤销,会立即失效。

  • 令牌有权限范围(scope),对于网络服务来说,只读令牌就比读写令牌更安全。密码一般是完整权限。

上面这些设计,保证了令牌既可以让第三方应用获得权限,同时又随时可控,不会危及系统安全。这就是 OAuth 2.0 的优点。

注意,只要知道了令牌,就能进入系统。系统一般不会再次确认身份,所以令牌必须保密,泄漏令牌与泄漏密码的后果是一样的。 这也是为什么令牌的有效期,一般都设置得很短的原因。

1.1 开放平台

        开放平台(Open Platform)在软件行业和网络中,开放平台是指软件系统通过公开其应用程序编程接口(API)或函数(function)来使外部的程序可以增加该软件系统的功能或使用该软件系统的资源,而不需要更改该软件系统的源代码。

        在互联网时代,把网站的服务封装成一系列计算机易识别的数据接口开放出去,供第三方开发者使用,这种行为就叫做Open API,提供开放API的平台本身就被称为开放平台。

        第一种是技术性的开放,例如百度、腾讯、阿里巴巴等,例如阿里可以提供标准化的应用软件,但是数百万形形色色的卖家对于个性化要求的软件,并不是一个公司的力量可以满足的,所以就把这些需求开放给众多的第三方开发者的方式。再例如google的基于Linux平台的开源手机操作系统就被认为会很快打败Nokia塞班系统。这一种技术性开放平台虽然目前来看跟B2C企业的开放平台关系不大,但是也能从一定程度上说明开放平台是互联网企业的趋势。

        第二种开放平台是指软件系统通过公开其应用程序编程接口(API)或函数(function)来使外部的程序可以增加该软件系统的功能或使用该软件系统的资源,而不需要更改该软件系统的源代码。B2C企业开放平台又包含两种形式,A:淘宝商城、日本乐天这种纯平台的模式,即自己不碰商品的进销存,全部由入驻商家来做;B:美国亚马逊、当当网、京东商城这种“自营+联营”的模式。

1.2 开放平台交互模型

三个角色:

  • 资源拥有者:用户

  • 客户端:各种app、浏览器

  • 服务提供方:包含两个角色

    认证服务器

    资源服务器

1.2.1 认证服务器

认证服务器负责对用户进行认证,并授权给客户端权限。一般的认证都是通过对账号密码进行验证实现,而难点在于怎么进行授权。比如我们使用第三方登录 "哔哩哔哩",可以看到如使用 QQ 登录的授权页面上有 "哔哩哔哩将获取以下权限" 的字样以及权限信息

image-20220506121510936

image-20220506121520612

认证服务器需要知道请求授权的客户端的身份以及该客户端请求的权限。常见的做法是为每一个客户端预先分配一个 id,并给每个 id 对应一个名称以及权限信息。这些信息可以写在认证服务器上的配置文件里,今后客户端每次打开授权页面的时候,客户端需要将该id发送到认证服务器,0Auth2.0就可以用来自动给客户端分配id,同时完成配置文件的自动更新。

1.3 OAuth2 开放平台

开放平台是由 OAuth2.0 协议发展而来的一个产品,它的作用是让客户端自己去这上面进行注册、申请,通过之后系统自动分配 客户端id ,并完成配置的自动更新。

客户端要完成申请,通常需要申请人填写客户端程序的类型(Web、App、微信小程序、支付宝小程序等等)、企业信息、营业执照、法人信息以及想要获取权限等信息,申请需要得到得到服务提供上的审核通过之后,开发平台才会自动分配一个客户端id给客户端。

在通过审核之后,第三方应用在进行认证时,就会想需要获取到的权限信息展示到页面上,例如哔哩哔哩获取QQ权限。授权成功之后认证服务器需要把产生的 access_token 发送给客户端,客户端才能访问具体的资源(头像、性别之类的),大致过程如下:

  • 让客户端在开放平台提交申请时候,填写一个 网址,例如:www.baidu.com,此网址主要用来获取认证码。

  • 当有用户授权成功之后,认证服务器将页面重定向到这个网址,并将生成的 access_token拼接到该网址后面,例如:www.baidu.com?access_token=123 

  • 客户端接收到access_token,之后客户端就可以拿着这个token去获取需要的数据了

1.3.1 令牌

传统项目向服务端请求数据,服务端需要频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,这样效率非常低下,怎么提高效率呢?Token便应运而生。

Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

1.3.2 Access Token

Access Token 是客户端访问资源服务器的令牌。拥有这个令牌代表着得到用户的授权,即具备了访问资源的权限。同时这个授权应该是临时的,只能在一定期限内使用。主要原因是因为Access Token 在使用的过程中很有可能会泄露,被不法分子利用获取我们的数据。所以Access Token应该只能在某个期限内使用,这样可以降低因 Access Token 泄露而带来的风险。

1.4 认证模式

OAuth2.0中定义了四种授权模式:

  • authorization code 授权码模式

  • implicit 简化模式

  • resource owner password credentials 密码模式

  • client credentials 客户端模式

常见模式:授权码、密码模式

1.4.1 授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式,code保证了token的安全性,即使code被拦截,由于没有secret,也是无法通过code获得token的。

角色行为与功能
  • 资源所有者

    只需要允许或拒绝第三方应用获得授权

  • 第三方应用

    申请成为资源服务器的第三方应用

    获取资源服务器提供的资源

  • 授权服务器

    提供授权许可code、令牌token等

  • 资源服务器

    提供给第三方应用开放资源的接口

image-20230423093957585

时序图

image-20230423094544416

环境搭建

创建父项目

image-20220506141039544

image-20220506141533483

指定打包方式为pom

<?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.woniuxy</groupId>
    <artifactId>oauth2</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
</project>

创建auth-server认证服务器模块

image-20220506141646384

image-20220506141740947

导入依赖

image-20220506141849136

导入依赖版本如下

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    <spring-cloud-alibaba.version>2.2.2.RELEASE</spring-cloud-alibaba.version>
</properties>

oauth2依赖

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

创建用户信息配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter{
	
	//密码编码器
	@Bean
	public BCryptPasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}
	
    // 基于内存的用户信息
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication()	//内存认证
			.withUser("zhangsan")		//用户名
			.password(passwordEncoder().encode("123"))	//密码
			.authorities("ROLE_ADMIN");	//角色
	}
}

创建客户端配置类,配置客户端信息

import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

import javax.annotation.Resource;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter{
	
	@Resource
	private BCryptPasswordEncoder passwordEncoder;
	
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		//配置客户端
		clients
			.inMemory()		//内存方式
			.withClient("client")	//客户端名字
			.secret(passwordEncoder.encode("secret"))	//客户端秘钥
			.authorizedGrantTypes("authorization_code")//授权类型
			.scopes("all")	//授权范围
			.redirectUris("http://www.baidu.com");	//回调网址,携带授权码
	}
}

在application.yml文件中配置以下信息

server:
  port: 8000
spring:
  application:
    name: oauth

启动项目进行登录

localhost:8000/login

进入登录页面,输入账号:zhangsan,密码:123进行登录

image-20220506143057117

登录成功之后向服务器发送请求获取授权码,在地址栏上输入以下内容回车

http://localhost:8080/oauth/authorize?client_id=client&response_type=code

可以看到一个授权页面,询问用户是否进行授权

image-20220506143330317

授权成功之后会重定向到AuthorizationServerConfiguration配置类中指定的地址,并以参数的方式携带授权码

通过postman发送请求向服务器获取token

地址栏填写:http://client:secret@localhost:8000/oauth/token

填写客户端账号密码

image-20230831110955586

填写授权类型、授权码,发送请求

image-20220506145021696

成功之后在postman上可以看到以下信息

image-20220506145131136

表示成功

注意:每个授权码只能使用一次

1.4.2 密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

修改AuthorizationServerConfiguration配置类,添加密码模式

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //配置客户端
    clients
        .inMemory()		
        .withClient("client")
        .secret(passwordEncoder.encode("secret"))
        .authorizedGrantTypes("authorization_code","password") //添加密码授权模式
        .scopes("all")	//授权范围
        .redirectUris("http://www.woniuxy.com");
}

在postman中新开一个请求,地址栏中填写:http://localhost:8080/oauth/token

密码授权模式要求以请求头的方式提交客户端账号密码,并且需要对账号密码进行base64加密,因此选择Authorization选项卡,设置TYPE为"Basic Auth",并填写客户端账号密码

image-20220506151829604

在请求体中设置授权类型、用户账号密码参数

image-20220506151940710

发送请求测试

image-20220506152019225

可以发现此时并不支持密码模式,即使在AuthorizationServerConfiguration配置类中指定了密码模式。

原因是此时代码中缺少对密码模式的支持,在oauth2中需要添加AuthenticationManager对象对密码模式进行支持。

在WebSecurityConfiguration配置类中配置 AuthenticationManager

// 配置 AuthenticationManager(密码模式需要该对象进行账号密码校验)
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

在AuthorizationServerConfiguration类中注入AuthenticationManager,并重写以下方法

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

//配置使用的 AuthenticationManager 实现用户认证的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints.authenticationManager(authenticationManager);
}

重启项目再次发送请求获取token

image-20220506152551071

整合JWT

导入了oauth2依赖就自动导入的JWT相关依赖,因此不用单独导入JWT,只需要进行设置就行

创建TokenConfiguration配置类

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

@Configuration
public class TokenConfiguration {
    // 密码
    private static String SIGNING_KEY="www.woniuxy.com";

    // token转换器
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = 
            new JwtAccessTokenConverter();
        jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
        return jwtAccessTokenConverter;
    }

    // 令牌存储策略:jwt方式
    @Bean
    public TokenStore tokenStore(){
        return new JwtTokenStore(accessTokenConverter());
    }
}

在AuthorizationServerConfiguration配置类中注入相关对象

@Resource
private TokenStore tokenStore;

@Resource
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Resource
private ClientDetailsService clientDetailsService;

在AuthorizationServerConfiguration配置类中编写token服务方法,该方法主要用来设置

private AuthorizationServerTokenServices tokenServices(){
    // 创建服务对象
    DefaultTokenServices services = new DefaultTokenServices();
    // 设置客户端详情服务
    services.setClientDetailsService(clientDetailsService);
    // 支持刷新令牌
    services.setSupportRefreshToken(true);
    // 不重复使用refreshtoken,每次刷新之后只能用新的refreshtoken才能继续刷新
	services.setReuseRefreshToken(false);
    // 设置令牌存储策略
    services.setTokenStore(tokenStore);

    // 设置令牌增强
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter));
    services.setTokenEnhancer(tokenEnhancerChain);

    // 设置令牌过期时间
    services.setAccessTokenValiditySeconds(600);
    services.setRefreshTokenValiditySeconds(6000);

    return services;
}

修改configure(AuthorizationServerEndpointsConfigurer endpoints)方法,添加token服务

//配置使用的 AuthenticationManager 实现用户认证的功能
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
        .authenticationManager(authenticationManager) // 认证管理器
        .tokenServices(tokenServices());	// 配置token服务
}

重启项目发送请求获取token

image-20220506160230848

如果想要获取到refreshtoken,可以修改AuthorizationServerConfiguration配置类,添加refresh_token授权方式

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //配置客户端
    clients
        .inMemory()		
        .withClient("client")
        .secret(passwordEncoder.encode("secret"))	
        .authorizedGrantTypes("authorization_code","password","refresh_token") 
        .scopes("all")
        .redirectUris("http://www.woniuxy.com");
}

重启项目测试

image-20220506161053399

image-20220506161135294

整合数据库(user)

建表SQL

create database sc default character set=utf8;

DROP TABLE IF EXISTS `perms`;
CREATE TABLE `perms` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `perms` VALUES (3001,'user:add'),(3002,'user:del'),(3003,'user:find'),(3004,'user:update'),(3005,'goods:add'),(3006,'goods:find'),(3007,'goods:del'),(3008,'goods:update');

DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
  `id` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `role` VALUES (2001,'ROLE_ADMIN'),(2002,'ROLE_USER');


DROP TABLE IF EXISTS `role_perms`;
CREATE TABLE `role_perms` (
  `rid` int(11) DEFAULT NULL,
  `pid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `role_perms` VALUES (2001,3001),(2001,3003),(2001,3004),(2002,3005),(2002,3006),(2002,3007),(2002,3008);

DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) DEFAULT NULL,
  `username` varchar(20) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES (1001,'zhangsan','$2a$10$pINVnd8.cXScFXCxI2x4cem4fOexA2J5TNY/Mx2CjN6mJuYGBNG0m'),(1002,'wangwu','wangwu');

DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
  `uid` int(11) DEFAULT NULL,
  `rid` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user_role` VALUES (1001,2001),(1002,2002),(1003,2002);

auth-server的pom.xml中引入mybatis

<!--mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.4</version>
</dependency>

在application.yml中配置mybatis参数

mybatis:
  type-aliases-package: com.woniuxy.authserver.entity
  mapper-locations: classpath:/mapper/*.xml

创建Perms、Role、User实体类,注意:实体类必须实现序列化接口,不然运行过程中可能会报Failed to find access token for token错误

import lombok.Data;

@Data
public class Perms implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
}
import lombok.Data;
import java.util.List;

@Data
public class Role implements Serializable {
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    private List<Perms> perms;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Slf4j
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails, Serializable {

    private static final long serialVersionUID = 1L;

    private int id;
    private String username;
    private String password;
    private List<Role> roles;

    // 返回当前用户的所有角色、权限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        log.debug("获取用户角色权限信息");
        // 新建集合
        List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
        // 遍历role
        for(Role role : this.roles){
            // 放入角色信息
            grantedAuthorities.add(new SimpleGrantedAuthority(role.getName()));
            // 遍历当前角色的所有权限信息
            for(Perms perms : role.getPerms()){
                grantedAuthorities.add(new SimpleGrantedAuthority(perms.getName()));
            }
        }
        log.debug(grantedAuthorities.toString());
        return grantedAuthorities;
    }

    // 获取用户名
    @Override
    public String getUsername() {
        return this.username;
    }

    // 账号是否过期    true表示未过期   false表示过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 账号是否被锁定  true表示未锁定   false表示锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 凭证是否过期  true表示未过期   false表示过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 用户是否被禁用  true表示未禁用   false表示禁用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

创建UerMapper接口

import com.woniuxy.springsecurity.entity.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    public User findByName(String username);
}

在resources目录下创建mapper文件夹,并在该文件夹下创建Mapper文件

image-20220416173702294

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.woniuxy.authserver.mapper.UserMapper" >
    <select id="findByName" resultMap="user_map">
        select * from user where username = #{username}
    </select>

    <resultMap id="user_map" type="User">
        <id column="id" property="id"></id>
        <result column="username" property="username"></result>
        <result column="password" property="password"></result>

        <collection property="roles" ofType="Role" column="id" select="findRolesByUid"></collection>
    </resultMap>

    <select id="findRolesByUid" resultMap="role_map">
        select r.id,r.name from user_role ur,role r where ur.rid = r.id and ur.uid = #{id}
    </select>
    <resultMap id="role_map" type="Role">
        <id column="id" property="id"></id>
        <result column="name" property="name"></result>

        <collection property="perms" ofType="Perms" column="id" select="findPermsByRid"></collection>
    </resultMap>

    <select id="findPermsByRid" resultType="Perms">
        select p.id,p.name from role_perms rp,perms p where rp.pid = p.id and rp.rid = #{rid}
    </select>
</mapper>

创建CustomUserDetailsServiceImpl类实现UserDetailsService接口

import com.woniuxy.authserver.entity.User;
import com.woniuxy.authserver.mapper.UserMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class CustomUserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //1.查询用户
        User user = userMapper.findByName(username);

        //2.判断
        if (user == null) throw new UsernameNotFoundException("用户不存在");

        //3.返回用户信息
        return user;
    }
}

在配置类WebSecurityConfiguration中注入UserDetailsService对象,并修改configure(AuthenticationManagerBuilder auth)反方指定用户信息从数据库中获取

@Resource
private UserDetailsService userDetailsService;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //auth
        //.inMemoryAuthentication()	//内存认证
        //.withUser("zhangsan")		//用户名
        //.password(passwordEncoder().encode("123"))	//密码
        //.authorities("ROLE_ADMIN");	//角色
    auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

重启auth-server服务,进行认证

封装用户id

在生成token时可以将用户id封装到token中,以便后期使用

修改TokenConfiguration类中的accessTokenConverter()方法,在创建转换器时重写enhance方法

// token转换器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter jwtAccessTokenConverter = 
        new JwtAccessTokenConverter(){
        @Override
        public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
            final Map<String,Object> map = new HashMap<>();
            // 从认证对象中得到用户信息
            User user = (User) authentication.getUserAuthentication().getPrincipal();
            // 将用户id放到token中
            map.put("uid", user.getId());
            ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(map);
            // 返回
            return super.enhance(accessToken, authentication);
        }
    };
    jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
    return jwtAccessTokenConverter;
}

利用postman进行测试

image-20220726160937503

返回的结果中可以看到用户id,token中也包含了用户id

检验token是否过期

在org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint类中定义了校验token的接口/oauth/check_token,该接口可以用来校验token是否合法、是否过期、是否是伪造的

@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {

    OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
    if (token == null) {
        throw new InvalidTokenException("Token was not recognised");
    }

    if (token.isExpired()) {
        throw new InvalidTokenException("Token has expired");
    }

    OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());

    Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);

    // gh-1070
    response.put("active", true);	// Always true if token exists and not expired

    return response;
}

只是该接口oauth2默认情况下是不对外公开的,如果要使用该接口那就必须手动配置开启,在AuthorizationServerConfiguration配置类中重写以下方法

//设置 /oauth/check_token 端点,通过认证后可访问。
//该端点对应 CheckTokenEndpoint类,用于校验访问令牌的有效性。
//在客户端访问资源服务器时,会在请求中带上访问令牌。
//在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    // 默认是denyAll():拒绝所有
    oauthServer.checkTokenAccess("permitAll()");
}

checkTokenAccess常用值有三种:

  • denyAll():拒绝所有请求,不开放该接口

  • isAuthenticated():只对完成认证之后的请求开放

  • permitAll():对所有请求开放

测试:登录成功之后在Postman中发送请求进行测试

接口url:http://localhost:8080/oauth/check_token

image-20220726114103407

返回的结果中包含了用户的用户名、权限等信息,还包括了token是否可用的信息

如果返回以下信息表示token已经过期

image-20230831142542647

而如果返回以下信息表示token非法

image-20230831142606770

通过refresh_token获取新token

获取token和刷新token使用的是同一个接口,所以地址栏url还是

http://local:8080/oauth/token

只是grant_type需要换成refresh_token,然后将之前的refresh token作为参数传递给后台

image-20220507100748397

还是需要将客户端id、密码以base64编码放到请求头中

image-20220507100833359

发送请求得到结果

image-20220507101406277

根据结果可以知道,token和refresh_token都会自动刷新,这样做的好处是当token过期时通过程序调用刷新接口,获取到新的token和refresh_token,实现自动续期。

refresh_token如果过期会得到以下结果

image-20220726115252787

refresh_token过期就需要重新登录

1.5 资源服务器

创建resource子模块,导入相关依赖

image-20220507112247458

设置父子关系

创建OAuth2ResourceServerConfig配置类

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
 
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            // 设置请求,需要认证后访问
            .anyRequest().authenticated();
    }
}

创建controller

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

@RestController
@RequestMapping("/resource")
public class ResourceController {

    @RequestMapping("/info")
    public String info(){

        return "success";
    }
}

配置application.yml

server:
  port: 8001
spring:
  application:
    name: resource
security:
  oauth2:
    # OAuth2 Client 配置,对应 OAuth2ClientProperties 类
    client:
      client-id: client
      client-secret: secret
    # OAuth2 Resource 配置,对应 ResourceServerProperties 类
    resource:
      token-info-uri: http://127.0.0.1:8000/oauth/check_token # 获得 Token 信息的 URL
    # 访问令牌获取 URL,自定义的
    access-token-uri: http://127.0.0.1:8000/oauth/token
management:
  endpoints:
    web:
      exposure:
        include: '*'

启动resource资源服务器

先进行认证,得到token和refresh_token

localhost:8000/oauth/token

image-20220507115312984

然后将得到的token放到请求资源服务器的请求头中

image-20220507115927410

发送请求后可以发现报500错误,查看resource控制台可以发现以下信息

org.springframework.web.client.HttpClientErrorException$Forbidden: 403 : [{"timestamp":"2022-05-07T03:40:14.063+00:00","status":403,"error":"Forbidden","message":"","path":"/oauth/check_token"}]

根据信息提示:没有权限访问 /oauth/check_token,该URL是认证服务器用来校验token是否合法的接口。资源服务器在接收到请求时会获取到token,然后调用认证服务器的/oauth/check_token接口去检验token,但是此时认证服务器还没有开放该端口(默认关闭),所以造成了403无法访问。

到认证服务器的AuthorizationServerConfiguration配置类中开启/oauth/check_token

//设置 /oauth/check_token 端点,通过认证后可访问。
//该端点对应 CheckTokenEndpoint类,用于校验访问令牌的有效性。
//在客户端访问资源服务器时,会在请求中带上访问令牌。
//在资源服务器收到客户端的请求时,会使用请求中的访问令牌,找授权服务器确认该访问令牌的有效性。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
    // 默认是denyAll():拒绝所有
    oauthServer.checkTokenAccess("isAuthenticated()");
}

重启认证服务器

重新进行认证得到token,然后用新的token再访问资源服务器

image-20220507120732391

看到success表明成功

角色权限管理

在资源服务器主启动类上添加@EnableGlobalMethodSecurity注解,开启spring security权限注解的支持

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceApplication {
    public static void main(String[] args) {
        SpringApplication.run(ResourceApplication.class, args);
    }
}

在resource/info接口方法上添加注解@PreAuthorize并指定角色或权限

@RequestMapping("/info")
@PreAuthorize("hasRole('USER')")
public String info(){

    return "success";
}

利用postman再次访问该接口

image-20220507150406869

得到不允许访问的结果,表明角色权限管理生效

1.6 整合数据库(client)

建表SQL

CREATE TABLE `clientdetails` (
  `appId` VARCHAR(128) NOT NULL,
  `resourceIds` VARCHAR(256) DEFAULT NULL,
  `appSecret` VARCHAR(256) DEFAULT NULL,
  `scope` VARCHAR(256) DEFAULT NULL,
  `grantTypes` VARCHAR(256) DEFAULT NULL,
  `redirectUrl` VARCHAR(256) DEFAULT NULL,
  `authorities` VARCHAR(256) DEFAULT NULL,
  `access_token_validity` INT(11) DEFAULT NULL,
  `refresh_token_validity` INT(11) DEFAULT NULL,
  `additionalInformation` VARCHAR(4096) DEFAULT NULL,
  `autoApproveScopes` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`appId`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_access_token` (
  `token_id` VARCHAR(256) DEFAULT NULL,
  `token` BLOB,
  `authentication_id` VARCHAR(128) NOT NULL,
  `user_name` VARCHAR(256) DEFAULT NULL,
  `client_id` VARCHAR(256) DEFAULT NULL,
  `authentication` BLOB,
  `refresh_token` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_approvals` (
  `userId` VARCHAR(256) DEFAULT NULL,
  `clientId` VARCHAR(256) DEFAULT NULL,
  `scope` VARCHAR(256) DEFAULT NULL,
  `status` VARCHAR(10) DEFAULT NULL,
  `expiresAt` TIMESTAMP NULL DEFAULT NULL,
  `lastModifiedAt` TIMESTAMP NULL DEFAULT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_client_details` (
  `client_id` VARCHAR(128) 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;

CREATE TABLE `oauth_client_token` (
  `token_id` VARCHAR(256) DEFAULT NULL,
  `token` BLOB,
  `authentication_id` VARCHAR(128) NOT NULL,
  `user_name` VARCHAR(256) DEFAULT NULL,
  `client_id` VARCHAR(256) DEFAULT NULL,
  PRIMARY KEY (`authentication_id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_code` (
  `code` VARCHAR(256) DEFAULT NULL,
  `authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;

CREATE TABLE `oauth_refresh_token` (
  `token_id` VARCHAR(256) DEFAULT NULL,
  `token` BLOB,
  `authentication` BLOB
) ENGINE=INNODB DEFAULT CHARSET=utf8;

在表 oauth_client_details 中增加一条客户端配置记录,在填入时可以按照AuthorizationServerConfiguration配置类中的客户端配置进行配置

配置的效果如下:

image-20220507161710501

注:各字段解释说明

  • client_id:客户端标识

  • client_secret:客户端安全码。注意安全码不能是明文需要加密,此处可以写一段程序,然后使用BCryptPasswordEncoder为客户端安全码加密,得到加密之后的安全码,再写入到数据库中,例如:

  • System.out.println(new BCryptPasswordEncoder().encode("secret"));

  • scope:客户端授权范围

  • authorized_grant_types:客户端授权类型,支持多种类型,多种类型之间用逗号隔开

  • web_server_redirect_uri:服务器回调地址

创建实体类User、Role、Perms

在auth-server模块的pom.xml中引入mybatis相关依赖

<!-- spring-boot-starter-jdbc 内置了HikariCP 连接池,所以使用该连接池连接数据库 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- mysql -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 获取application.yml文件中的配置 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>

application.yml文件中添加数据库相关配置

server:
  port: 8000
spring:
  application:
    name: oauth
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbc-url: jdbc:mysql://localhost:3306/sc?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
    username: root
    password: root
    hikari:
      minimum-idle: 5
      maximum-pool-size: 10
      auto-commit: true #自动提交
      pool-name: MYHIKARICP
      connection-test-query: SELECT 1 #测试是否能连接上数据库的SQL语句
  main:
    #true,后定义的bean会覆盖之前定义的相同名称的bean,生成dataSource替换掉原生的dataSource
    allow-bean-definition-overriding: true

创建数据库配置类DataSourceConfiguration,主要配置用到的数据源,用HikariCP连接池的数据源替换到spring内置的数据源。

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfiguration {
    @Bean   
    @Primary    
    //根据application.yml中的配置信息创建dataSource
    @ConfigurationProperties(prefix = "spring.datasource")
    //import javax.sql.DataSource;
    public DataSource dataSource() {
        //创建dataSource
        return DataSourceBuilder.create().build();
    }
}

在TokenConfiguration配置类中把token存储策略改成JDBC方式,将jwt存放到数据库中DataSource

@Resource
private DataSource dataSource;

// 令牌存储策略:jwt方式
@Bean
public TokenStore tokenStore(DataSource dataSource){
    //return new JwtTokenStore(accessTokenConverter());
    return new JdbcTokenStore(dataSource);
}

修改AuthorizationServerConfiguration配置类,添加ClientDetailsService clientDetailsService(DataSource dataSource)方法,让程序通过DataSource从数据库中获取到客户端信息

@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
    //在数据库中去获取客户端信息(oauth_client_details表)
    return new JdbcClientDetailsService(dataSource);
}

修改configure(ClientDetailsServiceConfigurer clients)方法指定到数据库获取客户端信息

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //配置客户端
    //clients
    //.inMemory()		//内存方式
    //.withClient("client")	//客户端名字
    //.secret(passwordEncoder.encode("secret"))	//客户端秘钥
    //.authorizedGrantTypes("authorization_code","password","refresh_token")
    //.scopes("all")	//授权范围
    //.redirectUris("http://www.woniuxy.com");	//回调网址

    clients.withClientDetails(clientDetailsService);
}

完成之后重启项目,再次进行认证测试

正常情况下,测试完毕之后会在数据库的oauth_access_token 表中会增加一个记录,这个记录就是浏览器获取到的token和refresh token

image-20220509101431817

角色、权限管理测试

在resource服务的controller中添加以下方法

@RequestMapping("/message")
@PreAuthorize("hasRole('ADMIN')")
public String message(){

    return "message";
}

@RequestMapping("/data")
@PreAuthorize("hasAuthority('user:add')")
public String data(){

    return "data";
}
@RequestMapping("/test")
@PreAuthorize("hasAuthority('user:del')")
public String test(){

    return "test";
}

启动resource服务,通过postman分别测试info、message、data、test接口,如果只有message、data接口可以访问,那么说明角色、权限管理成功。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/969089.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

reference based image enhancement 论文调研

Enhance Images as You Like with Unpaired Learning 这是IJCAI 2021的文章文章提出一个条件GAN模型&#xff0c;用reference image作为条件&#xff0c;可以在unpaired images上训练暗图增强模型&#xff0c;使得增强结果根据reference image来调节色调亮度和对比度。训练的监…

Redis事务为什么不支持回滚

Redis事务中过程中的错误分类两类&#xff1a; 在exec执行之前的错误&#xff0c;这种错误通常是指令错误&#xff0c;比如指令语法错误、内存不足等... --> 在开始事务后&#xff0c;传输指令时&#xff0c;遇到这种错误&#xff0c;Redis会给出Error错误提示&#xff0c;…

【多线程案例】定时器应用及实现

文章目录 1. 定时器是什么&#xff1f;2. 定时器的应用3. 自己实现定时器 1. 定时器是什么&#xff1f; 定时器就类似生活中的闹钟&#xff0c;它是软件开发中的一个重要组件。当有些线程我们并不希望它立刻执行&#xff0c;这个时候我们就可以使用定时器&#xff0c;规定线程在…

苹果iPhone15系列不再使用皮革保护壳?“FineWoven“官方认证替代

根据9月3日的报道&#xff0c;苹果即将推出的iPhone 15系列将不再使用皮革保护壳&#xff0c;取而代之的将是一种名为"FineWoven"的新材料编织工艺保护壳。 这种保护壳将有十种颜色可供选择&#xff0c;包括黑色、桑葚色、灰褐色、常绿色、太平洋蓝色、紫藤色、古白色…

Elasticsearch安装,Springboot整合Elasticsearch详细教程

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎&#xff0c;能够实现近乎实时的搜索。 Elasticsearch官网https://www.elastic.co/cn/ 目录 第一步&#xff1a;下载Elasticsearch 下载7.6.2版本 下载其他版本 第二步&#xff1a;安装Elasticsearch 第三…

【Spring+SpringMVC+Mybatis】SSM框架的整合、思想、工作原理和优缺点的略微讲解

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…

Redis布隆过滤器原理

其实布隆过滤器本质上要解决的问题&#xff0c;就是防止很多没有意义的、恶意的请求穿透Redis&#xff08;因为Redis中没有数据&#xff09;直接打入到DB。它是Redis中的一个modules&#xff0c;其实可以理解为一个插件&#xff0c;用来拓展实现额外的功能。 可以简单理解布隆…

2.(Python数模)(优化模型一)线性规划问题

Python解决线性规划问题 参考了以下博文 https://blog.csdn.net/m0_46692607/article/details/126784109?spm1001.2014.3001.5506 目标是解决以下的线性规划&#xff0c;程序计算出目标函数的最大值&#xff0c;并在最大值下取得的x1x2x3对应值。 源代码如下&#xff1a; …

Android studio 实现生成二维码和扫描二维码

效果图 build.gradle(:app)添加依赖 dependencies {implementation com.google.zxing:core:3.3.3implementation com.journeyapps:zxing-android-embedded:3.6.0implementation com.google.zxing:javase:3.0.0 }Manifests.xml <uses-permission android:name"android…

SceneXplain 图片叙事升级:如何让图片听得到

‍SceneXplain 是一个由多模态 AI 驱动的产品服务&#xff0c;它不仅 提供一流的图像和视频标注解决方案&#xff0c;还具备卓越的多模态视觉问答能力&#xff0c;为用户解锁视觉内容的全新维度。 在《图像描述算法排位赛》中&#xff0c;我们探讨了图像描述&#xff08;Image …

DSSM实战中文文本匹配任务

引言 本文我们通过DSSM模型来完成中文文本匹配任务&#xff0c;其中包含了文本匹配任务的一般套路&#xff0c;后续只需要修改实现的模型。 数据准备 数据准备包括 构建词表(Vocabulary)构建数据集(Dataset) 本次用的是LCQMC通用领域问题匹配数据集&#xff0c;它已经分好…

利用 GNU Radio + HackRF 做 FM 收音机

比特的打包与解包 GNU Radio 系列教程&#xff08;四&#xff09;&#xff0d;&#xff0d; 比特的打包与解包_哔哩哔哩_bilibili SDR 教程 —— 利用 GNU Radio HackRF 做 FM 收音机_哔哩哔哩_bilibili

Nginx+keepalived实现高可用项目实战

一、环境搭建 此次项目准备四台虚拟机&#xff1a; 防火墙关闭 安装好nginx&#xff08;一台master,一台back&#xff0c;两台Web服务器&#xff09; ip:(根据自己的进行搭建) 192.168.85.128(master) 192.168.85.129(back) 192.168.85.132(web1) 192.168.85.133(web2)…

排序算法问题

给你一个整数数组 nums&#xff0c;请你将该数组升序排列。 示例 1&#xff1a; 输入&#xff1a;nums [5,2,3,1] 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;nums [5,1,1,2,0,0] 输出&#xff1a;[0,0,1,1,2,5] 代码如下&#xff1a; 1.插入排序(简…

Python 中轻松实现串口通信

迷途小书童的 Note 读完需要 3分钟 速读仅需 1 分钟 1 简介 pyserial 是一个 Python 库&#xff0c;它可以让您轻松地与串行端口进行通信。它支持多种操作系统&#xff0c;包括 Windows、Linux 和 macOS。pyserial 模块非常易于使用&#xff0c;并且提供了许多有用的功能。 2 实…

数学建模--二次规划型的求解的Python实现

目录 1.算法流程简介 2.算法核心代码 3.算法效果展示 1.算法流程简介 #二次规划模型 #二次规划我们需要用到函数:Cvxopt.solvers.qp(P,q,G,h,A,b) #首先解决二次规划问题和解决线性规划问题的流程差不多 """ 求解思路如下: 1.针对给定的代求式,转化成标准式…

8.(Python数模)(预测模型一)马尔科夫链预测

Python实现马尔科夫链预测 马尔科夫链原理 马尔科夫链是一种进行预测的方法&#xff0c;常用于系统未来时刻情况只和现在有关&#xff0c;而与过去无关。 用下面这个例子来讲述马尔科夫链。 如何预测下一时刻计算机发生故障的概率&#xff1f; 当前状态只存在0&#xff08;故…

数学建模--最短路径算法的Python实现

目录 1.算法流程简介 2.算法核心代码 3.算法效果展示 1.算法流程简介 #最短路径算法 #针对有向图的最短路径问题,我们有很多的算法能解决. """ 目前主流算法如下所示: Dijkstra算法:Dijkstra算法是一种单源最短路径算法,用于计算从起点到其它所有节点的最短…

VIRTIO-BLK代码分析(0)概述

也无风雨也无晴。- 苏轼&#xff08;宋&#xff09; 接下来介绍VIRTIO相关内容。首先从VIRTIO-BLK开始分析&#xff0c;VIRTIO-BLK各部分交互图如下所示&#xff1a; 这里包含以下几个部分&#xff1a; Guest UserSpace&#xff1a;虚拟机用户空间&#xff0c;如虚拟机中运行f…