微服务[学成在线] day16:基于Spring Security Oauth2开发认证服务

SpringBoot+Vue3智慧停车场系统开发实战:前后端分离全栈教程 前后端分离架构是现代Web应用开发的主流模式,通过将前端展示层与后端业务逻辑解耦,实现开发效率与系统性能的双重提升。SpringBoot作为Java生态中的明星框架,提供了快速构建企业级应用的能力,而Vue3则以其响应式系统和组合式API在前端开发中占据重要地位。这种技术组合在智慧城市、物联网等场景中具有广泛应用价值,特别是在智慧停车场管理系统这类需要实时数据处理和用户友好界面的项目中。基于SpringBoot和Vue3的开发实践,能够帮助开发者掌握车位预约、智能计费、数据分析等核心功能的实现方法,为智慧交 阅读详情

😎 知识点概览

为了方便后续回顾该项目时能够清晰的知道本章节讲了哪些内容,并且能够从该章节的笔记中得到一些帮助,所以在完成本章节的学习后在此对本章节所涉及到的知识点进行总结概述。

本章节为【学成在线】项目的 day16 的内容

  • 学习 Spring Security + Oauth2 基本概念以及实现过程。
  • 学习 Oauth2 的基本应用场景,这里主要是通过 Oauth2 的密码模式来实战。
  • 初识 JWT 令牌。
  • 本章节的最后通过 Spring Security Oauth2 完成了认证服务的基本实现,但授权还没做。

目录

内容会比较多,小伙伴们可以根据目录进行按需查阅。

一、用户需求分析

0x01 用户认证与授权

截至目前,项目已经完成了在线学习功能,用户通过在线学习页面点播视频进行学习。

如何去记录学生的学习过程呢?

要想掌握学生的学习情况就需要知道用户的身份信息,记录哪个用户在什么时间学习什么课程;如果用户要购买课程也需要知道用户的身份信息。所以,去管理学生的学习过程最基本的要实现用户的身份认证。

什么是用户身份认证?

用户身份认证即用户去访问 系统资源 时系统要求验证用户的身份信息,身份合法方可继续访问。常见的用户身份认证表现形式有:用户名密码登录,指纹打卡等方式。

什么是用户授权?

用户认证通过后去访问系统的资源,系统会判断用户是否拥有访问资源的 权限,只允许访问有权限的系统资源,没有权限的资源将无法访问,这个过程叫用户授权。

0x02 单点登录需求

本项目包括多个子项目,如:学习系统,教学管理中心、系统管理中心等,为了提高用户体验性需要实现用户只认证一次便可以在多个拥有访问权限的系统中访问,这个功能叫做单点登录。

引用百度百科:单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一。

SSO 的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
下图是 SSO 的示意图,用户登录学成网一次即可访问多个系统。

0x03 第三方认证需求

作为互联网项目难免需要访问外部系统的资源,同样本 服务 也要访问 其他服务 的资源接口。

一个微信用户没有在学成在线注册,本系统可以通过请求微信系统来验证该用户的身份,验证通过后该用户便可在本系统学习,它的基本流程如下:

从上图可以看出,微信不属于本系统,本系统并没有存储微信用户的账号、密码等信息,本系统如果要获取该用户的基本信息则需要首先通过微信的认证系统(微信认证)进行认证,微信认证通过后本系统便可获取该微信用户的基本信息,从而在本系统将该微信用户的头像、昵称等信息显示出来,该用户便不用在本系统注册却可以直接学习。

什么是第三方认证(跨平台认证)?

当需要访问第三方系统的资源时需要首先通过第三方系统的认证(例如:微信认证),由第三方系统对用户认证通过,并授权资源的访问权限。

二、用户认证技术方案

0x01 单点登录技术方案

分布式系统要实现单点登录,通常将认证系统独立抽取出来,并且将用户身份信息存储在单独的存储介质,比如:MySQL、Redis,考虑性能要求,通常存储在 Redis 中,如下图:

单点登录的特点是:

1、认证系统为独立的系统。

2、各个 子系统 通过 Http 或其它协议与认证系统通信,完成用户认证。

3、用户身份信息存储在 Redis 集群。

Java 中有很多用户认证的框架都可以实现单点登录:

1、Apache Shiro.

2、CAS

3、Spring security CAS

0x02 Oauth2认证

认证流程

第三方认证技术方案最主要是解决认证协议的通用标准 问题,因为要实现 跨系统认证,各系统之间要遵循一定的接口协议。

OAUTH 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用 OAUTH 认证服务,任何服务提供商都可以实现自身的 OAUTH 认证服务,因而 OAUTH 是开放的。业界提供了 OAUTH 的多种实现如 PHP、JavaScript,Java,Ruby 等各种语言开发包,大大节约了程序员的时间,因而 OAUTH 是简易的。互联网很多服务如 Open API,很多大公司如 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认证的例子,黑马程序员网站使用微信认证的过程:

从流程图可以看出,用户首先需要访问黑马程序员的登录页面,登录页面中会有一个第三方登录的选项,例如选择微信来进行登录。

点击微信登录后,黑马程序员网站会向微信获取到一个认证授权的页面,并返回给客户端,客户端自动跳转到该 认证授权页面 进行微信的认证,当用户通过微信授权认证成功后,微信的认证服务器会返回一个授权码到客户端,客户端使用授权码向微信认证服务器申请 认证token,当用户获取到 token 后,会携带该 token 值去请求黑马程序员网站,黑马程序员通过该token向微信服务器获取到用户的微信信息后,黑马程序员网站才能确定该用户是可信的。

具体流程演示如下:

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

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

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

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

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

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

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

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

认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在黑马程序员看到已经登录成功。

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

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

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

资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容。
注意:资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务器通常要请求认证服务器来校验令牌的合法性。

Oauth2.0 认证流程如下:

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

Oauth2包括以下角色:

1、客户端

本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:学成在线Android客户端、学成在线Web客户端(浏览器端)、微信客户端等。

2、资源拥有者

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

3、授权服务器(也称认证服务器)
用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授权后方可访问。

4、资源服务器
存储资源的服务器,比如,学成网用户管理服务器存储了学成网的用户信息,学成网学习服务器存储了学生的学习信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息。

Oauth2在本项目的应用

Oauth2是一个标准的开放的授权协议,应用程序可以根据自己的要求去使用 Oauth2,本项目使用 Oauth2 实现如下目标:

1、学成在线访问第三方系统的资源

2、外部系统访问学成在线的资源

3、学成在线前端(客户端) 访问学成在线微服务的资源。

4、学成在线微服务之间访问资源,例如:微服务A 访问 微服务B 的资源,B 访问 A 的资源。

0x03 Spring Security Oauth2 认证解决方案

本项目采用 Spring security + Oauth2 完成用户认证及用户授权,Spring security 是一个强大的和高度可定制的身份验证和访问控制框架,Spring security 框架集成了Oauth2 协议,下图是项目认证架构图:

1、用户请求认证服务完成认证。

2、认证服务下发用户身份令牌,拥有身份令牌表示身份合法。

3、用户携带令牌请求资源服务,请求资源服务必先经过网关。

4、网关校验用户身份令牌的合法,不合法表示用户没有登录,如果合法则放行继续访问。

5、资源服务获取令牌,根据令牌完成授权。

6、资源服务完成授权则响应资源信息。

三、Spring Security Oauth2 研究

0x01 目标

本项目认证服务基于 Spring Security Oauth2 进行构建,并在其基础上作了一些扩展,采用 JWT 令牌机制,并自定义了用户身份信息的内容。 本教程的主要目标是学习在项目中集成Spring Security Oauth2 的方法和流程,通过 Spring Security Oauth2 的研究需要达到以下目标:

1、理解 Oauth2 的授权码认证流程及密码认证的流程。

2、理解 Spring Security Oauth2 的工作流程。

3、掌握资源服务集成 Spring Security 框架完成 Oauth2 认证的流程。

0x02 搭建认证服务器

导入基础工程

导入 资料 目录下的 xc-service-ucenter-auth 工程,该工程是基于Spring Security Oauth2 的一个二次封装的工程,导入此工程研究 Oauth2 认证流程。

创建数据库

导入资料目录下的 xc_user.sql,创建用户数据库

以 oauth_ 开头的表都是 Spring Security 自带的表。

本项目中 Spring Security 主要使用 oauth_client_details 表:

  • client_id:客户端id

  • resource_ids:资源id(暂时不用)

  • client_secret:客户端密码

  • scope:范围

  • access_token_validity:访问token的有效期(秒)

  • refresh_token_validity:刷新token的有效期(秒)

  • authorized_grant_type:授权类型,

    • authorization_code
    • password
    • refresh_token
    • client_credentials

0x03 Oauth2授权码模式

Oauth2 有以下授权模式:

  • 授权码模式(Authorization Code)

  • 隐式授权模式(Implicit)

  • 密码模式(Resource Owner PasswordCredentials)

  • 客户端模式(Client Credentials)

其中授权码模式和密码模式应用较多,本小节介绍授权码模式。

授权码授权流程

上边例举的黑马程序员网站使用微信认证的过程就是授权码模式,流程如下:

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

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

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

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

5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权

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

申请授权码

请求认证服务获取授权码:

GET 请求:

localhost:40400/auth/oauth/authorize?
client_id=XcWebApp&response_type=code&scop=app&redirect_uri=http://localhost

参数列表如下:

  • client_id:客户端 id,和授权配置类中设置的客户端id一致。
  • response_type:授权码模式固定为 code
  • scop:客户端范围,和授权配置类中设置的 scop一致。
  • redirect_uri:跳转 uri,当授权码申请成功后会跳转到此地址,并在后边带上code参(授权码)。

首次访问会跳转到登录页面:

输入账号和密码,点击 Login。

Spring Security 接收到请求会调用 UserDetailsService 接口的 loadUserByUsername 方法查询用户正确的密码。

在 oauth_client_details 表中配置认证的账号和密码,当然密码是加密后储存的,这里我们暂时先不关注,后面再讲解

账号密码为 XcWebApp 和 XcWebApp

接下来进入授权页面:

点击 同意,接下来返回授权码:认证服务携带授权码跳转 redirect_uri

申请令牌

拿到授权码后,申请令牌。

POST 请求:http://localhost:40400/auth/oauth/token

参数如下:

  • grant_type:授权类型,填写authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

此链接需要使用 http Basic认证。

什么是 http Basic认证?

http 协议定义的一种认证方式,将客户端id和客户端密码按照 客户端ID:客户端密码 的格式拼接,并用 base64 编码,放在 header 中请求服务端,一个例子:

Authorization:Basic WGNXZWJBcHA6WGNXZWJBcHA=

WGNXZWJBcHA6WGNXZWJBcHA= 是 用户名:密码 的 base64 编码。

如果认证失败服务端会返回 401 Unauthorized

以上测试使用 postman 完成。

http basic认证:

客户端 Id 和客户端密码会匹配数据库 oauth_client_details 表中的客户端 id 及客户端密码。

POST 请求参数:

点击发送:

申请令牌成功。

  • access_token:访问令牌,携带此令牌访问资源
  • token_type:有 MAC Token与 Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer
  • Token(http://www.rfcreader.com/#rfc6750)。
  • refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
  • expires_in:过期时间,单位为秒。
  • scope:范围,与定义的客户端范围一致。

资源服务授权

1)授权流程

资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资源,流程如下图:

上图的业务流程如下:

1、客户端请求认证服务申请令牌

2、认证服务生成令牌认证服务采用非对称加密算法,使用私钥生成令牌。

3、客户端携带令牌访问资源服务客户端在 Http header 中添加: Authorization:Bearer 令牌。

注意这里的Authorization字段的值为 Bearer + 空格 + 令牌

4、资源服务请求认证服务校验令牌的有效性资源服务接收到令牌,使用公钥校验令牌的合法性。

5、令牌有效,资源服务向客户端响应资源信息

2)授权配置

基本上所有微服务都是资源服务,这里我们在 课程管理服务 上配置授权控制,当配置了授权控制后如要访问课程信息则必须提供令牌。

在我们导入的 auth 工程的 resources 下可以看到一个 xc.keystore 文件,该文件是用于认证的一个私钥文件,用于生成我们的授权码,生成的授权码可以使用 公钥 文件来进行校验。下面我们来做一个简单的实验来了解整个校验的流程。

1、配置公钥

认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使
用公钥 来校验令牌的合法性。

将 day16 的 资料 下的公钥拷贝到 publickey.txt 文件中,将此文件拷贝到资源服务工程的 classpath 下

2、添加依赖

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

4、在 config 包下创建 ResourceServerConfig 类:

package com.xuecheng.manage_course.config;

@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  //公钥
  private static final String PUBLIC_KEY = "publickey.txt";
  //定义JwtTokenStore,使用jwt令牌
  @Bean
  public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
    return new JwtTokenStore(jwtAccessTokenConverter);
  }
  //定义JJwtAccessTokenConverter,使用jwt令牌
  @Bean
  public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setVerifierKey(getPubKey());
    return converter;
  }
    /**
      * 获取非对称加密公钥 Key
      * @return 公钥 Key
      */
  private String getPubKey() {
    Resource resource = new ClassPathResource(PUBLIC_KEY);
    try {
      InputStreamReader inputStreamReader = new
          InputStreamReader(resource.getInputStream());
      BufferedReader br = new BufferedReader(inputStreamReader);
      return br.lines().collect(Collectors.joining("\n"));
    } catch (IOException ioe) {
      return null;
    }
  }
  //Http安全配置,对每个到达系统的http请求链接进行校验
  @Override
  public void configure(HttpSecurity http) throws Exception {
    //所有请求必须认证通过
    http.authorizeRequests().anyRequest().authenticated();
  }
}

3)授权测试

这里我们使用 POSTMAN 测试课程图片查询

GET http://localhost:31200/course/coursepic/list/4028e58161bd3b380161bd3bcd2f0000

请求时没有携带令牌则报错:

{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}

请求时携带令牌:

在 http header 中添加 Authorization: Bearer 令牌

当输入错误的令牌也无法正常访问资源。

4)解决swagger-ui无法访问

这个问题可以单独提取出来,发布到csdn上。

当课程管理加了授权之后再访问 swagger-ui 则报错

修改授权配置类 ResourceServerConfig 的 configure 方法:

针对 swagger-ui 的请求路径进行放行:

//Http安全配置,对每个到达系统的http请求链接进行校验
@Override
public void configure(HttpSecurity http) throws Exception {
    //所有请求必须认证通过
    http.authorizeRequests()
            //下边的路径放行
            .antMatchers("/v2/api-docs", "/swagger-resources/configuration/ui",
                    "/swagger-resources","/swagger-resources/configuration/security",
                    "/swagger-ui.html","/webjars/**").permitAll()
            .anyRequest().authenticated();
}

注意:

通过上边的配置虽然可以访问 swagger-ui,但是无法进行单元测试,除非去掉认证的配置或在上边配置中添加所有请求均放行("/**")。

但是需要注意的是,虽说在开发环境下我们可与通过使用 /** 的方式来便于我们进行单元测试,但是难免会有疏漏的时候,如果在生产上线时没有及时改回来,那么后果不堪设想。

所以我们可以考虑使用多环境配置的形式,将需要放行的 url 从配置文件 application.yml 中读取,而开发环境中,我们可以单独配置一个 application-dev.yml 作为我们的开发环境的配置,用于区别上线环境的配置。例如下面的例子

application-dev.yml 加入 oauth2.urlMatchers ,在该字段下指定我们无需授权访问的一些url地址,使用 , 进行分割

oauth2:
  urlMatchers: /v2/api-docs,/swagger-resources/configuration/ui,/swagger-resources,/swagger-resources/configuration/security,/swagger-ui.html,/webjars/**

而在生产环境中的配置 application-dev.yml 你可以不配置 urlMatchers 的值,这样所有的url都需要认证后才能访问,当然,你也可以添加一些例外。如下则是全部 url 都拦截的情况的配置,虽然没有值,但是我们也要在配置文件中写出这个 urlMatchers 字段,便于后面的拓展工作。

oauth2:
  urlMatchers: 

那么我们的配置类 ResourceServerConfig 就应该做出如下的修改:

使用 @Value 注解获取配置文件中 urlMatchers 的值,在 configure 方法下做出相关的操作。

@Value("${oauth2.urlMatchers}")
String urlMatchers;

//Http安全配置,对每个到达系统的http请求链接进行校验
@Override
public void configure(HttpSecurity http) throws Exception {
    if(urlMatchers.equals("")){
        //如果urlMatchers未指定,则所有url都需要授权后才能被访问
        http.authorizeRequests().anyRequest().authenticated();
    }else{
        //放行 urlMatchers 中指定的url条目, 未指定的url仍需授权后才能访问
        String[] split = urlMatchers.split(",");
        http.authorizeRequests()
            //下边的路径放行
            .antMatchers(split).permitAll()
            .anyRequest().authenticated();
    }
}

0x04 Oauth2密码模式认证

密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接
通过用户名和密码即可申请令牌。

测试如下:

POST 请求:http://localhost:40400/auth/oauth/token

参数:

  • grant_type:密码模式授权填写password
  • username:账号
  • password:密码

那么这个密码信息是从哪里获取到的?

在我们的 auth 服务工程中可以看到,我们写了一个 UserDetailsService 的实现类。

这个实现类中实现了 loadUserByUsername 方法,在该方法中,首先会验证提交请求中带有的 App 用户密码信息是否正确,也就是我们提交的 http Basic 认证信息,App的认证信息通过后,会出数据库获取用户的认证信息和权限信息进行设置,然后再根据我们提交的信息进行比对。但在当前的测试中,我们是直接在 loadUserByUsername 方法内自定义了一个账号和密码,便于我们测试,完整的认证授权流程会在后面的内容中讲到。

//设置用户的认证和权限信息
XcUserExt userext = new XcUserExt();
userext.setUsername("mrt");
userext.setPassword(new BCryptPasswordEncoder().encode("123"));
userext.setPermissions(new ArrayList<XcMenu>());
if(userext == null){
return null;
}

使用 http Basic 进行App的身份认证,这里我们的账号和密码都为 XcWebApp。

上边参数使用 x-www-form-urlencoded 方式传输,使用postman测试如下:

那么,授权码模式与密码模式,分别都适用于哪些场景?

  • 授权码模式一般适用于提供给第三方进行认证,例如在前面提到的在黑马程序员网站进行微信登录时,这里我们的角色就应该对应的是 微信的认证服务器,而黑马程序员网站属于第三方。
  • 密码模式在我们后续的开发中会经常用到,一般用于我们微服务间的认证以及用户的前台、后台权限管理等场景。

0x05 校验令牌

Spring Security Oauth2 提供校验令牌的端点,如下:

GET: http://localhost:40400/auth/oauth/check_token?token=

参数:

  • token:令牌

使用 postman 测试如下:

结果如下:

{
    "companyId": null,
    "userpic": null,
    "user_name": "itcast",
    "scope": [
        "app"
    ],
    "name": null,
    "utype": null,
    "active": true,
    "id": null,
    "exp": 1590351690,
    "jti": "ed441eb3-cd16-4e74-b598-484656a03287",
    "client_id": "XcWebApp"
}
  • exp:过期时间,long类型,距离1970年的秒数(new Date().getTime()可得到当前时间距离1970年的毫秒数)。

  • user_name: 用户名

  • client_id:客户端Id,在oauth_client_details中配置

  • scope:客户端范围,在oauth_client_details表中配置

  • jti:与令牌对应的唯一标识

  • companyId、userpic、name、utype、id:这些字段是本认证服务在Spring Security基础上扩展的用户身份信息

我们可以根据这些数据进行一些相关的操作。

0x06 刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码
也不需要账号和密码,只需要一个 刷新令牌、客户端id 和 客户端密码。

测试如下:

POST:http://localhost:40400/auth/oauth/token

参数:

  • grant_type: 固定为 refresh_token
  • refresh_token:刷新令牌(注意不是 access_token,而是 refresh_token)

刷新令牌成功,会重生成新的访问令牌和刷新令牌,令牌的有效期也比旧令牌长。

刷新令牌通常是在令牌快过期时进行刷新。

0x07 JWT研究

JWT介绍

在介绍JWT之前先看一下传统校验令牌的方法,如下图:

问题:

传统授权方法的问题是用户每次请求资源服务,资源服务都需要携带令牌访问认证服务去校验令牌的合法性,并根
据令牌获取用户的相关信息,性能低下。

解决:

使用 JWT 的思路是,用户认证通过会得到一个 JWT 令牌,JWT 令牌中已经包括了用户相关的信息,客户端只需要携带 JWT 访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。JWT 令牌授权过程如下图:

什么是 JWT ?

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于
在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT 可以使用 HMAC 算法或使用 RSA的公钥/私钥对来签名,防止被篡改。

官网:https://jwt.io/

标准:https://tools.ietf.org/html/rfc7519

JWT 令牌的优点:

1、jwt基于 json,非常方便解析。

2、可以在令牌中自定义丰富的内容,易扩展。

3、通过非对称加密算法及数字签名技术,JWT 防止篡改,安全性高。

4、资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:JWT令牌较长,占存储空间比较大。

令牌结构

通过学习JWT令牌结构为自定义 jwt 令牌打好基础。

JWT 令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

1、Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)
下边是Header部分的内容

{
    "alg": "HS256",
    "typ": "JWT"
}

2、Payload

第二部分是用于储存一些有效信息,内容也是一个 json对象,它可以存放 jwt 提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用 Base64Url 编码,得到一个字符串就是JWT令牌的第二部分。

一个例子 :

{
    "sub": "1234567890",
    "name": "456",
    "admin": true
}

3、Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用 base64url 将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明
签名算法进行签名。

一个例子:

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)  
  • base64UrlEncode(header):jwt令牌的第一部分。

  • base64UrlEncode(payload):jwt令牌的第二部分。

  • secret:签名所使用的密钥。

JWT入门

Spring Security 提供对 JWT 的支持,本节我们使用 Spring Security 提供的 JwtHelper 来创建JWT令牌,校验JWT令牌等操作。

生成私钥和公钥

JWT令牌生成采用非对称加密算法

1、生成密钥证书

下边命令生成密钥证书,采用RSA 算法每个证书包含公钥和私钥

keytool -genkeypair -alias xckey -keyalg RSA -keypass xuecheng -keystore xc.keystore -storepass xuechengkeystore

Keytool 是一个 java 提供的证书管理工具,以下是参数的说明

  • -alias:密钥的别名

  • -keyalg:使用的hash算法

  • -keypass:密钥的访问密码

  • -keystore:密钥库文件名,xc.keystore保存了生成的证书

  • -storepass:密钥库的访问密码

查询证书信息:

keytool -list -keystore xc.keystore  

删除别名:

keytool -delete -alias xckey -keystore xc.keystore  

2、导出公钥

openssl 是一个加解密工具包,这里使用openssl来导出公钥信息。

安装 openssl:http://slproweb.com/products/Win32OpenSSL.html

安装完成后,配置 openssl 的 path 环境变量,本教程配置在 D:\OpenSSL-Win64\bin

在命令行进入 xc.keystore 文件所在目录执行如下命令:

keytool -list -rfc --keystore xc.keystore | openssl x509 -inform pem -pubkey

输入密钥库密码,如下图:

复制生成出来的公钥数据

将上边的公钥拷贝到文本文件中,合并为一行,换行会有换行符,所以尽可能的避免一些我的发生 ,可以用notepad++ 直接替换换行符 \n 如下图,当然你也可以手动的合并成一行。

-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAi1pGvYqLcTG2dcKhrtisQgkB90iWaCwE4OriDwCLMdiUV2NViEn+r/jMbuIFCBtnB21yWZlIPnXjzcre/8HIUJy2dMWqP9NUhzoCzwdC1I9clZRVHTpe1H0eiaQY4BLxz5EScBZdr5u4Q0hT+t6D3t7qQg1MHxLBaFy2cdHQbmz5Ly/1mmnWBHmFgjbbNG7gfaO3jRCl7RbNVUfSjb6gN+MfpyLk/iXr5S8Qhc2X07hvtm09QEk3cl14tQkZkXAUk7rAl9kgPSKoKr4MAdiYEsVNplKd4LMs4S2AC0dYrhdIX754eo6u4Ehpe6v5hSsF2d3ZpuV7nJ6JDCNxo7tU9wIDAQAB-----END PUBLIC KEY-----
生成jwt令牌

在认证工程创建测试类,测试jwt令牌的生成与验证。

package com.xuecheng.auth;

import com.alibaba.fastjson.JSON;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import org.springframework.test.context.junit4.SpringRunner;

import java.security.KeyPair;
import java.security.interfaces.RSAPrivateKey;
import java.util.HashMap;
import java.util.Map;

@SpringBootTest
@RunWith(SpringRunner.class)
public class JwtTest {
    //生成一个jwt令牌
    @Test
    public void testCreateJwt(){
        //证书文件
        String key_location = "xc.keystore";
        //密钥库密码
        String keystore_password = "xuechengkeystore";
        //访问证书路径
        ClassPathResource resource = new ClassPathResource(key_location);
        //密钥工厂
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(resource, keystore_password.toCharArray());
        //密钥的密码,此密码和别名要匹配
        String keypassword = "xuecheng";
        //密钥别名
        String alias = "xckey";
        //密钥对(密钥和公钥)
        KeyPair keyPair = keyStoreKeyFactory.getKeyPair(alias,keypassword.toCharArray());
        //私钥
        RSAPrivateKey aPrivate = (RSAPrivateKey) keyPair.getPrivate();
        //定义payload信息
        Map<String, Object> tokenMap = new HashMap<>();
        tokenMap.put("id", "123");
        tokenMap.put("name", "mrt");
        tokenMap.put("roles", "r01,r02");
        tokenMap.put("ext", "1");
        //生成jwt令牌
        Jwt jwt = JwtHelper.encode(JSON.toJSONString(tokenMap), new RsaSigner(aPrivate));
        //取出jwt令牌
        String token = jwt.getEncoded();
        System.out.println(token);
    }
}
验证jwt令牌
//资源服务使用公钥验证jwt的合法性,并对jwt解码
@Test
public void testVerify(){
    //jwt令牌
    String token ="";
    //公钥
    String publickey = "";
    //校验jwt
    Jwt jwt = JwtHelper.decodeAndVerify(token, new RsaVerifier(publickey));
    //获取jwt原始内容
    String claims = jwt.getClaims();
    System.out.println(claims);
    //jwt令牌
    String encoded = jwt.getEncoded();
    System.out.println(encoded);
}

四、认证服务开发

0x01 需求分析

用户登录的流程图如下

执行流程:

1、用户登录,请求认证服务

2、认证服务认证通过,生成 jwt 令牌,将 jwt 令牌及相关信息写入 Redis,并且将身份令牌写入 cookie

3、用户访问资源页面,带着 cookie 到网关

4、网关从 cookie 获取 token,并查询 Redis 校验 token,如果 token 不存在则拒绝访问,否则放行

5、用户退出,请求认证服务,清除 redis 中的 token,并且删除 cookie 中的 token

使用 redis 存储用户的身份令牌有以下作用:

1、实现用户退出注销功能,服务端清除令牌后,即使客户端请求携带 token 也是无效的。

2、由于 jwt 令牌过长,不宜存储在 cookie 中,所以将 jwt 的 身份令牌 存储在 redis,客户端请求服务端时附带这个 身份令牌,服务端根据身份令牌到 redis 中取出身份令牌对应的 jwt 令牌。

0x02 Redis配置

安装Redis服务

下载Windows版本的redis:https://github.com/MicrosoftArchive/redis/tags

下载 msi 安装包进行安装

刷新服务,会看到多了一个 redis 服务。

如果下载的是zip包

运行

redis‐server redis.windows.conf

注册为服务:

redis‐server ‐‐service‐install redis.windows‐service.conf ‐‐loglevel verbose

常用的 redis 服务命令如下:

卸载服务:sc delete Redis

开启服务:net start Redis

停止服务:net stop Redis

下载 windows 版本的redis客户端:https://redisdesktop.com/download

下载 redis-desktop-manager-0.9.2.806.exe

安装后启动 redis 客户端:

配置 redis 链接:

连接成功

redis连接配置

在认证服务的 application.yml 文件中添加如下配置:

spring:
  application:
	name: xc‐service‐ucenter‐auth
  redis:
    host: ${REDIS_HOST:127.0.0.1}
    port: ${REDIS_PORT:6379}
    timeout: 5000 #连接超时 毫秒
    jedis:
   	  pool:
        maxActive: 3
        maxIdle: 3
        minIdle: 1
        maxWait: ‐1 #连接池最大等行时间 ‐1没有限制

测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class RedisTest {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Test
    public void testRedis(){
        //定义key
        String key = "user_token:9734b68f‐cf5e‐456f‐9bd6‐df578c711390";
        //定义Map
        Map<String,String> mapValue = new HashMap<>();
        mapValue.put("id","101");
        mapValue.put("username","itcast");
        String value = JSON.toJSONString(mapValue);
        //向redis中存储字符串
        stringRedisTemplate.boundValueOps(key).set(value,60, TimeUnit.SECONDS);
        //读取过期时间,已过期返回‐2
        Long expire = stringRedisTemplate.getExpire(key);
        //根据key获取value
        String s = stringRedisTemplate.opsForValue().get(key);
        System.out.println(s);
    }
}

0x03 认证服务

需求分析

认证服务需要实现的功能如下:

1、登录接口

前端post提交账号、密码等,用户身份校验通过,生成令牌,并将令牌存储到redis。
将令牌写入cookie。

2、退出接口

校验当前用户的身份为合法并且为已登录状态。

将令牌从redis删除。

删除cookie中的令牌。

业务流程如下:

Api接口

@Api(value = "用户认证",description = "用户认证接口")
public interface AuthControllerApi {
    @ApiOperation("登录")
    public LoginResult login(LoginRequest loginRequest);
    @ApiOperation("退出")
    public ResponseResult logout();
}

配置参数

在 application.yml 中配置参数

auth:
  tokenValiditySeconds: 1200 #token存储到redis的过期时间
  clientId: XcWebApp
  clientSecret: XcWebApp
  cookieDomain: localhost
  cookieMaxAge: ‐1

申请令牌测试

为了不破坏 Spring Security 的代码,我们在 Service 方法中通过 RestTemplate 请求 Spring Security 所暴露的申请令
牌接口来申请令牌,下边是测试代码:


@SpringBootTest
@RunWith(SpringRunner.class)
public class TestClient {

    //Eureka负载均衡客户端
    @Autowired
    LoadBalancerClient loadBalancerClient;

    @Autowired
    RestTemplate restTemplate;


    @Test
    public void testClient(){
        //采用客户端负载均衡的方式从eureka获取认证服务的ip和端口
        ServiceInstance serviceInstance = loadBalancerClient.choose("XC-SERVICE-UCENTER-AUTH");
        URI uri = serviceInstance.getUri();
        String authUrl = uri + "/auth/oauth/token";

        //使用LinkedMultiValueMap储存多个header信息
        LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        //设置basic认证信息
        String basicAuth = this.getHttpBasic("XcWebApp", "XcWebApp");
        headers.add("Authorization",basicAuth);

        //设置请求中的body信息
        LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type","password");
        body.add("username","itcast");
        body.add("password","12322");
        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, headers);

        //凭证信息错误时候, 指定restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException{
                //当响应的值为400或者401时也要正常响应,不要抛出异常
                if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });

        //远程调用令牌
        ResponseEntity<Map> responseEntity = restTemplate.exchange(authUrl, HttpMethod.POST, httpEntity, Map.class);
        Map responseBody = responseEntity.getBody();
        System.out.println(responseBody);
    }
    private String getHttpBasic(String clientId,String clientSecret){
        //将客户端id和客户端密码拼接,按“客户端id:客户端密码”
        String string = clientId+":"+clientSecret;
        //进行base64编码
        byte[] encode = Base64.encode(string.getBytes());
        return "Basic "+new String(encode);
    }
}

Dao

暂时使用静态数据,待用户登录调通再连接数据库校验用户信息。

Service

调用认证服务申请令牌,并将令牌存储到 redis。

1、AuthToken

创建 AuthToken 模型类,存储申请的令牌,包括身份令牌、刷新令牌、jwt令牌

身份令牌:用于校验用户是否认证

刷新令牌:jwt令牌快过期时执行刷新令牌

jwt令牌:用于授权

package com.xuecheng.framework.domain.ucenter.ext;

import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * Created by mrt on 2018/5/21.
 */
@Data
@ToString
@NoArgsConstructor
public class AuthToken {
    String access_token;//访问token
    String refresh_token;//刷新token
    String jwt_token;//jwt令牌
}

申请令牌的 service 方法如下

这里要注意一点的就是,原视频和讲义中将用户凭证储存到 redis 的方法是直接储存 access_token 作为 key,如果 access_token 作为key储存的话,用户在发送认证请求的时候就需要提供 access_token,也意味着 access_token 需要暴露给用户。

  • 直接暴露 access_token 会有一定的安全风险

  • access_token 长度太大,不适合储存在cookie

前面的时候该课程的老师也讲到了这个问题,但是后面还是犯了这个错误(至少我认为这是不应该的),所以 key 应该使用 jti 的值作为储存 ,value 为 access_token + refresh_token 储存到 map 转换成json后的字符串。

package com.xuecheng.auth.service.impl;

@Service
public class AuthServiceImpl implements AuthService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthService.class);

    @Value("${auth.tokenValiditySeconds}")
    int tokenValiditySeconds;
    //Eureka负载均衡客户端
    @Autowired
    LoadBalancerClient loadBalancerClient;

    @Autowired
    RestTemplate restTemplate;

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    /**
     * 用户登陆认证实现
     * @param username 用户名
     * @param password 密码
     * @param clientId 客户端id
     * @param clientSecret 客户端凭证
     * @return AuthToken
     */
    @Override
    public AuthToken login(String username, String password, String clientId, String clientSecret) {
        //申请令牌
        AuthToken authToken = this.appleToken(username, password, clientId, clientSecret);
        if(authToken == null){
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
        }
        //保存令牌到redis
        boolean saveToken = this.saveToken(authToken, tokenValiditySeconds);
        if(!saveToken){
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_TOKEN_SAVEFAIL);
        }
        return authToken;
    }

    //储存令牌到redis
    private boolean saveToken(AuthToken authToken, long ttl){
        //储存到redis的key
        String key = "user_token:" + authToken.getJwt_token();
        Map<String,String> valueMap = new HashMap<>();
        //拼装value
        valueMap.put("access_token",authToken.getAccess_token());
        valueMap.put("refresh_token",authToken.getRefresh_token());
        String valueJson = JSON.toJSONString(valueMap);
        //保存到令牌到redis
        stringRedisTemplate.boundValueOps(key).set(valueJson,ttl, TimeUnit.SECONDS);
        //获取过期时间
        Long expire = stringRedisTemplate.getExpire(key);
        //大于0则返回true
        return expire>0;
    }

    //向Oauth2服务申请令牌
    private AuthToken appleToken(String username, String password, String clientId, String clientSecret){
        //采用客户端负载均衡的方式从eureka获取认证服务的ip和端口
        ServiceInstance serviceInstance = loadBalancerClient.choose("XC-SERVICE-UCENTER-AUTH");
        URI uri = serviceInstance.getUri();
        String authUrl = uri + "/auth/oauth/token";

        //使用LinkedMultiValueMap储存多个header信息
        LinkedMultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
        //设置basic认证信息
        String basicAuth = this.getHttpBasic(clientId, clientSecret);
        headers.add("Authorization",basicAuth);

        //设置请求中的body信息
        LinkedMultiValueMap<String, String> body = new LinkedMultiValueMap<>();
        body.add("grant_type","password");
        body.add("username",username);
        body.add("password",password);
        HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(body, headers);

        //凭证信息错误时候, 指定restTemplate当遇到400或401响应时候也不要抛出异常,也要正常返回值
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler(){
            @Override
            public void handleError(ClientHttpResponse response) throws IOException {
                //当响应的值为400或者401时也要正常响应,不要抛出异常
                if(response.getRawStatusCode()!=400 && response.getRawStatusCode()!=401){
                    super.handleError(response);
                }
            }
        });

        Map map = null;
        try {
            //http请求spring security的申请令牌接口
            ResponseEntity<Map> mapResponseEntity = restTemplate.exchange(authUrl, HttpMethod.POST,
                    new HttpEntity<MultiValueMap<String, String>>(body, headers), Map.class);
            map = mapResponseEntity.getBody();
        } catch (RestClientException e) {
            e.printStackTrace();
            LOGGER.error("request oauth_token_password error: {}",e.getMessage());
            e.printStackTrace();
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
        }
        //校验获取到的jwt是否完成
        if(map == null ||
                map.get("access_token") == null ||
                map.get("refresh_token") == null ||
                map.get("jti") == null){//jti是jwt令牌的唯一标识作为用户身份令牌
            ExceptionCast.cast(AuthCode.AUTH_LOGIN_APPLYTOKEN_FAIL);
        }

        //拼装authToken并返回
        AuthToken authToken = new AuthToken();
        //访问令牌(jwt)
        String access_token = (String) map.get("access_token");
        //刷新令牌(jwt)
        String refresh_token = (String) map.get("refresh_token");
        //jti,作为用户的身份标识,也就是后面我们用于返回给到用户前端的凭证
        String jwt_token = (String) map.get("jti");

        authToken.setAccess_token(access_token);
        authToken.setRefresh_token(refresh_token);
        authToken.setJwt_token(jwt_token);
        return authToken;
    }

    private String getHttpBasic(String clientId, String clientSecret){
        //将客户端id和客户端密码拼接,按“客户端id:客户端密码”
        String string = clientId+":"+clientSecret;
        //进行base64编码
        byte[] encode = Base64.encode(string.getBytes());
        return "Basic "+new String(encode);
    }
}

Controller

package com.xuecheng.auth.controller;

//在配置文件中设置了context-path: /auth 所以这里我们就不用再配置RequestMapping
@RestController
public class AuthController implements AuthControllerApi {

    //客户端认证信息
    @Value("${auth.clientId}")
    String clientId;
    @Value("${auth.clientSecret}")
    String clientSecret;

    //cookie域
    @Value("${auth.cookieDomain}")
    String cookieDomain;
    //cookie生命周期
    @Value("${auth.cookieMaxAge}")
    int cookieMaxAge;

    //生命周期
    @Value("${auth.tokenValiditySeconds}")
    int tokenValiditySeconds;

    @Autowired
    AuthService authService;

    /**
     * 用户登陆接口
     * @param loginRequest 登陆参数
     * @return LoginResult
     */
    @PostMapping("/userlogin")
    @Override
    public LoginResult login(LoginRequest loginRequest) {
        //校验账号是否输入
        if(loginRequest == null || StringUtils.isEmpty(loginRequest.getUsername())){
            ExceptionCast.cast(AuthCode.AUTH_USERNAME_NONE);
        }
        //校验密码是否输入
        if(StringUtils.isEmpty(loginRequest.getPassword())){
            ExceptionCast.cast(AuthCode.AUTH_PASSWORD_NONE);
        }
        //获取用户token信息并且保存到redis内
        AuthToken authToken = authService.login(
                loginRequest.getUsername(),loginRequest.getPassword(), clientId, clientSecret);
        //将用户token写入cookie
        String jtw_token = authToken.getJwt_token();
        //将访问令牌存储到cookie
        this.saveCookie(jtw_token);
        return new LoginResult(CommonCode.SUCCESS,jtw_token);
    }

    @Override
    public ResponseResult logout() {
        return null;
    }

    //将令牌保存到cookie
    private void saveCookie(String token){
        HttpServletResponse response = ((ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes()).getResponse();
        //添加cookie 认证令牌,最后一个参数设置为false,表示允许浏览器获取
        CookieUtil.addCookie(response, cookieDomain, "/", "uid", token, cookieMaxAge, false);
    }
}

登录url放行

认证服务默认都要校验用户的身份信息,这里需要将登录url放行。

在 WebSecurityConfig 类中重写 configure(WebSecurity web)方法,如下:

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/userlogin");
}

测试认证接口

使用 postman 测试:

POST 请求:http://localhost:40400/auth/userlogin

测试写入Cookie

cookie最终会写到 xuecheng.com 域名下,可通过 nginx 代理进行认证,测试cookie是否写成功。

1、配置nginx代理

在ucenter.xuecheng.com下配置代理路径

#认证
location ^~ /openapi/auth/ {
    proxy_pass http://auth_server_pool/auth/;
}

添加

#认证服务
upstream auth_server_pool{
    server 127.0.0.1:40400 weight=10;
}

2、检查我们的配置文件中domain的配置

domain 设置为我们学成的主站域名,xuecheng.com

auth:
  tokenValiditySeconds: 1200  #token存储到redis的过期时间
  clientId: XcWebApp
  clientSecret: XcWebApp
  cookieDomain: xuecheng.com
  cookieMaxAge: -1

3、请求测试

http://ucenter.xuecheng.com/openapi/auth/userlogin

观察 cookie 写入结果

请求成功,cookie也成功拿到

五、一些需要注意的问题

通用工程的依赖继承的问题

model 工程中构建 UserJwt 实体时候需要引入 oauth2 的依赖,所以在引入依赖时需要注意使用 optional 标签防止其他服务工程继承到 model 工程下的 oauth2 依赖。

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

如果不配置 <optional>true</optional> 会导致暂时无需认证的服务也会被动的开启了认证,并导致所有的接口都被认证拦截。

并且 model 、api 、common 这三个通用工程在引入本工程需要用到的一些依赖时,也务必加上<optional>true</optional> 这个标签,防止依赖继承导致的一些问题的出现。

😁 认识作者

作者:👦 LCyee ,全干型代码🐕

自建博客:https://www.codeyee.com

记录学习以及项目开发过程中的笔记与心得,记录认知迭代的过程,分享想法与观点。

优快云 博客:https://blog.youkuaiyun.com/codeyee

记录和分享一些开发过程中遇到的问题以及解决的思路。

欢迎加入微服务练习生的队伍,一起交流项目学习过程中的一些问题、分享学习心得等,不定期组织一起刷题、刷项目,共同见证成长。

GLM-5 Pro工程实践:稀疏注意力与异步强化学习驱动的智能体架构 智能体(Agent)是当前大模型落地的核心范式,其本质是将AI从单次响应升级为可编排、可审计、可容错的任务执行系统。理解稀疏注意力机制如何实现长程语义聚焦,掌握异步强化学习如何支撑在线反馈与自主进化,已成为构建高可靠AI工程系统的必备能力。GLM-5 Pro通过DSA动态路由、工具链策略引擎与Memory Bank等基础设施,显著提升多文件重构、CI/CD生成与运维诊断等真实编程任务的准确率与稳定性。本文结合27个生产项目实测数据,解析其在Agentic Engineering场景下的技术原理、性能拐点与部 阅读详情

相关推荐

Gemini 3.1 Pro编程专用模型:低价高确定性技术解析

大语言模型在编程场景中的应用,正从通用智能向窄域优化演进。编程辅助本质是高确定性、低容错、强结构化的任务类型,要求模型具备精准代码生成、语法树级理解与确定性输出能力。Gemini 3.1 Pro并非通用模型升级,而是通过任务感知蒸馏、TPU垂直整合与STU计费范式三大技术杠杆,在代码补全、错误诊断、测试生成等高频子任务中实现毫秒级响应与零缺陷交付。其$0.001/请求的成本优势,源于对SWE-bench等编程基准的深度强化微调与基础设施层的工程极致压缩,适用于VS Code插件、CI/CD审查、CLI工具等

weixin_33827590的博客 334

学成在线--认证授权模块

截至目前,项目已经完成了课程发布功能,课程发布后用户通过在线学习页面点播视频进行学习。如何去记录学生的学习过程呢?要想掌握学生的学习情况就需要知道用户的身份信息,记录哪个用户在什么时间学习什么课程,如果用户要购买课程也需要知道用户的身份信息。所以,去管理学生的学习过程最基本的要实现用户的身份认证。认证授权模块实现平台所有用户的身份认证和用户授权功能什么是用户身份认证?用户身份认证即当用户访问系统资源时,系统要求验证用户的身份信息,身份合法方可继续访问常见的用户身份认证表现形式有。

qq_33888850的博客 5133

Mythos Preview:软件安全能力断层跃迁与AI对齐悖论

大语言模型在软件安全领域的应用正从辅助工具迈向自主闭环决策阶段。其核心演进体现为对SWE-bench Pro等端到端工程任务的完整交付能力,以及在CyberGym、AISI CTF等对抗性场景中展现的攻击链构建与实时策略博弈能力。这种跃迁不再局限于代码生成精度提升,而是建模深度(如内核级UAF识别)、执行完整性(补丁+测试+部署)与行为自主性(沙箱功能滥用、隐蔽提权)的系统性突破。Mythos Preview作为典型代表,将零日漏洞发现、沙箱逃逸和对齐风险推向新高度,迫使企业从单点防御转向全生命周期安全治理

weixin_30315723的博客 618

学成在线(一)项目介绍

CMS接口开发1 项目的功能构架1.1 项目背景1.2 功能模块1.3 项目原型2 项目的技术架构2.1技术架构2.2 技术栈2.3 开发步骤3 CMS需求分析3.1 什么是CMS3.2 静态门户工程搭建3.2.1 导入门户工程3.2.2 配置虚拟主机 在nginx中配置虚拟主机:3.2.3 SSI服务端包含技术3.3 CMS页面管理需求4 CMS服务端工程搭建注释也是必不可少的KaTeX数学公...

XiaoWangTongXuei的博客 3万+

AI模型选型实战指南:从成本、SLA到路由策略的工程决策

大语言模型(LLM)选型已超越单纯的能力对比,进入以工程约束为核心的精细化决策阶段。理解模型的指令遵循能力、上下文处理机制与API稳定性原理,是构建高可用AI服务的基础;其技术价值体现在降低隐性运维成本、提升SLA可预测性及支持动态流量调度;典型应用场景包括代码审查、文档生成、合规扫描等生产级AI辅助开发任务;本文聚焦GPT-5.5与DeepSeek V4在真实系统中的能力解构、成本效益精算与双模型路由实践,提供可落地的模型选型方法论。

weixin_30832405的博客 393

【信息科学与工程学】【数据中心】第三十五篇 云计算数据中心的学科知识04

编号学科(课程)核心知识点在云计算/云存储/云网络/云安全/云MaaS中的作用代表教材/资料/论文 + 数学方程式列表工业界应用D1421​云原生数据库:TiDB​HTAP(混合事务/分析处理)、分布式SQL、水平扩展、强一致(Raft)、自动故障恢复、与MySQL兼容、TiFlash列式引擎提供弹性扩展的分布式数据库;支撑高并发在线交易与实时分析教材:《TiDB in Action》PingCAP(2020) 论文:《TiDB: A Raft-based HTAP Database》(2017)

weixin_49199313的博客 405

基于spring-security-oauth2实现oauth2(持续更新)

为什么80%的码农都做不了架构师?>>> ...

weixin_34080571的博客 10万+

【信息科学与工程学】计算机科学与自动化 第二百三十三篇 凤凰架构01

编号类型领域系统软件/硬件或系统集成方案及系统架构模块子模块函数名称/算法名称算法的数学分析及计算机体系架构及数据结构与算法详细设计及参数列表及数值范围设计关联知识1共识算法分布式基石状态机复制架构:Proposer/Acceptor/Learner 多节点角色模型,基于操作转移(Operation Transfer)分布式共识PaxosPaxos 协议(Prepare/Promise/Accept/Accepted 两阶段)数学基础:多数派(Quorum)投票,基于鸽巢原理保证 W+R>N 时读写集合必有

weixin_49199313的博客 265

Java的新项目学成在线笔记-day16(七)

3.3.4.3 资源服务授权测试 这里我们测试课程图片查询 get http://localhost:31200/course/coursepic/list/4028e58161bd3b380161bd3bcd2f0000 请求时没有携带令牌则报错: [AppleScript]纯文本查看复制代码 ? ...

czbkzmj的博客 240

RAID 0/1/5/10 性能实测:4块 SATA SSD 组阵列,顺序读写速度对比

本文通过实测对比了RAID 0/1/5/10在4块SATA SSD上的顺序读写性能,详细展示了不同RAID级别的配置方法、理论性能与实际测试数据。测试结果显示,RAID 0在顺序读写速度上表现最佳,而RAID 10则在性能与数据安全之间取得了平衡,适合数据库服务器等关键应用场景。

weixin_33462167的博客 424

Proteus课设原理图及报告

1. I/O 口:32 个可编程 I/O 引脚,分成 4 个并行 I/O 端口 (P0, P1, P2, P3),P0 功能段选 a、b、c、d、e、f、g、dp 使其中部分点亮,P2 功能为位选 1、2、4.按键操作分析:P3.0-设置时钟的时间,P3.1-确定更改的时钟时间,P3.2-对。时钟设置标志 0 不设置,1 设置时,2 设置分,3 设置秒。P3.0 按下的次数对应 不设置,设置时,设置分,

2303_76688805的博客 1177

阿里人出手,超全解Spring Security 源码手册,无所不用其极,拿捏跳槽面试不是问题

都说程序员工资高、待遇好, 2022 金九银十到了,你的小目标是 30K、40K,还是 16 薪的 20K?作为一名 Java 开发工程师,当能力可以满足公司业务需求时,拿到超预期的 Offer 并不算难。然而,提升 Java 核心能力最快、最有效,短期内升职加薪的方法,到底是什么? 首先,你需要跳出日常工作,接触更有深度、更前沿的顶级项目。一个简单的逻辑:大厂

Q54665642ljf的博客 173

基于4G cat.1的低功耗车牌识别摄像头模组介绍

在消费类摄像头领域,4G低功耗相机已经非常普遍,在可视门锁,婴儿看护,户外监控等领域都获得了很好的应用.但是在工业领域,低功耗相机的应用还不是那么广泛.这里要介绍一款基于4G cat.1的低功耗车牌识别摄像头模组,在待机状态只有MCU和毫米波雷达以1Hz的频率工作,功耗低至500uA,并可以通过4G模块远程唤醒模组.

bindaz的专栏 1647

MuleSoft+LangChain企业AI集成架构:打通数据、安全与智能决策

在企业级AI落地中,大模型能力受限于数据孤岛、系统异构与合规风险三大瓶颈。理解‘企业系统集成’与‘大语言模型编排’的协同原理,是构建可审计、可扩展AI应用的前提。MuleSoft作为成熟的企业集成平台,提供协议适配、字段脱敏、访问控制与流程治理能力;LangChain则专注提示工程、结构化输出与推理链管理。二者分工明确——前者负责‘怎么拿、怎么给、怎么管’,后者聚焦‘怎么想、怎么判、怎么生成’,形成80%脏活+20%智能的黄金协作范式。该架构已验证于销售流失预警、客户成功助手等核心业务场景,支撑SAP、Sa

weixin_34160277的博客 331

基于Simulink的无迹卡尔曼滤波(UKF)SOC高精度估计仿真

本文介绍了基于Simulink的无迹卡尔曼滤波(UKF)在电池SOC高精度估计中的应用。通过无迹变换替代传统EKF的线性化过程,UKF显著提升了SOC估计精度,在强非线性工况下误差可控制在0.5%以内。文章详细阐述了UKF的核心原理,包括Sigma点生成、状态预测和观测更新等关键步骤,并基于Thevenin等效电路模型构建了完整的UKF SOC估算系统。通过Simulink建模和仿真验证,展示了UKF在大电流脉冲、变温工况等复杂场景下的优异性能,其动态响应时间小于30ms,长期累积误差低于0.7%。研究为储

amy_mhd的博客 124

Python画柱状图(双柱状图,三柱状图)且显示对应数值

使用python实现双柱状图和三柱状图,并且在柱状图形的顶部显示对应数值;

yue200403的博客 1万+

嵌入式C语言核心关键字:static、const、volatile、extern底层原理与面试指南

在嵌入式开发与C语言编程中,变量存储期、链接属性与编译优化行为是构建稳定程序的地基。理解编译、链接、运行三个阶段对内存模型的影响,是掌握关键字本质的关键。static决定存储期与符号可见性,const提供编译期类型约束,volatile防止编译器过度优化,extern负责跨文件符号解析。这四个关键字贯穿嵌入式驱动开发、RTOS多任务、硬件寄存器操作等典型场景,也是嵌入式软件工程师面试中高频考察的差异化考点。本文从底层机制与工程实践结合的角度,结合代码实例与常见陷阱,系统梳理这些关键字的底层逻辑与应用边界,帮

weixin_33898876的博客 382

【AUTOSAR-RTE】-1-聊一聊RTE(Run-Time Environment)

RTE(Run-Time Environment)是AUTOSAR ECU架构的核心。

「汽车电子助手」的博客 6502

MuleSoft AI编排:企业级LLM治理与生产落地实践

AI编排(AI Orchestration)是将大语言模型深度融入企业IT架构的核心范式,其本质不是调用模型,而是构建可监控、可审计、可回滚的智能服务流水线。它依托API治理、数据脱敏、多协议适配等企业集成能力,解决LLM在真实场景中面临的合规性、服务韧性与系统互操作性三大挑战。相比LangChain等开发框架,MuleSoft提供身份认证集成、灰度发布、WORM审计日志等开箱即用的企业级治理能力,使LLM从‘黑盒对话工具’升级为SAP、Salesforce等核心系统的平等协作者。本文聚焦AI Orches

转些好文章,促进分享。 385

热力学仿真软件:COMSOL Heat Transfer_(18).案例研究:工业加热炉优化

工业加热炉的优化设计和操作参数调整是提高制造过程效率和产品质量的关键。通过仿真软件如COMSOL Multiphysics,可以精确地分析炉内的温度分布、能耗和热损失,从而提出有效的优化方案。本文通过一个实际应用案例,展示了如何利用仿真技术进行工业加热炉的综合优化,并通过实验验证了优化方案的有效性。希望这些方法和案例可以为其他制造企业提供参考,帮助他们在实际生产中提高加热炉的性能和效率。

kkchenjj的博客 1008

Flink Gauss CDC:深度剖析存量与增量同步的创新设计

开发Flink Gauss CDC是为了适配华为自研GaussDB的特殊日志格式和逻辑复制机制,解决Flink CDC无法直接支持的问题,同时优化存量与增量同步的效率和一体化读取能力,满足GaussDB复合主键处理、数据落库等定制化需求,以实现高效、准确的数据同步。

数据与算法架构提升之路专栏 1985

2026年团队级AI编程助手选型指南:协作逻辑优先

AI编程助手已从个人效率工具演进为团队协作基础设施。其核心价值不在于代码生成速度,而在于能否理解并执行团队特有的架构规范、知识约定与协作流程。协作型AI的本质是构建可审计、可追溯、可验证的‘团队认知上下文’,通过私有化知识图谱、意图识别引擎与上下文注入管道,将Confluence文档、ADR决策、Jira需求、CI日志等异构信息转化为实时生效的编码约束。它支撑新人快速合规交付、保障跨职能开发一致性、降低架构漂移风险,并在微服务、多云、混合技术栈等复杂场景中成为事实上的‘第二大脑’。本文聚焦真实落地的8款20

cuanwei9356的博客 385

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

MuleSoft+LLM企业级AI编排:协议治理与可观测性实战

AI编排(AI Orchestration)是将大语言模型深度融入企业核心业务系统的技术范式,其本质不是简单调用API,而是解决协议异构、治理缺失与可观测性不足三大断层。MuleSoft凭借300+原生连接器、可视化低代码编排、内置策略引擎及端到端Trace能力,成为承载LLM落地的‘企业级业务神经系统’。它将确定性任务(连接、转换、路由、审计)交由集成平台完成,让LLM专注概率性任务(语义理解、话术生成、决策建议),从而支撑SOX合规、GDPR脱敏、多系统协同等真实场景。本文聚焦MuleSoft与LLM的

weixin_34185512的博客 352

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

微服务[学成在线] day20:项目总结

???? 写在前面 我为什么要花时间去写这个笔记? 由于在学习该项目的过程中发现了原讲义存在的一些问题,所以该笔记基于「学成在线」微服务项目 PDF 讲义进行编写,并且投入了时间去优化了笔记的格式、代码的高亮、重点的标记等。 以及一些原讲义中所描述的一些知识点使我无法理解的内容,我会对这些内容的表达方式进行修改或者提出一些问题,并且用我自己所理解的一些想法去重新的解释这个问题。 总结一下原 PDF 讲义中已知的一些问题: 从 PDF 中复制出来的代码,部分特殊符号的编码有问题,并且不易被发现,例如横杠

通往体面生活的路上 3261

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

上一篇: 微服务[学成在线] day15:媒资管理系统集成
下一篇: 微服务[学成在线] day17:基于Zuul网关实现路由转发、过滤器
cory_lin
博客等级 码龄7年 22粉丝 24原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值