首页 > 其他分享 >最详细的Keycloak教程(建议收藏):Keycloak实现手机号、验证码登陆——(三)基于springboot&keycloak+vue的前后端分离项目

最详细的Keycloak教程(建议收藏):Keycloak实现手机号、验证码登陆——(三)基于springboot&keycloak+vue的前后端分离项目

时间:2024-03-17 22:00:04浏览次数:25  
标签:vue return springboot register Redis 验证码 token login Keycloak

在前面两节分别介绍了 Keycloak的下载与使用keycloak与springboot的集成
接下来第三节让我们一步步的去完成一个简单的前后端分离项目,并且可以扩展实现sso。

一、简介

本文将介绍如何使用Spring Boot、Keycloak和Vue构建一个具有前后端分离架构的Web应用程序。通过将前端与后端完全独立开发和部署,我们可以实现更高效的团队协作和灵活的技术选型。Spring Boot提供了一个稳定可靠的后台框架,Keycloak提供了身份验证和授权的解决方案,而Vue作为一种灵活易用的前端框架,使我们能够快速开发出优秀的用户界面。

二、keycloak配置

首先回顾一下上一节提到的访问类型

public: 适用于客户端应用,如前端web系统,包括采用vue、react实现的前端项目等。不需要秘钥访问。
confidential: 适用于服务端应用,比如需要浏览器登录以及需要通过密钥获取access token的web系统。需要秘钥访问。
bearer-only: 适用于服务端应用,只允许使用bearer token请接口,项目里的权限是针对接口做校验,请求没有带上token就会返回401。需要秘钥访问。

在这里我们新创一个keycloak客户端,因为我们的项目是前后端分离的,所以此客户端的访问类型为 bearer-only
create-client

三、springboot配置

创建好一个新的springboot项目之后,配置yml文件,下面是我的配置,仅供大家参考。

keycloak:
  realm: springboot
  resource: sso-project-backend
  auth-server-url: http://localhost:8080/
  ssl-required: NONE
  bearer-only: true
  credentials:
    secret: nCTbFSEBZDTME14MMf0LBxyzSmmPzbee
  cors: true #允许跨域
  #  use-resource-role-mappings: false
  #鉴权
  security-constraints:
    #需要用户权限的接口
    - auth-roles:
        - user
        - admin
      security-collections:
        - name: user-role
        - patterns:
            - /api/v1/*
    #放行接口
    - auth-roles:
      security-collections:
        - name: any
        - patterns:
            - /api/v1/user/login
            - /api/v1/user/register
            - /api/v1/user/register/register-captcha
            - /api/v1/user/login/captcha
            - /api/v1/user/retrieve-pwd/captcha
            - /api/v1/user/getTokenByRefreshToken

里面的相关数据在keycloak客户端的“安装”中,选择json格式,复制粘贴即可。
配置
秘钥我们前两节也讲过了。
在这里插入图片描述

四、代码实现

最简单的实现思路:前端通过调用后端的注册、登录接口,在后端使用api请求keycloak的相关接口去创建、更新、删除用户的信息。

废话不多说,直接上代码。

4.1 后端接口的实现

在controller层创建一个名为LoginController的文件,在里面编写相关的接口,包括注册、登录、获取验证码等等,示例如下:

@RestController
@RequestMapping("/api/v1/user")
public class LoginController {

    @Autowired
    private LoginService loginService;

    @SneakyThrows
    @PostMapping("/register")
    public Response userRegister(@RequestBody Register register) {
        return Response.status(loginService.doRegister(register));
    }

    @SneakyThrows
    @PostMapping("/login")
    public Response userLogin(@RequestBody Login login) {
        return Response.success(loginService.doLogin(login));
    }

    @SneakyThrows
    @GetMapping("/getTokenByRefreshToken")
    public Response getTokenByRefreshToken(String refreshToken) {
        if (refreshToken == null) {
            throw new AuthException("无权限访问!");
        }
        return Response.success(loginService.getTokenByRefreshToken(refreshToken));
    }

    @SneakyThrows
    @GetMapping("/login/captcha")
    public Response captcha() {
        return Response.success(loginService.captcha());
    }

    @SneakyThrows
    @GetMapping("/register/register-captcha")
    public Response registerPhoneCaptcha(String phoneNumber) {
        return Response.status(loginService.phoneCaptcha(phoneNumber, SmsTypeEnum.USER_REGISTER.getType()));
    }

    @SneakyThrows
    @GetMapping("/retrieve-pwd/captcha")
    public Response forgotPwdCaptcha(String phoneNumber) {
        return Response.status(loginService.phoneCaptcha(phoneNumber, SmsTypeEnum.RETRIEVE_PWD.getType()));
    }

    @SneakyThrows
    @GetMapping("/logout")
    public Response logout(String refreshToken) {
        return Response.success(loginService.logout(refreshToken));
    }
}

在service层编写对应的接口,直接上代码了哈~

public interface LoginService {

    /**
     * 注册
     * @param register
     * @return
     */
    boolean doRegister(Register register);

    /**
     * 登录
     * @param login
     * @return
     */
    KeycloakTokenResponse doLogin(Login login);

    /**
     * 根据刷新token获取token
     * @param refreshToken
     * @return
     */
    KeycloakTokenResponse getTokenByRefreshToken(String refreshToken) throws AuthException;

    /**
     * 退出登录
     * @param refreshToken
     * @return
     */
    boolean logout(String refreshToken);

    /**
     * 验证码
     * @return
     */
    Map<String,String> captcha();

    /**
     * 发送手机验证码
     * @param phoneNumber
     * @param sendType
     * @return
     */
    boolean phoneCaptcha(String phoneNumber,String sendType);

    /**
     * 重置用户登录密码
     * @param resetUserPassword
     * @return
     */
    boolean resetUserPassword(ResetUserPassword resetUserPassword);

}

在实现类里面编写对应的方法,示例代码如下:

4.1.1 注册
@Override
    public boolean doRegister(Register register) {
        try {
            String phoneNumber = register.getPhoneNumber();
            if (StrUtil.isBlank(register.getPassword()) || StrUtil.isBlank(register.getConfirmPassword())) {
                throw new RuntimeException("密码不能为空!");
            }
            if (StrUtil.isBlank(phoneNumber)) {
                throw new RuntimeException("手机号不能为空!");
            }
            if (StrUtil.isBlank(register.getPhoneCaptcha())) {
                throw new RuntimeException("手机验证码不能为空!");
            }
            if (!register.getPassword().equals(register.getConfirmPassword())) {
                throw new RuntimeException("两次密码不一致!");
            }
            String key = SmsTypeEnum.USER_REGISTER.getRedisKey() + phoneNumber;
            //验证码校验
            Object captchaObject = redis.get(key);
            if (captchaObject == null) {
                throw new RuntimeException("验证码已失效!");
            }
            String redisCaptcha = String.valueOf(captchaObject);
            if (!redisCaptcha.equals(register.getPhoneCaptcha())) {
                throw new RuntimeException("验证码错误!请重新输入!");
            }
            //调取admin注册接口
            //注册KC
            Boolean keycloakUser = keycloakAdminUtil.createKeycloakUser(register, KeycloakRegisterUserTypeEnum.LEARNER.getType());
         
            if (keycloakUser) {
                //注册成功删除验证码
                redis.deleteByKey(key);
                
            }
                
            return keycloakUser;
        } catch (RuntimeException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

前端把手机号和验证码、密码等信息传进来之后,先去做校验,然后再调取keycloak createUser方法去创建keycloak用户。
在这里用到了RedisRedis是一个开源的键值对(Key-Value)存储系统,它支持网络、可基于内存、分布式、可选持久性的数据库,并提供多种语言的API。以下是关于Redis的一些详细介绍:

  1. 设计目的:Redis被设计为一个高性能的非关系型数据库,主要用于处理大量数据的高速读写操作。它的数据存储在内存中,这使得其访问速度非常快,每秒可以处理超过10万次的读写操作。
  2. 数据结构:Redis不仅可以存储简单的字符串,还支持多种复杂的数据类型,如列表(list)、集合(set)、有序集合(sorted sets)和哈希(hash)。这些数据结构使得Redis可以满足更多应用场景的需求。
  3. 应用场景:Redis通常用于缓存系统,以减轻后端数据库的压力。例如,在Web应用中,可以将频繁访问的数据缓存在Redis中,从而提高读取速度和系统的响应能力。此外,Redis也常用于实现分布式锁,以保证在分布式环境中的数据一致性。
  4. 性能特点:由于Redis的数据存储在内存中,其读写速度远超传统的基于磁盘的数据库。这使得Redis非常适合需要快速响应的应用,如实时分析、消息队列等。
  5. 持久化机制:虽然Redis是基于内存的,但它提供了持久化机制,可以将内存中的数据定期保存到磁盘中,以防止数据丢失。Redis支持RDB和AOF两种持久化方式。
  6. 支持事务:Redis支持简单的事务功能,可以确保一系列命令的原子性执行。这对于需要保证操作完整性的应用来说非常重要。
  7. 集群支持:Redis支持多种集群方案,可以通过分片(sharding)等方式实现数据的分布式存储,提高系统的可扩展性和容错能力。
  8. 社区和生态:作为一个开源项目,Redis拥有一个活跃的社区,提供了大量的文档和工具,方便开发者使用和维护。同时,许多编程语言都提供了与Redis交互的库,使得集成Redis变得简单便捷。
4.1.2 登录

注册完成之后,需要登录获取token,然后前端拿到这个token来请求后端接口。登录的示例代码如下:

  @Override
    public KeycloakTokenResponse doLogin(Login login) {
        Object redisCaptchaObj = redis.get(login.getCaptchaKey());
        if (ObjectUtil.isEmpty(redisCaptchaObj)) {
            throw new RuntimeException("验证码已过期!");
        }
        String redisCaptcha = String.valueOf(redisCaptchaObj);
        if (!redisCaptcha.equalsIgnoreCase(login.getCaptcha())) {
            throw new RuntimeException("验证码错误,请重试!");
        }
        KeycloakTokenResponse response = token.getTokenByPassword(login.getUsername(), login.getPassword());
        if (StrUtil.isNotEmpty(response.getAccessToken())) {
            userService.saveLoginLog(login.getUsername());
        }
        return response;
    }

这里获取了登录信息,根据用户输入的密码调取keycloak api接口,得到token response信息,返回到前端之后,就可以通过accessToken访问接口了。

前面在yml文件配置了接口鉴权,/api/v1/* 表示/api/v1下的所有接口都需要鉴权,如下图所示:

接口鉴权
下面是不需要token鉴权的接口:
放行接口

4.2 前端代码

前端部分都是简易的代码,提供一个思路,仅供参考。

4.2.1 注册

<template>
  <div>
    <h2>注册</h2>
    <form @submit.prevent="register">
      <div>
        <label for="phone">手机号:</label>
        <input type="text" id="phone" v-model="phone" />
      </div>
      <div>
        <label for="code">验证码:</label>
        <input type="text" id="code" v-model="code" />
        <button type="button" @click="sendCode">发送验证码</button>
      </div>
      <div>
        <label for="password">密码:</label>
        <input type="password" id="password" v-model="password" />
      </div>
      <button type="submit">注册</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      phone: '',
      code: '',
      password: '',
    };
  },
  methods: {
    async register() {
      try {
        const response = await fetch('你的后端接口地址', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ phone: this.phone, code: this.code, password: this.password }),
        });
        const data = await response.json();
        if (data.success) {
          alert('注册成功');
        } else {
          alert('注册失败:' + data.message);
        }
      } catch (error) {
        console.error('注册失败:', error);
      }
    },
    async sendCode() {
      try {
        const response = await fetch('你的发送验证码接口地址', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ phone: this.phone }),
        });
        const data = await response.json();
        if (data.success) {
          alert('验证码已发送');
        } else {
          alert('发送验证码失败:' + data.message);
        }
      } catch (error) {
        console.error('发送验证码失败:', error);
      }
    },
  },
};
</script>

4.2.2登录
<template>
  <div>
    <h2>登录</h2>
    <form @submit.prevent="login">
      <div>
        <label for="phone">手机号:</label>
        <input type="text" id="phone" v-model="phone" />
      </div>
      <div>
        <label for="password">密码:</label>
        <input type="password" id="password" v-model="password" />
      </div>
      <button type="submit">登录</button>
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      phone: '',
      password: '',
      token: '',
    };
  },
  methods: {
    async login() {
      try {
        const response = await fetch('你的后端接口地址', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({ phone: this.phone, password: this.password }),
        });
        const data = await response.json();
        if (data.success) {
          this.token = data.token;
          console.log('Token:', this.token);
          alert('登录成功');
        } else {
          alert('登录失败:' + data.message);
        }
      } catch (error) {
        console.error('登录失败:', error);
      }
    },
  },
};
</script>

登录完成之后,可以把token存储到local或者cookie里面,随便怎么玩,在请求接口的时候带上token即可!

五、结语

这样就实现了一个通过keycloak鉴权的简单前后端分离项目,当springboot版本太高(3.0)的话,也可以集成spring-security来进行操作,不过得手动添加一些config才能正常使用,这个会在后面再出一篇博客来演示springbootV3+keycloak的集成。

长路漫漫,代码作伴!

标签:vue,return,springboot,register,Redis,验证码,token,login,Keycloak
From: https://blog.csdn.net/mltaozi/article/details/134275808

相关文章

  • SpringBoot拦截器
    目录拦截器概念拦截器的作用应用场景SpringBoot中的拦截器实现实现HandlerInterceptor接口注册拦截器到InterceptorRegistry配置拦截器的拦截规则拦截器的执行顺序和生命周期拦截器的执行顺序拦截器的生命周期多个拦截器的执行流程拦截器的性能优化和常见问题拦截器的常见问题和解......
  • Vue — Vue3.0快速掌握
    一.使用create-vue创建项目1.环境条件node版本在16.0以上2.创建vue3.0应用npminitvue@latest//创建npminstall//下载依赖3.项目目录和关键文件1.vite.config.js:项目的配置文件基于vite的配置2.package.接送:项目包文件核心依赖变成了Vue3.X和vite3.main.js:入......
  • ant design vue动态显示隐藏表格列字段,支持记忆功能
    本文档内容下载:动态显示隐藏表格列字段,支持记忆功能.docx.zip:​​https://url37.ctfile.com/f/8850437-1036113839-678952?p=4760​​(访问密码:4760)链接:​​https://caiyun.139.com/m/i?135CdoJGCdpkg​​新版本以及新版本代码生成,会自动增加该功能,无需额外修改。仅......
  • 【前端Vue】Vue3+Pinia小兔鲜电商项目第1篇:认识Vue3,1. Vue3组合式API体验【附代码文
    全套笔记资料代码移步:前往gitee仓库查看感兴趣的小伙伴可以自取哦,欢迎大家点赞转发~全套教程部分目录:部分文件图片:认识Vue31.Vue3组合式API体验通过Counter案例体验Vue3新引入的组合式API<script>exportdefault{data(){return{count:0......
  • Vue.js前端开发零基础教学(一)
    目录第一章 初识Vue.js前言 开发的好处一.前端技术的发展什么是单页Web应用?二.Vue的简介三.Vue的特性四.Vue的版本五.常见的包管理六.安装node环境第一章 初识Vue.js学习目标:了解前端技术的发展了解什么是Vue掌握使用方法掌握Node.js环境的搭建前言......
  • RuoYi-Vue开源项目2-前端登录验证码生成过程分析
    前端登录验证码实现过程生成过程分析生成过程分析验证码的生成过程简单概括为:前端登录页面加载时,向后端发送一个请求,返回验证码图片给前端页面展示前端页面加载触发代码: import{getCodeImg}from"@/api/login"; created(){ this.getCode(); this.......
  • Vue项目简介
    Vue项目的创建: 1)在桌面上创建一个Vue文件夹,打开后在搜索栏中 输入cmd,打开命令窗,输入vueui 调出图形化界面。2)创建一个项目3)点击手动 4)将路由打开router5)vue版本选择为2X,语法检测选择第一项。6)创建好项目之后,直接用vscode打开vue文件夹,以下为所创建的......
  • 基于SpringBoot+Vue实现的二手交易系统
    系统介绍校园二手交易网站是一种专门针对有二手物品交易需求用户的二手交易的网站。它的设计和开发主要是为了满足用户之间的二手物品交易需求,方便大家在线买卖二手物品。近年来,随着互联网技术的发展,人们越来越喜欢在线购物,二手交易也不例外。功能模块图技术选型开发工......
  • Vue3 组件通信方式小结
    也是零零散散用vue3来搞一些前端的页面,每次在组件通信,主要是传数据这块总是忘记,大多无非父传子,子传父等情况,这里再来做一个小结.父传子Props最常见的就是父组件给子组件传递数据,不论是传字符串也好,还是数组,对象,函数等,都可以通过属性传值的方式,子组件......
  • 【开源】SpringBoot框架开发就医保险管理系统
    目录一、摘要1.1项目介绍1.2项目录屏二、功能模块2.1科室档案模块2.2医生档案模块2.3预约挂号模块2.4我的挂号模块三、系统展示四、核心代码4.1用户查询全部医生4.2新增医生4.3查询科室4.4新增号源4.5预约号源五、免责说明一、摘要1.1项目介绍基......