Ai项目:

项目全局使用Kni4j调试工具

首先Auth模块:

微信小程序登录

三个端:小程序端,开发者服务器端,微信接口服务端

我们首先通过wx.login()(微信平台集成直接调用即可)获取Code

拿到code之后通过wx.request()发送code到你的开发者服务器后端,后端

发送appid+appsecret+code到微信接口服务,微信接口服务验证成功后

微信将返回以下信息:

{
  "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",  // 用户唯一标识(微信开放平台下唯一,openid 是由微信官方提供的)
  "session_key": "tiihtNczf5v6AKRyjwEUhQ==", // 会话密钥(用于解密用户敏感信息)
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" // 当用户绑定了微信开放平台账号时返回
}

我们将openid 往数据库里一保存,生成id,然后根据主键id也好,根据openid也好,生成token返回前端,然后前端把token存入storage,然后用户在活动中向后端发送请求时,必须携带此token即可(后端可以校验),成功后返回业务数据就行了!

微信小程序登录代码:

import $http from './utils/request.js'
import { token } from './api/auth.js'
import Storage from './utils/storage.js'
App({
  $http,
  onLaunch() {   //项目启动默认会执行
    console.log('小程序启动')
    // 展示本地存储能力
    const logs = Storage.getLogs() || []
    logs.unshift(Date.now())
    Storage.setLogs(logs)

    // 登录
    wx.login({  //调用微信提供的wx.login
      success: async res => {
        console.log('wx.login 成功,获取到 code:', res.code)//从res中拿到code
        // 发送 res.code 到后台换取 openId, sessionKey, unionId
        try {
          let loginRes = await token(res.code);//拿到code之后向本地发起请求
          console.log('token 请求响应:', loginRes)//后端返回loginRes
          if(loginRes && loginRes.code === 200){
            Storage.setToken(loginRes.data.tokenName, loginRes.data.tokenValue)//把后端返回的token信息保存到Storage,后期请求都要携带
            console.log('token 存储成功')
          } else {
            console.error('token 请求失败:', loginRes)
          }
        } catch (error) {
          console.error('token 请求异常:', error)
        }
      },
      fail: err => {
        console.error('wx.login 失败:', err)
      }
    })
  },
  globalData: {
    userInfo: null
  }
})

由于微信在获取token的时候要用get请求,get请求要跟参数必须用?加&拼接

{
  "openid": "o6_bmjrPTlm6_2sgVt7hMZOPfL2M",  // 用户唯一标识(微信开放平台下唯一,openid 是由微信官方提供的)
  "session_key": "tiihtNczf5v6AKRyjwEUhQ==", // 会话密钥(用于解密用户敏感信息)
  "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL" // 当用户绑定了微信开放平台账号时返回
}

拿到微信返回的数据后可以进行持久化,然后颁发token,这里用的是Sa-toekn

官网:https://sa-token.cc/doc.html#/up/disable

sa-token:
  # token 名称(同时也是 cookie 名称)
  token-name: aimin-auth-token
  # token 有效期(单位:秒) 默认30天,-1 代表永久有效
  timeout: 2592000
  # token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
  active-timeout: -1
  # 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
  is-concurrent: true
  # 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
  is-share: false
  # token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
  token-style: uuid
  # 是否输出操作日志
  is-log: true

配置类:
@Configuration
public class SaTokenConfigure {

    /**
     * 注册 [Sa-Token全局过滤器]
     *
     */
    @Bean
    public SaServletFilter getSaServletFilter() {
        return new SaServletFilter()
                // 指定 拦截路由 与 放行路由
                .addInclude("/**")
//                .addExclude("/test/**")

                .addExclude("/public/wx/token")
                .addExclude("/test/**")
                .addExclude("/doc.html/**")
                // 认证函数: 每次请求执行
                .setAuth(obj -> {
                    System.out.println("---------- 进入Sa-Token全局认证 -----------");

                    // 登录认证 -- 拦截所有路由,并排除/user/doLogin 用于开放登录
                    SaRouter.match("/**", "/public/wx/token", () -> StpUtil.checkLogin());

                    // 更多拦截处理方式,请参考“路由拦截式鉴权”章节 */
                })
                // 异常处理函数:每次认证函数发生异常时执行此函数
                .setError(e -> {
                    System.out.println("---------- 进入Sa-Token异常处理 -----------");
                    return SaResult.error(e.getMessage());
                })
                // 前置函数:在每次认证函数之前执行(BeforeAuth 不受 includeList 与 excludeList 的限制,所有请求都会进入)
                .setBeforeAuth(r -> {
                    // ---------- 设置一些安全响应头 ----------处理跨域问题
                    SaHolder.getResponse()
                            // 服务器名称
                            .setServer("sa-server")
                            // 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以
                            .setHeader("X-Frame-Options", "SAMEORIGIN")
                            // 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面
                            .setHeader("X-XSS-Protection", "1; mode=block")
                            // 禁用浏览器内容嗅探
                            .setHeader("X-Content-Type-Options", "nosniff")
                    ;
                })
                ;
    }

}

这里我们只需要放行/public/wx/token微信登录接口,登录成功后颁发token,

@RestController
@RequestMapping("/public/wx")
@RequiredArgsConstructor
public class Wxcontroller {
    private final WxService wxService;
    @RequestMapping("/token")
//    微信发送过来一个code
    public Result token(String code)
    {
        Jscode2SessionResult jscode2SessionResult = wxService.wxLogin(code);
//        假设数据库存储成功拿到id代表登录成功
        StpUtil.login(1);//为用户分配一个 token 并建立会话
//        获取token信息
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        String tokenName = tokenInfo.getTokenName();
        String tokenValue = tokenInfo.getTokenValue();
        return   Result.success(tokenInfo);
    }

SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
String tokenName = tokenInfo.getTokenName();
String tokenValue = tokenInfo.getTokenValue();

这一部分,拿到name和值就可以

 @GetMapping("/check")
    public  String userJoin2() {
        try {
            StpUtil.checkLogin();
        } catch (Exception e) {
            return "err:请登录!";
        }
        return "success";
    }

这是测试代码,Sa-token回去coookie,header等去找token,找到后校验,如果不通过则拦截

测试用例:

 public Result token(String code)
    {
        Jscode2SessionResult jscode2SessionResult = wxService.wxLogin(code);
//        假设数据库存储成功拿到id代表登录成功
        StpUtil.login(1);//为用户分配一个 token 并建立会话
//        获取token信息
        SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
        String tokenName = tokenInfo.getTokenName();
        String tokenValue = tokenInfo.getTokenValue();
        return   Result.success(tokenInfo);

这个地方StpUtil的getTokenInfo是自动生成的,tokenInfo里面包含TokenName和TokenValue

AI模块:

首先打开SpringAi官网:https://docs.spring.io/spring-ai/reference/api/chat/deepseek-chat.html

<!--把这个文件放入之后用SpringAi就不用加版本号了,它本身不下载任何东西-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

之后我们如果选择deepseek模型:

 <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-starter-model-deepseek</artifactId>
        </dependency>

这里不用加版本号

接着我们去配置一下秘钥:

  ai:
    deepseek:
      api-key: *******************
      chat:
        options:
          model: deepseek-chat

官方演示Controller:

@RestController
public class ChatController {

    private final DeepSeekChatModel chatModel;

    @Autowired
    public ChatController(DeepSeekChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @GetMapping("/ai/generate")
    public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", chatModel.call(message));
    }

    @GetMapping("/ai/generateStream")
	public Flux<ChatResponse> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        var prompt = new Prompt(new UserMessage(message));
        return chatModel.stream(prompt);
    }
}

一个是流式输出,一个是一问一答

其中Prompt是提示词

流式输出:

//    deepseek流式输出
    @GetMapping("/ai/generateStream2")
    public String deepSeekReasonerExample() {
        DeepSeekChatOptions promptOptions = DeepSeekChatOptions.builder()
                .model(DeepSeekApi.ChatModel.DEEPSEEK_REASONER.getValue())
                .build();
        Prompt prompt = new Prompt("用中文回复,9.11 and 9.8, which is greater?", promptOptions);
        ChatResponse response = chatModel.call(prompt);

        // Get the CoT content generated by deepseek-reasoner, only available when using deepseek-reasoner model
        DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) response.getResult().getOutput();
        String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
        String text = deepSeekAssistantMessage.getText();
        return text ;
    }

如果仅仅使用对话等一些简单的功能,那么private final DeepSeekChatModel chatModel;

够了,但是如果要实现对话,会话记忆等高级功能,那么就需要用

这个API可以声明多个模型,并且可以实现会话记忆功能

官方代码:


ChatMemory chatMemory = ... // Initialize your chat memory store
VectorStore vectorStore = ... // Initialize your vector store

var chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(
        MessageChatMemoryAdvisor.builder(chatMemory).build(), // chat-memory advisor
        QuestionAnswerAdvisor.builder(vectorStore).build()    // RAG advisor
    )
    .build();

var conversationId = "678";

String response = this.chatClient.prompt()
    // Set advisor parameters at runtime
    .advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, conversationId))
    .user(userText)
    .call()
	.content();

再次我们部署ollama。下载好模型后:

直接启动,命令 ollama run 模型名称

我们在配置类里声明这个moudle:

@Configuration
@RequiredArgsConstructor
public class ChatClientConfig {
    //  创建一个 DeepSeekChatModel 对象,并注入到容器中
//        @Bean
//        public ChatClient deepSeekChatClient(DeepSeekChatModel chatModel) {
//            return ChatClient.create(chatModel);
//        }
    private final ChatStorageMemory chatMemory;
    @Bean
    public ChatClient OllamadeepSeekChatClient(OllamaChatModel chatModel) {
        //基于内存实现聊天记忆
//        ChatMemory ChatMemoryemory = MessageWindowChatMemory.builder().build();
        MessageChatMemoryAdvisor messageChatMemoryAdvisor = MessageChatMemoryAdvisor.builder(chatMemory).build();
        ChatClient chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(messageChatMemoryAdvisor)
                .build();
        return chatClient;
      }

    }

对应controller:

@GetMapping("/ai")
    public String generate(@RequestParam(value = "message") String message) {
    //得到用户ID,这里假设为1
        return  chatClient.prompt(message).advisors(advisor -> advisor.param(ChatMemory.CONVERSATION_ID, 1))
                .call()//调用模型
                .content();//返回内容
//        return Map.of("generation", chatModel.call(message));
    }

这样就可以实现基于内存的聊天记忆功能!

集成Mongodb实现聊天记忆:

在common中创建mongo模块

和ds一样,集成之后,配置一下mongo的配置信息

在ai模块中创建msg实体类:

创建一个mapper,这里叫repository(仓库)

mongo中MongoRepository已经帮我们实现了基础的增删改查

对比:

这里用mongoTemplate去查询mongo里查询:

这里实现重写聊天记忆的方法,重写ChatMemory

然后我们到ChatClient中,

将chatMemory放到ChatClient中就可以实现聊天记忆

COMMON模块:

该模块主要放一些公共的组件,比如数据源啊等等

我们创建一个common模块只保留pom文件,类似aimin-parent,然后我们在此创建子模块,common的父项目依旧是aimin-parent,子模块的父项目是common

因为我们要做全局数据源统一配置,在auth等模块中引入ds依赖,所以我们在auth等模块(需要数据源配置的模块)引入bootstrap依赖:

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

然后我们在其配置文件中配置,首先将原来的application.yml改为bootstrap.yml,然后再配置

spring:
  config:
    import:
      - optional:classpath:application.yml

spring.config.import 的作用

1. 配置文件导入机制

  • 这是 Spring Boot 2.4+ 引入的新配置导入机制
  • 用于在运行时动态导入额外的配置文件
  • optional: – 表示这个配置文件是可选的,如果文件不存在不会报错
  • classpath: – 表示从 classpath(类路径)中查找文件
  • application.yml – 要导入的配置文件名

3. 为什么需要这个配置

在你的项目中:

  • 主配置文件是 bootstrap.yml
  • 但你可能还有其他配置在 application.yml 中
  • 通过这个配置,Spring Boot 会同时加载两个配置文件

4. 配置文件加载顺序

Spring Boot 会按以下顺序加载配置:

  1. bootstrap.yml (先加载)
  2. application.yml (后加载,会覆盖前面的配置)

由于common中的ds模块不需要启动,自然没有启动类,这个ds模块被谁注入,谁就可以将ds中的一些bean给配置在自己模块中的ioc容器中,那么怎么才能实现这个功能呢,

写入到org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件中jiuok,固定写法

将需要声明的bean在ds中org.springframework.boot.autoconfigure.AutoConfiguration.imports声明出来

MybatisPlusjoinhttps://mybatis-plus-join.github.io/pages/quickstart/quickstart.html

动态代理:

主要是通过Proxy.newProxyInstance来创建代理对象,里面有三个参数,一个是类加载器(固定),一个是

被代理的接口,比如UserService,还有一个就是被代理的类

可以看到newProxyInstance有三个参数,

InvocationHandler是个接口,所以需要实现一下

具体实现:只需要实现invoke方法

Satoken-Redis集成方案:

在配置好redis模块信息后,直接在

中添加

<dependency>
			<groupId>cn.dev33</groupId>
			<artifactId>sa-token-redis-jackson</artifactId>
		</dependency>

就代表已经将satoken存储方案改为redis

他会默认使用redis里面的所有默认配置

基于gateway:实现网关鉴权

在网关中添加以下依赖:


<!-- Sa-Token 权限认证(Reactor响应式集成), 在线文档:https://sa-token.cc -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-reactor-spring-boot-starter</artifactId>
    <version>1.44.0</version>
</dependency>

<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency>
    <groupId>cn.dev33</groupId>
    <artifactId>sa-token-redis-jackson</artifactId>
    <version>1.44.0</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

这里呢,我们把satoken给清理出来了

我们把SaTokenConfigure配置文件放到satoken starter里面

注意:

config是和cloud同一级,不用弄错,不然连接不上redis的那个starter

实现多账号登录:

如果tokenName一样,那么管理端就会有风险接口数据

那在管理端存token的时候tokenName改为admin-auth-token就好了

多账号认证登录用到了策略模式

用到了策略模式+简单工厂。

  • 策略接口:LoginCheckStrategy
  • 具体策略:AdminPathStrategy、PublicPathStrategy、UserPathStrategy
  • 策略选择器(简单工厂):StrategyFactory#getStrategy(String) 按路径用 AntPathMatcher 从 LinkedHashMap<String, LoginCheckStrategy> 中挑选策略,未命中返回默认 PublicPathStrategy。

建议优化:

  • 保证匹配优先级:已用 LinkedHashMap,高优先级规则放前面即可。
  • 将策略改为 Spring Bean(如有依赖注入需求),工厂里从容器获取。
  • 支持模式分组:如 /aimin-auth/test/** 统一走 UserPathStrategy,减少单接口配置。

实现截图:


首先我们在satoken模块定义了SaTokenAutoConfiguration,通过 @Configuration + @Bean 把 StpUtilConfig 作为 Spring Bean 放入容器。

初始化钩子

@Bean(initMethod = “init”) 会在 Bean 创建后自动调用 StpUtilConfig#init(),用于完成 Sa-Token 相关的初始化(例如自定义 StpUtil、注册多端登录逻辑、全局拦截器等)。

便于模块复用

放在公共模块 aimin-satken 中,被任意业务模块依赖后即可“开箱即用”,无需在每个业务模块重复配置。

一句话:集中化、自动化地完成 Sa-Token 的初始化与配置注入。

在StpUtilConfig中我们配置了setTokenName,用户端和管理端的具体信息

然后我们在

配置了用于校验用户端和管理端的stpLogic,用于校验不同身份的用户

然后我们来到网关,

这里我们配置了一个过滤器,通过String requestPath = SaHolder.getRequest().getRequestPath();去匹配不同路径的请求从而让不通的校验器去校验

我们写一个接口

让userPathStrategy等去实现它(多态的实现),

然后我们用StrategyFactory中getStrategy去校验

这里设置好哪种路径请求用哪个校验器

缓存实现:

我们使用的是caffeine,这个caffeine作为一级缓存,

我们设置了它的配置信息过期时间为20s,我们在pom文件中导入redis的依赖以及配置信息

那么他就会默认走二级缓存

博客内容均系原创,未经允许严禁转载!
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇