github编辑

Day4-登录页和jwt认证

前端登录页面实现

使用 Vue3+Vite4+VueRouter+ElementPlus+TailwindCss+AnimateCss 来实现。

创建前端项目

确保你拥有 nodejs 环境后进行下面操作!

进入 weblog 目录中创建前端项目

在目录下执行:

npm create vue@latest

执行过程中,会提示你命名新项目,我们命名为 weblog-vue3,以及是否需要开启一些诸如 TypeScript 和测试支持之类的可选功能,这里统一敲击回车键选择 No 即可。当你看到命令行中提示 Done , 表示你已经创建好了 weblog 前端项目。

添加依赖

# 进入项目文件夹
cd weblog-vue3
# 安装项目所需依赖
npm install
# 添加vue-router
npm install vue-router
# 添加TailwindCss
npm install -D tailwindcss postcss autoprefixer
# 添加Flowbite组件库
npm install flowbite
# 添加ElementPlus
npm install element-plus --save
# 添加ElementPlus图标库
npm install @element-plus/icons-vue
# 添加自动导入
npm install -D unplugin-vue-components unplugin-auto-import
# 添加AnimateCss
npm install animate.css --save

进行配置

在添加了上述依赖后,首先打开 src/main.js 文件,将内容导入并添加到 Vue app 实例中, main.js 修改后如下:

alias 是一个用于定义路径别名的配置选项。当你的项目结构变得复杂时,路径别名可以帮助你简化 importrequire 语句中的路径。通过 .. 来指定上一级目录,然后再定位具体路径下。考虑一个大型项目中,我们经常需要这样的引用,当文件层级很深,那么代码可能会像下面这样:

这种相对路径不易于管理和阅读。使用 alias,我们可以将路径简化为:

这样一来,不仅路径更短、更明确,而且移动文件时无需改动 import 路径。

unplugin-vue-componentsunplugin-auto-import 这两款插件是实现 ElementPlus 组件按需导入的插件。

接下来我们将对这两项进行配置。在项目的根目录中,找到 Vite 的配置文件 vite.config.js,编辑它,添加 alias 配置并导入插件,代码如下:

接下来对引入的各个依赖进行配置:

  1. vue-router

src 目录下,创建 /pages 文件夹,此文件夹中统一存放 页面 相关代码,然后 /pages 文件夹下,再创建两个文件夹, 分别是:

  • /admin : 存放管理后台相关页面;

  • /frontend :存放前台展示相关页面,如首页、博客详情页等;

/frontend 文件夹下,创建 index.vue 首页文件,代码如下:

/src 目录下,新建 /router 路由文件夹,用于存放路由相关代码,并在此文件夹下,新建 index.js 文件, 代码如下:

上面代码中,我们将首页组件 index.vue 导入,并且通过定义 routes 数组来统一声明所有的路由,在数组中,我们创建了一个绑定到了根目录 / 的路由地址,指向了首页组件。

<router-view> 是 Vue Router 的一个核心组件,它是一个功能性组件(functional component)。其主要作用是根据当前的路由(URL)动态地渲染对应的组件,相当于是一个“占位符”,它会自动渲染与当前路径匹配的组件。

  • 1、动态渲染组件<router-view> 根据当前的 URL,自动渲染与当前路径匹配的组件。

  • 2、嵌套路由: 在有嵌套路由的情况下,<router-view> 可以用于渲染嵌套的子路由组件。

接下来,我们需要在 app.vue 中添加该组件,用于动态渲染路由对应的组件,再删除 app.vue 中的内容后,添加如下代码:

  1. TailwindCss

之前的依赖安装命令会在项目中安装三个依赖,分别为:

  • tailwindcss:Tailwind CSS 框架本身。

  • postcss:一个用于转换 CSS 的工具。

  • autoprefixer:一个 PostCSS 插件,用于自动添加浏览器供应商前缀到 CSS 规则中,确保跨浏览器的兼容性。

同时我们还安装了 flowbite 组件库。Flowbite 是一个基于 Tailwind CSS 创建的组件库,旨在帮助开发者快速构建现代、响应式的 Web 应用界面。接下来我们需要进行一些配置以使这些内容生效。

在前端项目根目录执行如下命令, 此命令用于生成 tailwind.config.jspostcss.config.js 配置文件。:

生成后,打开 tailwind.config.js 文件,进行如下配置,添加所有模板文件路径,并且引入 flowbite 组件库:

清空 main.css 的初始内容,引入 Tailwind 样式:

构建首页框架与登录页

首页框架构建

清空 src/pages/frontend 文件夹下 index.vue 文件的内容,重写首页,主要是定义了响应式的导航栏,登录按钮加上了 $router.push('/login') 的点击跳转路由动作,该路由会在下面定义登录页时添加到 vue-router 的配置中:

登录页构建

src/pages/admin 文件夹下创建 login.vue 文件。登录页构建了一个左右分栏的页面样式,并添加了动画效果,代码如下:

上面样式可以根据自己需要进行调节美化,左右边栏的动画样式可以参考 AnimateCSSarrow-up-right 网站进行修改, 左侧图片出自 IconFontarrow-up-right ,可以使用其他图片进行替换,或者直接使用原图 developer.png (置于 src/assets 文件夹下):

developer.png

接下来配置一下路由,修改 src\router\index.js 文件,添加上登录页的路由:

数据库连接和JWT登录鉴权

MyBatis Plus和P6Spy

MyBatis Plus (简称 MP) 是一款持久层框架,说白话就是一款操作数据库的框架。它是一个 MyBatis 的增强工具,就像 iPhone手机一般都有个 plus 版本一样,它在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。而 P6Spy 组件则用来实现完整的 SQL 打印,在日志中打印SQL语句,能够及时了解每个操作都具体执行了什么,同时通过打印执行耗时,可以发现一些慢SQL,提前做好优化,不过该组件不适合在生产环境中使用,所以需要在配置文件中进行区分

建立数据库

搭建自己的 mysql 数据库 weblog (此处操作略)。再执行如下建表语句:

通过以上操作在 weblog 中建立了 t_user 表。

整合数据库依赖

在父项目 weblog-springbootpom.xml 文件中,添加依赖:

然后,在 weblog-module-common 模块的 pom.xml 文件中,引入 MP 和 MySQL 依赖:

添加配置

修改 application-dev.yml 配置文件

详细解释一下各个配置的含义和作用:

  • spring.datasource.driver-class-name: 指定数据库驱动类的完整类名,这里使用的是 MySQL 数据库的驱动类。

  • spring.datasource.url: 数据库连接的 URL,指向本地的 MySQL 数据库,端口是 3306,数据库名是 weblog,同时配置了一系列参数,如使用 Unicode 编码、字符编码为 UTF-8、自动重连、不使用 SSL、对零时间进行转换等。

  • spring.datasource.username: 数据库用户名,这里使用的是 root

  • spring.datasource.password: 数据库密码,这里使用的是 123456

数据库链接池这块,我们使用的是 Spring Boot 默认的 HikariCP,它是一个高性能的连接池实现 , 同时,它号称是速度最快的连接池:

  • spring.datasource.hikari.minimum-idle: Hikari 连接池中最小空闲连接数。

  • spring.datasource.hikari.maximum-pool-size: Hikari 连接池中允许的最大连接数。

  • spring.datasource.hikari.auto-commit: 连接是否自动提交事务。

  • spring.datasource.hikari.idle-timeout: 连接在连接池中闲置的最长时间,超过这个时间会被释放。

  • spring.datasource.hikari.pool-name: 连接池的名字。

  • spring.datasource.hikari.max-lifetime: 连接在连接池中的最大存活时间,超过这个时间会被强制关闭。

  • spring.datasource.hikari.connection-timeout: 获取连接的超时时间。

  • spring.datasource.hikari.connection-test-query: 用于测试连接是否可用的 SQL 查询,这里使用的是 SELECT 1,表示执行这个查询来测试连接。

修改 application-prod.yml 配置文件

添加P6Spy配置

weblog-web 模块中的 resources 目录下添加 spy.properties 配置文件:

测试数据库连接

weblog-module-common 模块中创建 config 包和 domain 包,同时在 domain 包下创建 dosmapper 包。在 config 包下,新建一个 MybatisPlusConfig 配置文件:

  • @Configuration : 此注解声明该类为配置类;

  • @MapperScan : 扫描 MP 的 mapper 接口存放位置。

    PS: 数据库相关的代码,我们统一放置在 domain 这个包中

dos 包下,新建 UserDO 类, 字段和数据库中的字段通过转驼峰的形式对应一一对应起来 ,MP 框架会默认通过这种规则将字段光联在一起:

mapper 包中新建 UserMapper 类,在该类中构建一个通过用户名在数据库中查找数据条目的方法:

weblog-web 模块中的单元测试类中,新增一个测试方法,代码如下:

接下来运行该方法,测试数据条目是否成功写入数据库中了,因为我们现在都是在测试环境中运行的,同时可以检查一下配置的 P6Spy 是否生效,看看日志中有没有打印出该条SQL语句。

数据写入
打印SQL

Spring Security 与 JWT 鉴权

配置 Spring Security

Spring Security 是一个功能强大的安全框架,能够帮助您在应用程序中实现认证(Authentication)和授权(Authorization)功能。在整合 Spring Security 框架之前,我们需要新建一个 weblog-module-jwt 子模块(可以参考之前的教程创建),它主要用于放置 jwt 相关的功能代码。创建完成后修改子模块的 pom.xml 文件:

另外,还需在父项目 weblog-springbootpom.xml 文件中添加 jwt 子模块 :

最后,在 weblog-module-admin 模块中,引入 weblog-module-jwt 模块和 Spring Security 依赖,因为认证、鉴权功能只有 admin 后台需要:

自定义配置

要更好地控制 Spring Security 的行为,可以创建一个自定义的 SecurityConfig 类,继承自 WebSecurityConfigurerAdapter。通过覆盖方法,我们可以配置认证、授权规则、自定义登录页面、注销等。

application-dev.yml中配置:

weblog-module-admin 模块中的 config 包下,创建一个 WebSecurityConfig 配置类,并进行如下自定义配置:

在上面 configure() 方法中,主要定义了如下功能:

  1. 禁用 CSRF 和表单登录

    禁用 CSRF(跨站请求伪造)保护,因为在前后端分离的架构中,通常使用 JWT 进行身份验证,CSRF 保护不再必要。

    禁用默认的表单登录,改用 JWT 进行身份验证。

  2. 应用 JWT 认证配置

    JwtAuthenticationSecurityConfig 中定义的 JWT 过滤器应用到安全配置中,以处理登录请求和生成 JWT。

  3. URL 访问控制

    保护所有以 /admin/** 开头的 URL,要求用户经过身份验证。允许其他所有请求不需要身份验证。

  4. 配置 HTTP 基本认证入口点

    使用自定义的 RestAuthenticationEntryPoint 处理未认证用户的请求。

  5. 会话管理策略

    设置会话管理策略为无状态(SessionCreationPolicy.STATELESS),因为在前后端分离的架构中,通常不使用服务器会话。

  6. 添加 Token 认证过滤器

    UsernamePasswordAuthenticationFilter 之前添加 TokenAuthenticationFilter,用于从请求中提取和验证 JWT。

实际上我们需要在 jwt 模块中实现如下主要类,来使自定义的 Spring Security 配置生效:

  • JwtAuthenticationSecurityConfig: 负责配置 JWT 认证的细节,包括成功和失败处理器、用户信息服务和密码编码器。

  • TokenAuthenticationFilter: 负责从请求头中提取 JWT,验证其有效性,并将用户信息存入 SecurityContextHolder

  • RestAuthenticationEntryPoint: 处理未认证用户的请求,返回适当的错误响应。

jwt 模块中创建如下包,接下进行JWT模块的逐步构建:

目录结构

构建JWT认证

以下所有内容若无提示都是在 jwt 子模块中进行的!

令牌工具类 JwtTokenHelper

utils 包中封装一个 JwtTokenHelper 工具类:

通过运行该工具类的 main 方法,我们可以得到一个 base64 编码的密匙,将密匙存放到我们的 application.yml 配置中,同时设置 token 的签发人(可以自行填写):

密码加密类 PasswordEncoderConfig

使用明文密码很容易受到工具,所以我们需要用密码加密算法来保护用户密码。

config 包中创建 PasswordEncoderConfig 配置类:

PasswordEncoder 接口是 Spring Security 提供的密码加密接口,它定义了密码加密和密码验证的方法。通过实现这个接口,您可以将密码加密为不可逆的哈希值,以及在验证密码时对比哈希值。

上述代码中,我们初始化了一个 PasswordEncoder 接口的具体实现类 BCryptPasswordEncoderBCryptPasswordEncoder 是 Spring Security 提供的密码加密器的一种实现,使用 BCrypt 算法对密码进行加密。BCrypt 是一种安全且适合密码存储的哈希算法,它在进行哈希时会自动加入“盐”,增加密码的安全性。通过运行该工具类的 main 方法,我们就得到了加密后的用户密码,请复制得到的加密密码,接下来将会使用到。

用户详细服务类 UserDetailServiceImpl

编辑 UserMapper 接口

service 包中创建 UserDetailServiceImpl 实现类:

该类是UserDetailsService 接口的实现。UserDetailsService 是 Spring Security 提供的接口,用于从应用程序的数据源(如数据库、LDAP、内存等)中加载用户信息。它是一个用于将用户详情加载到 Spring Security 的中心机制。UserDetailsService 主要负责两项工作:

  1. 加载用户信息: 从数据源中加载用户的用户名、密码和角色等信息。

  2. 创建 UserDetails 对象: 根据加载的用户信息,创建一个 Spring Security 所需的 UserDetails 对象,包含用户名、密码、角色和权限等。

在上面代码中,我们实现了 UserDetailsService 接口,并重写了 loadUserByUsername() 方法,该方法用于根据用户名加载用户信息的逻辑 ,需要从数据库中查询。在数据库表t_user中执行如下语句添加用户(请根据自己的配置文件定义修改用户名,密码需要是上一步加密后生成的密码):

自定义认证过滤器 JwtAuthenticationFilter

exception 包下新建 UsernameOrPasswordNullException 类:

然后在 filter 包下新建 JwtAuthenticationFilter 类:

过滤器继承了 AbstractAuthenticationProcessingFilter,用于处理 JWT(JSON Web Token)的用户身份验证过程。在构造函数中,调用了父类 AbstractAuthenticationProcessingFilter 的构造函数,通过 AntPathRequestMatcher 指定了处理用户登录的访问地址。这意味着当请求路径匹配 /login 并且请求方法为 POST 时,该过滤器将被触发。

attemptAuthentication() 方法用于实现用户身份验证的具体逻辑。首先,我们解析了提交的 JSON 数据,并获取了用户名、密码,校验是否为空,若不为空,则将它们封装到 UsernamePasswordAuthenticationToken 中。最后,使用 getAuthenticationManager().authenticate() 来触发 Spring Security 的身份验证管理器执行实际的身份验证过程,然后返回身份验证结果。

自定义认证处理器(成功&失败)

在 Spring Security 中,AuthenticationFailureHandlerAuthenticationSuccessHandler 是用于处理身份验证失败和成功的接口。它们允许您在用户身份验证过程中自定义响应,以便更好地控制和定制用户体验。

model 包下新建LoginRspVO 类。此类是登录的响参类,VO (View Object) 表示和视图相关的实体类,rspresponse 的缩写,表示返参,对应的 reqrequest 的缩写,表示请求。代码如下:

同时为了在过滤器中方便的返回 JSON 参数,我们需要封装一个工具类 ResultUtil, 放置在 /utils 包下,代码如下:

编辑 weblog-module-common 模块中的 ResponseCodeEnum 枚举类,添加用户异常类型的响应码:

handler 包下创建 RestAuthenticationSuccessHandler 类,用于处理认证成功的数据返回:

该自定义的认证成功处理器首先从 authentication 对象中获取用户的 UserDetails 实例,这里是主要是获取用户的用户名,然后通过用户名生成 Token 令牌,最后返回数据。

再在 /handler 包下,创建 RestAuthenticationFailureHandler 认证失败处理器:

这个类实现了 AuthenticationFailureHandler 接口,主要用于处理用户认证失败的情况。

当用户登录失败时,onAuthenticationFailure 方法会被调用。这个方法接收三个参数:HttpServletRequestHttpServletResponseAuthenticationException。在方法内部,首先通过日志记录认证失败的详细信息。接着,根据异常的类型,返回不同的错误信息。如果异常是 UsernameOrPasswordNullException,则表示用户名或密码为空,使用 ResultUtil.fail 方法返回相应的错误信息。如果异常是 BadCredentialsException,则表示用户名或密码错误,同样使用 ResultUtil.fail 返回错误信息。最后,若认证失败的原因不属于上述两种情况,则返回一个通用的登录失败信息。

自定义token校检过滤器 TokenAuthenticationFilter

在建立过滤器之前,我们需要在 /handler 包下,新增 RestAuthenticationEntryPoint 处理器,它专门用来处理当用户未登录时,访问受保护的资源的情况:

当请求受保护的接口且请求头中未携带 token 时,我们收到的异常类型为 InsufficientAuthenticationException,请求的 HTTP 响应码为 401, 这就是未授权的异常类型。因此将这个类型的异常单独拎出来处理。覆写的方法主要实现当校验到 Token 异常时,进行捕获并返回不同的提示信息。

/filter 包下,创建 TokenAuthenticationFilter 过滤器,专用于校检 token 令牌:

这一步自定义了一个 Spring Security 过滤器 TokenAuthenticationFilter,它继承了 OncePerRequestFilter,确保每个请求只被过滤一次。在重写的 doFilterInternal() 方法中来定义过滤器处理逻辑,首先,从请求头中获取 keyAuthorization 的值,判断是否以 Bearer 开头,若是,截取出 Token, 对其进行解析,并对可能抛出的异常做出不同的返参。最后,我们获取了用户详情信息,将用户信息存入 authentication,方便后续进行校验,同时将 authentication 存入 ThreadLocal 中,方便后面方便的获取用户信息,然后在处理完成并且没有出错之后放行请求。

你可能注意到在上面我们实现了处理器类 RestAuthenticationEntryPoint ,但上面 @Autowired 注解的是该类实现的接口 AuthenticationEntryPoint 。在 Spring 框架中,@Autowired 注解用于自动装配依赖项。当你在类中声明一个带有 @Autowired 注解的字段时,Spring 容器会在启动时自动查找与该字段类型匹配的 Bean,并将其注入到该字段中。在该项目中,因为我们之定义了一个 AuthenticationEntryPoint 类型的 Bean RestAuthenticationEntryPoint ,可以直接使用 AuthenticationEntryPoint 。但如果有多个实现,则需要使用 @Qualifier("restAuthenticationEntryPoint") 进行指定。

权限不足处理器 RestAccessDeniedHandler

/handler 包下,另外新增 RestAccessDeniedHandler 处理器,它主要用于处理当用户登录成功时,访问受保护的资源,但是权限不够的情况:

该处理器目前并不会用到,为了后续可能引入多角色时,预留该类方便拓展使用。

自定义 JWT 认证配置 JwtAuthenticationSecurityConfig

/config 包下新建 JwtAuthenticationSecurityConfig, 代码如下:

上述代码是一个 Spring Security 配置类,用于配置 JWT(JSON Web Token)的身份验证机制。它继承了 Spring Security 的 SecurityConfigurerAdapter 类,用于在 Spring Security 配置中添加自定义的认证过滤器和提供者。通过重写 configure() 方法,我们将之前写好过滤器、认证成功、失败处理器,以及加密算法整合到了 httpSecurity 中。

测试

使用设置的用户名和密码(明文)请求 \login 登录接口,得到token值。

添加新的测试API:

在不设置Authorization请求头时请求上面接口,设置Authorization请求头(值为Bearer + token值),以及输入错误的请求头后得到测试结果返回正常:

未设置token
token正确
token错误

最后更新于