一、创建vue项目
# 创建Vue项目
npm create vue@latest# 进入项目目录
cd security-frontend# 进入项目目录
cd security-frontend# 安装依赖
npm install axios# 安装其它依赖
npm i
配置main.js文件

配置路由router/index.js

登录组件Login.Vue
<script setup>
import { ref } from 'vue'
import {login} from "@/api/request.js"// 表单数据
const username = ref('')
const password = ref('')
const errorMessage = ref('')
const isLoading = ref(false)// 表单验证
const validateForm = () => {
if (!username.value.trim()) {
errorMessage.value = '请输入用户名'
return false
}
if (!password.value.trim()) {
errorMessage.value = '请输入密码'
return false
}
errorMessage.value = ''
return true
}// 登录处理函数
const handleLogin = async () => {
if (!validateForm()) return
isLoading.value = true
// 模拟登录请求
try {
// 在实际应用中,这里应该是一个API调用
} catch (error) {
errorMessage.value = '登录失败,请稍后再试'
} finally {
isLoading.value = false
}
}
</script><template>
<div class="login-container">
<div class="login-form">
<h2>用户登录</h2>
<div v-if="errorMessage" class="error-message">
{{ errorMessage }}
</div>
<div class="form-group">
<label for="username">用户名</label>
<input
type="text"
id="username"
v-model="username"
placeholder="请输入用户名"
@keyup.enter="handleLogin"
/>
</div>
<div class="form-group">
<label for="password">密码</label>
<input
type="password"
id="password"
v-model="password"
placeholder="请输入密码"
@keyup.enter="handleLogin"
/>
</div>
<button
class="login-button"
:disabled="isLoading"
@click="handleLogin"
>
{{ isLoading ? '登录中...' : '登录' }}
</button>
</div>
</div>
</template><style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
width: 100%;
}.login-form {
background-color: rgba(255, 255, 255, 0.1);
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(10px);
width: 100%;
max-width: 400px;
}@media (prefers-color-scheme: light) {
.login-form {
background-color: rgba(255, 255, 255, 0.95);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
}.login-form h2 {
text-align: center;
margin-bottom: 2rem;
color: inherit;
}.form-group {
margin-bottom: 1.5rem;
}.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: inherit;
}.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
background-color: rgba(255, 255, 255, 0.1);
color: inherit;
font-size: 1rem;
box-sizing: border-box;
transition: border-color 0.25s, background-color 0.25s;
}@media (prefers-color-scheme: light) {
.form-group input {
border: 1px solid #ddd;
background-color: #fff;
}
}.form-group input:focus {
outline: none;
border-color: #646cff;
background-color: rgba(255, 255, 255, 0.15);
}@media (prefers-color-scheme: light) {
.form-group input:focus {
background-color: #fff;
}
}.login-button {
width: 100%;
padding: 0.75rem;
border: none;
border-radius: 8px;
background-color: #646cff;
color: white;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.25s, transform 0.1s;
}.login-button:hover:not(:disabled) {
background-color: #535bf2;
}.login-button:active:not(:disabled) {
transform: scale(0.98);
}.login-button:disabled {
background-color: #666;
cursor: not-allowed;
}.error-message {
background-color: rgba(255, 0, 0, 0.1);
color: #ff4d4f;
padding: 0.75rem;
border-radius: 8px;
margin-bottom: 1.5rem;
border: 1px solid rgba(255, 0, 0, 0.3);
}
</style>
样式文件style/style.css

根组件

配置Axios实例

实现前端登陆功能

api/request.js

在资料中导入vue项目,在登录页上不需要使用隐藏域(csrf)的这个参数了,因为它的值是通过security的session中获取的,既然是前后端分离了,显然获取不到session中的数据了。但是这样我们就不能预防csrf攻击了,但是不要紧,我们后续有解决方案(jwt)。
二、Security配置类配置
在配置类中我们首先将预防csrf的攻击禁用掉,同时添加上解决跨域问题的解决方案

跨域的解决方案中需要一个Bean,这个bean是系统提供的,所以需要我们自己来注册spring容器中


而这个bean是一个接口,那么很显然需要它的实现类来注册bean

我们采用标注的bean来实现,而该bean需要调用registerCorsConfiguration来定义跨域的规则



有无参构造方法,我们就通过这个方式创建这个bean,然后设置跨域规则就好了。

三、成功和失败的配置
当我们登录成功或者失败后,是不是应该有一个处理方案,因为这个时候,我们使用的前后端分离,就不能去使用我们之前的thymeleaf语法的页面了。
后端只需要返回json数据,然前端去进行页面之间的跳转就可以了。
那么接下来处理:

然后定义对应的成功和失败的处理器就好了, 返回给前端数据的时候,是不是应该有一个统一的格式呢?

成功的处理器:

失败的处理器:

然后通过依赖注入的方式注入进来就可以去使用了:

四、前端项目处理
路由配置/router/index.js

登录成功后跳转到首页

当登录成功后跳转到首页Index.vue中就好了。
但是有一个问题,我们登录成功后,在前端发送其它请求去后端,发现竟然不能访问,为啥呢,因为是前后端分离,后端服务器不保存登录的凭证了,也就是说哪怕登录成功也不能访问获取后端的数据了。如何解决呢?
没有session、前端cookie中也不会存储sessionid;那么这样的话,用户状态怎么保持呢?
使用JWT即可解决。
五、JWT
解决方案:

5.1 什么是JWT
JSON Web Token (JWT)是一个开放标准(RFC 7519) ,它定义了一种紧凑和自包含的方式,用于作为 JSON 对象在各方之间安全地传输信息。此信息可以进行验证和信任,因为它是经过数字签名的。JWT 可以使用机密(使用 HMAC 算法)或使用 RSA 或 ECDSA 的公钥/私钥对进行签名。
虽然可以对 JWT 进行加密,以便在各方之间提供保密性,但是我们将关注已签名的Token。签名Token可以验证其中包含的声明的完整性,而加密Token可以向其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,该签名还证明只有持有私钥的一方才是对其进行签名的一方( 签名技术是保证传输的信息不可抵赖,并不能保证信息传输的安全 )。
5.2 JWT能做什么
5.2.1 授权
这是使用jwt最常见方案。一旦用户登录,后续每个请求将包含jwt,从而允许用户访问该令牌允许的路由,服务和资源,单点登录就是其中一个使用场景,因为开销很小且可以在不同的域中轻松使用。
5.2.2 信息交换
jwt是在各方之间安全传输信息的好方法,因为可以对jwt进行签名,保证发送端和发送内容的没有被篡改
5.3 为什么是JWT
5.3.1 基于传统的session认证
认证方式
传统使用session就是前端cookie保存一个sessionID,下次发送请求自动携带
认证流程

暴露问题
- session保存在内存中,用户越多,内存负载越大
- 分布式应用的话限制了负载均衡能力
- cookie被截获容易受到跨站请求伪造攻击
- 在前后端分离系统重更加痛苦
5.3.2 基于JWT认证

认证流程:
- 前端发送请求将用户名和密码发送到后端。
- 后端核对用户名和密码成功后,将用户的ID等其他信息作为jwt的payload,将其与头部分别进行base64编码拼接后签名,形成一个jwt。
- 后端将jwt字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或者sessionStorage上,退出登录时,前端删除保存的jwt即可
- 每次发送请求时将jwt放入Http Header中的Authorization中
- 后端检查是否存在,如存在验证jwt的有效性。例如检查签名是否正确,token是否过期,token的接收方是不是自己
- 验证后,后端使用jwt包含的用户信息进行其他逻辑操作,返回相应结果
5.4 JWT结构
由三部分组成,组成部分以.隔开,结构类似
xxxxx.yyyyy.zzzzz,每部分使用 Base64Url 进行编码。
- Header 标头(一般是固定的)
- Payload 有效载荷(一般理解为传参的参数,官方给我们一些固定的key值,不一定要使用,可以自己定义)
- Signature 签名 (防伪标志)
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
这种就是jwt的结构组成。
我们可以通过https://jwt.io/#debugger-io 或者 https://jwt.p2hp.com/ 中解析出三个部分的信息

5.4.1 Header 标头
报头通常有两部分组成:
Token的类型即JWT,所使用的签名算法(如 HMAC SHA256或 RSA)。 如上图所示:最终这个json由base64进行加密(该加密可以对称解密),构成JWT的第一部分。
5.4.2 Payload 有效载荷
有效载荷,包含声明,其实就是声明实体的数据,一般都是用户的数据。
三种声明类型为:registered claims,public claims,private claims
{
"sub": "1234567890",// 注册声明
"name": "John Doe",// 公共声明
"admin": true // 私有声明
}
注册声明:
这些是一组预定义的声明,不是强制性的,而是推荐的,以提供一组有用的,可互操作的声明
比如:iss:jwt签发者 sub:jwt所面向的用户 aud:接收jwt的一方 exp:jwt的过期时间 iat:jwt的签发时间
注意:声明名称只有三个字符,因为jwt意味着紧凑的
public claims(公共的声明)
使用JWT的人开一随意定义这些声明(可以自己声明一些有效信息,如用户id,nam等,但是不要设置一些敏感信息,如密码)。但是为了避免冲突,应该在JWT注册表中定义或者将他们定义为包含抗冲突名称空间的URI.
private claims(私人声明)
这些是创建用于在同一使用他们的各方之间共享信息的习惯声明(是提供者和消费者所共同定义的声明)
注意:对于已经签名的Token,这些信息虽然受到保护,不会被篡改,但是任何人都可以阅读。除非加密,否则不要将机密信息放在JWT的有效负载或者头元素中。
5.4.1 Signature 签名
要创建Signature,必须获取编码的标头(header),编码的有效载荷(payload)、secret(自定义,越复杂越好)、标头中指定的算法,并对其进行签名。
例如:如果想使用HMACSHA256算法,签名将按以下方式创建:
HMACSHA256(
base64UrlEncode(header)+"."+base64UrlEncode(payload),secret
)
上面的Json将会通过HMACSA256算法结合secret进行加盐签名(私钥加密),其中header和payload将通过base64UrlEncode()方法进行base64加密然后通过字符串拼接"."生成新字符串,最终生成JWT的第三部分.
注意:secret是保存在服务器端的,jwt的签发生成也是正在服务器端的,secret就是用来进行jwt的签发和验证,所以,他就是服务器端的私钥,在任何场景都不要泄露出去,一旦客户端知道这个secret,那就以为这客户端可以自己签发jwt了。
JWT的生成与解析
JWT输出是三个由点分割的Base64-URL字符串,这些字符串在HTML与HTTP环境传递。
5.5 JWT基本使用
采用糊涂工具包:https://www.hutool.cn/ 官网
5.5.1 添加依赖

5.5.2 测试

5.5.1 项目中的应用
在验证成功后去后端服务器去生成token,并返回给前端

在yml中自定义属性:

5.5.1 如何在本地保存
sessionStorage:会话存储,只能在当前页生效(推荐)



localStorage:本地存储,只要是同一个浏览器



5.5.1 如何保持认证状态
因为jwt是无状态的(服务器不知道),这里我们采用每次发送请求带请求头加上生成的jwt,进入服务器后存入到redis中,再次请求的时候先验证jwt是否一致,然后去redis中取出存储的jwt匹配

1万+

被折叠的 条评论
为什么被折叠?



