18.Oauth2-微服务认证

news2024/12/23 22:50:24

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/954677.html

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

相关文章

百度等8家企业首批上线大模型服务;大语言模型微调之道

&#x1f989; AI新闻 &#x1f680; 百度等8家企业首批上线大模型服务 摘要&#xff1a;百度、字节、中科院旗下8家企业/机构的大模型通过备案&#xff0c;正式面向公众提供服务。百度旗下AI大模型产品文心一言率先开放&#xff0c;用户可下载App或登录官网体验。百川智能也…

【前端自动化部署】,Devops,CI/CD

DevOps 提到Jenkins&#xff0c;想到的第一个概念就是 CI/CD 在这之前应该再了解一个概念。 DevOps Development 和 Operations 的组合&#xff0c;是一种方法论&#xff0c;并不特指某种技术或者工具。DevOps 是一种重视 Dev 开发人员和 Ops 运维人员之间沟通、协作的流程。…

高级DBA手把手教你Mysql大数据量批量导入人大金仓国产数据库方法(全网最详细)

Mysql数据批量导入人大金国产数据库方法 参考下面地址&#xff0c;先安装好整体的安装包到电脑上 https://blog.csdn.net/nasen512/article/details/132599267迁移工具安装方法查看我另一篇上面地址的文章&#xff0c;作者是全量安装金仓自带的windows迁移程序! 1、打开官方…

微信小程序原生框架转Uni-App:你需要知道的一切

目录 微信小程序原生框架转Uni-App&#xff1a;你需要知道的一切引言目录为什么选择Uni-App准备工作文件结构调整代码转换API适配微信小程序&#xff1a;获取用户地理位置Uni-App&#xff1a;获取用户地理位置对比与分析 测试与调试总结与建议 博主 默语带您 Go to New World. …

CGLIB 问题

全部错误信息如下&#xff1a;&#xff08;为了大家能搜到&#xff0c;拼了&#xff0c;全部截下来~&#xff09; [main] INFO org.springframework.aop.framework.CglibAopProxy - Final method [protected final org.hibernate.Session org.springframework.orm.hibernate5.s…

【算法笔记】二维的哈希与迭代转换;Runtime Error 的解决思路

https://vjudge.net/problem/UVA-11019 如何对一个二维数组进行哈希 对于一个一维数组A(1*M)&#xff0c;哈希的方式是&#xff1a; s e e d M − 1 ∗ A [ 0 ] s e e d M − 2 ∗ A [ 1 ] s e e d M − 3 ∗ A [ 2 ] . . . s e e d 0 ∗ A [ M − 1 ] seed^{M-1}*A[0] …

继续绷紧油市神经,市场预计沙特10月继续自愿减产

KlipC报道&#xff1a;据了解&#xff0c;市场参与者大都认为沙特阿拉伯将会把自愿额外减产的措施延长至10月底&#xff0c;以寻求在经济低迷的背景下提振油价。 KlipC的合伙人Andi D表示&#xff1a;“今年5月起&#xff0c;沙特就自愿减产日均50万桶原油&#xff0c;今年6月初…

app加固怎么做?

app加固是一种保护应用程序免受攻击的技术&#xff0c;可以防止未经授权的访问、数据泄露和其他安全问题。下面是一些app加固的最佳实践&#xff1a; 1.加密和数据保护&#xff1a;对应用程序的数据进行加密和保护&#xff0c;以防止未经授权的访问和数据泄露。您可以使用AES、…

工具及方法 - 色彩分析仪

在手机或其他带有显示屏的产品开发中&#xff0c;需要对显示屏显示的颜色进行测量&#xff0c;就会用到色彩分析仪。 看到别人在使用的一款&#xff0c;就是CA-410 display color analyzer&#xff0c;柯尼卡美能达CA-410高精度色彩分析仪。(Konica Minolta) 产品页链接&#x…

【输入法】Knife4j打字时,莫名其妙删除文本

一直以为是ui的问题&#xff0c;想着升级下Knife4j&#xff0c;无意中发现切换输入法后&#xff0c;该问题不复现。 遂卸载QQ拼音输入法6.6

信息图片怎么做二维码?图片生码的操作方法

现在很多人会将自己的证件信息的图片生成二维码使用&#xff0c;那么图片转二维码的操作方法是什么样的呢&#xff1f;想要将图片生成二维码的方法非常简单&#xff0c;利用二维码生成器&#xff08;免费在线二维码生成器-二维码在线制作-音视频二维码在线生成工具-机智熊二维码…

降噪音频转录 Krisp: v1.40.7 Crack

主打人工智能降噪服务的初创公司「Krisp」近期宣布推出音频转录功能&#xff0c;能对电话和视频会议进行实时设备转录。该软件还整合的ChatGPT&#xff0c;以便快速总结内容&#xff0c;开放测试版于今天上线。 随着线上会议越来越频繁&#xff0c;会议转录已成为团队工作的重…

国标GB28181视频平台EasyGBS国标视频云平台级联到EasyCVR,上级平台无法播放通道视频的问题解决方案

EasyGBS国标视频云平台是基于国标GB28181协议的视频能力兼服务平台&#xff0c;可实现的视频能力包括将设备通过国标GB28181协议接入、流媒体转码、处理及分发、直播录像、语音对讲、云存储、告警、平台级联等功能。其中&#xff0c;平台级联功能是指平台与平台之间可以通过国标…

2023抖音小店体验分迎来重大更新,考核标准要变啦!

抖音小店体验分即将有重大变化&#xff01;4月13日&#xff0c;平台发布了一条意见征集&#xff0c;关于体验分修改的问题。 核心变化主要包括以下几点&#xff1a; 1、指标体系调整 1.1 商品体验&#xff1a;移除商品基础分指标&#xff1b;移除融合型指标【综合负向反馈率…

IDEA常用快捷键大全(最新)

一、 前言 IDEA 中提供了很多快捷键&#xff0c;点击File --> Settings --> keymap便可进入看到 IDEA 提供的快捷键。我们也可以搜索和自定义所有快捷键&#xff0c;下面给出的是 IDEA 中默认的快捷键&#xff1b; 默认idea用的是windows模式 二、常用快捷键 快捷键 …

Matlab怎么引入外部的latex包?Matlab怎么使用特殊字符?

Matlab怎么引入外部的latex包&#xff1f;Matlab怎么使用特殊字符&#xff1f; Matlab怎么使用特殊字符&#xff1f;一种是使用latex方式&#xff0c;Matlab支持基本的Latex字符【这里】&#xff0c;但一些字符需要依赖外部的包&#xff0c;例如“&#x1d53c;”&#xff0c;需…

在 Tubi,英文书面沟通是这样的

在之前的文章《有了英文&#xff0c;你将在这些场景中更自信》中&#xff0c;我们曾对 Tubi 使用英文听说读写方面的场景进行了汇总整理。尽管英文听与说曾是大家最关注的方面&#xff0c;但在今年七月和八月期间 Tubi 中国团队开展的一场名为“Writing for Results” 的工作坊…

如何从ChatGPT中获得最佳聊天对话效果

从了解ChatGPT工作原理开始&#xff0c;然后从互动中学习&#xff0c;这是一位AI研究员的建议。 人们利用ChatGPT来撰写文章、论文、生成文案和计算机代码&#xff0c;或者仅仅作为学习或研究工具。然而&#xff0c;大多数人不了解它的工作原理或它能做什么&#xff0c;所以他…

算法设计 || 第11题:多段图的最短路径问题动态规划

利用动态规划来编程求解多段图最短路径问题。 对如下图所示的一个5段图&#xff0c;图上的数字代表该段路径的成本。 写出求最短路径的计算过程&#xff0c;给出最短路径和距离。