目录

前端:

数据库:

后端:

运行:

前段时间写了一篇spring security的详细入门,但是没有联系实际。

所以这次在真实的项目中来演示一下怎样使用springsecurity来实现我们最常用的登录校验。本次演示使用现在市面上最常见的开发方式,前后端分离开发。前端使用vue3进行构建,用到了element-plus组件库、axios封装、pinia状态管理、Router路由跳转等技术。后端还是spring boot整合springsecurity+JWT来实现登录校验。

本文适合有一定基础的人来看,如果你对springsecurity安全框架还不是很了解,建议你先去看一下我之前写过的spring security框架的快速入门:

springboot3整合SpringSecurity实现登录校验与权限认证(万字超详细讲解)_springboot3 + springsecurity6 校验密码-CSDN博客

技术栈版本:vue3.3.11、springboot3.1.5、spring security6.x

业务流程:

可以看到整个业务的流程还是比较简单的,那么接下来就基于这个业务流程来进行我们具体代码的编写和实现;

前端:

新建一个vue项目,并引入一些具体的依赖;我们本次项目用到的有:element-plus、axios、pinia状态管理、Router路由跳转(注意我们在项目中使用到的pinia要引入持久化插件)

在vue项目中新建两个组件:Login.vue(登录组件,负责登录页面的展示)、Layout.vue(布局页面,负责整体项目的布局,登录成功之后就是跳转到这个页面)

路由的定义:在router文件夹下新建index.ts文件

import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({

history: createWebHistory(import.meta.env.BASE_URL),

routes: [

{

path: '/',

name: 'login',

component: () => import('@/components/Login.vue')

},

{

path: '/layout',

name: 'layout',

component: () => import('@/components/Layout.vue')

}

]

})

export default router

定义Login登录组件为默认的组件,并定义Layout组件;

useToken的状态封装:在stoers文件夹下新建useToken.ts

import { defineStore } from 'pinia'

import { ref } from 'vue'

const useTokenStore = defineStore('token', ()=>{

const token=ref()

const removeToken=()=>{

token.value=''

}

return {token,removeToken}

},

{persist: true}

)

export default useTokenStore

axios的封装:在utils文件夹在新建request.ts文件

import axios from "axios";

import useTokenStore from '@/stores/useToken'

import { ElMessage } from 'element-plus';

// 先建一个api

const api = axios.create({

baseURL: "http://localhost:8888",

timeout: 5000

});

// 发送请求前拦截

api.interceptors.request.use(

config =>{

const useToken = useTokenStore();

if(useToken.token){

console.log("请求头toekn=====>", useToken.token);

// 设置请求头

// config.headers['token'] = useToken.token;

config.headers.token = useToken.token;

}

return config;

},

error =>{

return Promise.reject(error);

}

)

// 响应前拦截

api.interceptors.response.use(

response =>{

console.log("响应数据", response);

if(response.data.code !=200){

ElMessage.error(response.data.message);

}

return response;

},

error =>{

return Promise.reject(error);

}

)

export default api;

在请求前拦截,主要是为了在请求头中新增token。在request.ts中引入了useToken,并判断如果token不为空,那么在请求头中新增token。

在响应前也进行了一次拦截,如果后端返回的状态码不为200,那么就打印出错误信息;

接下来就可以在Login.vue中进行我们的登录逻辑的具体编写了(我直接将组件内容进行复制了,也不是什么太难的东西,主要还是element-plus的表单):

这个页面中,我还加入了一个图形验证码。还有一个注册的表单。其他的就和普通的登录一样了;

这个页面的最终效果如图:

Layout.vue页面中,我们只进行两个方法的测试;一个是获取当前用户的具体信息,一个是退出登录的按钮;

数据库:

我新建一个数据表,用于登录校验:

CREATE TABLE users (

id INT PRIMARY KEY AUTO_INCREMENT,

username VARCHAR(255) NOT NULL,

password VARCHAR(255) NOT NULL,

status INT DEFAULT 0

);

这张表中只有简单的用户名,密码,和用户是否过期等字段;

后端:  

新建一个spring boot项目,并导入以下的依赖:

org.springframework.boot

spring-boot-starter-web

com.mysql

mysql-connector-j

runtime

org.projectlombok

lombok

true

com.auth0

java-jwt

4.3.0

com.baomidou

mybatis-plus-boot-starter

3.5.3.1

cn.hutool

hutool-all

5.8.18

com.alibaba

fastjson

2.0.21

org.springframework.boot

spring-boot-starter-data-redis

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-test

test

后端使用MybatisPlus做用户的增、删、改、查等。基础的controller、service、mapper,我就不再这里进行赘述了;

新建一个类MyTUserDetail ,继承UserDetail:

@Data

public class MyTUserDetail implements Serializable, UserDetails {

private static final long serialVersionUID = 1L;

private Users Users;

@JsonIgnore //json忽略

@Override

public Collection getAuthorities() {

return null;

}

@JsonIgnore

@Override

public String getPassword() {

return this.getUsers().getPassword();

}

@JsonIgnore

@Override

public String getUsername() {

return this.getUsers().getUsername();

}

@JsonIgnore

@Override

public boolean isAccountNonExpired() {

return this.getUsers().getStatus()==0;

}

@JsonIgnore

@Override

public boolean isAccountNonLocked() {

return this.getUsers().getStatus()==0;

}

@JsonIgnore

@Override

public boolean isCredentialsNonExpired() {

return this.getUsers().getStatus()==0;

}

@JsonIgnore

@Override

public boolean isEnabled() {

return this.getUsers().getStatus()==0;

}

}

新建一个类MyUserDetailServerImpl,实现MyUserDetailServer接口的loadUserByUsername方法

@Service

public class MyUserDetailServerImpl implements MyUserDetailServer {

@Autowired

UserMapper userService;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

User user = userService.selectOne(new LambdaQueryWrapper().

eq(username != null, User::getUsername, username));

if (tUser == null) {

throw new UsernameNotFoundException("用户名不存在");

}

MyTUserDetail myTUserDetail=new MyTUserDetail();

myTUserDetail.setUser(user);

return myTUserDetail;

}

}

新建一个JwtUtils的工具类,来生成token;

@Component

public class JwtUtil {

private final String secret="zhangqiao";

private final Long expiration=36000000L;

public String generateToken(Integer id) {

Date now = new Date();

Date expiryDate = new Date(now.getTime() + expiration);

Algorithm algorithm = Algorithm.HMAC256(secret);

return JWT.create()

.withSubject(String.valueOf(id))

.withIssuedAt(now)

.withExpiresAt(expiryDate)

.sign(algorithm);

}

public Integer getUsernameFromToken(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

return Integer.valueOf(jwt.getSubject());

} catch (JWTDecodeException e) {

return null;

}

}

/*

* 判断token是否过期

* */

public boolean isTokenValid(String token) {

try {

Algorithm algorithm = Algorithm.HMAC256(secret);

JWT.require(algorithm).build().verify(token);

return true;

} catch (Exception e) {

return false;

}

}

/*

* 刷新token

* */

public String refreshToken(String token) {

try {

DecodedJWT jwt = JWT.decode(token);

String username = jwt.getSubject();

Algorithm algorithm = Algorithm.HMAC256(secret);

Date now = new Date();

Date expiryDate = new Date(now.getTime() + expiration);

return JWT.create()

.withSubject(username)

.withIssuedAt(now)

.withExpiresAt(expiryDate)

.sign(algorithm);

} catch (JWTDecodeException e) {

return null;

}

}

}

新建一个Jwt的拦截类,继承一个OncePerRequestFilter类,用来在每次请求前拦截请求,并从中获取token,并判断这个token是否是我们用户表中的token;

如果是,那么将用户信息存储到security中,这样后面的过滤器就可以获取到用户信息了,如果不是,那么直接放行。我们会将这个拦截器加入到UsernamePasswordAuthenticationFilter过滤器之前。

@Component

public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

@Autowired

private RedisTemplate redisTemplate;

@Autowired

private JwtUtil jwtUtil;

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

//获取请求头中的token

String token = request.getHeader("token");

System.out.println("前端的token信息=======>"+token);

//如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求

if(!StringUtils.hasText(token)){

filterChain.doFilter(request,response);

return;

}

// 解析Jwt中的用户id

Integer userId = jwtUtil.getUsernameFromToken(token);

//从redis中获取用户信息

String redisUser = redisTemplate.opsForValue().get(String.valueOf(userId));

if(!StringUtils.hasText(redisUser)){

filterChain.doFilter(request,response);

return;

}

MyTUserDetail myTUserDetail= JSON.parseObject(redisUser, MyTUserDetail.class);

//将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。这表明当前这个用户是登录过的,后续的拦截器就不用再拦截了

UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,null);

SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);

filterChain.doFilter(request,response);

}

}

security配置类的设置:

(由于我们本次采用前后端分离的方式来进行开发,所以不在需要使用spring security默认提供的formLogin 方法)

formLogin 方法是 Spring Security 中用于配置基于表单的登录认证的一种方式。它通常用于传统的 Web 应用程序,其中前端页面由后端动态生成,并且用户在页面中输入用户名和密码来进行登录。在这种情况下,Spring Security 负责处理登录请求、验证用户身份、生成会话等操作。

但是,在前后端分离的开发模式中,前端和后端是完全分离的,前端负责渲染界面和处理用户交互,后端负责提供 API 接口和数据服务。因此,通常不会使用 formLogin 方法,因为我们的前端不会通过后端渲染的页面来进行登录。后端只需要返回一些相应的数据和状态,有关页面的跳转和渲染是由前端(vue3)来实现的。

@Configuration

@EnableWebSecurity

public class MyServiceConfig {

@Autowired

private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

/*

* security的过滤器链

* */

@Bean

public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {

http.csrf(AbstractHttpConfigurer::disable);

http.authorizeHttpRequests((auth) ->

auth

.requestMatchers("/getCaptcha","user/login","user/register").permitAll()

.anyRequest().authenticated()

);

http.cors(cors->{

cors.configurationSource(corsConfigurationSource());

});

//自定义过滤器放在UsernamePasswordAuthenticationFilter过滤器之前

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();

}

@Autowired

private MyUserDetailServerImpl myUserDetailsService;

/*

* 验证管理器

* */

@Bean

public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){

DaoAuthenticationProvider provider=new DaoAuthenticationProvider();

//将编写的UserDetailsService注入进来

provider.setUserDetailsService(myUserDetailsService);

//将使用的密码编译器加入进来

provider.setPasswordEncoder(passwordEncoder);

//将provider放置到AuthenticationManager 中

ProviderManager providerManager=new ProviderManager(provider);

return providerManager;

}

//跨域配置

@Bean

public CorsConfigurationSource corsConfigurationSource() {

CorsConfiguration configuration = new CorsConfiguration();

configuration.setAllowedOrigins(Arrays.asList("*"));

configuration.setAllowedMethods(Arrays.asList("*"));

configuration.setAllowedHeaders(Arrays.asList("*"));

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

source.registerCorsConfiguration("/**", configuration);

return source;

}

/*

* 密码加密器*/

@Bean

public PasswordEncoder passwordEncoder(){

return new BCryptPasswordEncoder();

}

}

在security的配置类中,设置了跨域问题、拦截器链的配置(并将一些需要放行的接口放行,将我们自定义的Jwt拦截器加入了security拦截链)、密码编译器、AuthenticationManager 验证管理等等一系列配置;

Usercontroller控制器:

@RestController

@RequestMapping("/user")

public class UsersController {

@Autowired

private IUsersService userService;

@Autowired

private PasswordEncoder passwordEncoder;

@Autowired

private RedisTemplate redisTemplate;

@Autowired

private JwtUtils jwtUtils;

@PostMapping("/login")

public Result login(@RequestBody DtoLogin dtoLogin) {

System.out.println(dtoLogin);

String token = userService.login(dtoLogin);

return Result.successData(token);

}

@PostMapping("/register")

public Result register(@RequestBody DtoLogin dtoLogin) {

System.out.println(dtoLogin);

Users users = new Users();

users.setUsername(dtoLogin.getUsername());

users.setPassword(passwordEncoder.encode(dtoLogin.getPassword()));

userService.save(users);

return Result.success();

}

@Autowired

private RedisTemplate redisTemplate;

@Autowired

private JwtUtil jwtUtil;

@GetMapping("/info")

public Result info(@RequestHeader("token")String token){

System.out.println("controller层获取到的token=======>"+token);

Integer id = jwtUtil.getUsernameFromToken(token);

String redisUser = redisTemplate.opsForValue().get(String.valueOf(id));

MyTUserDetail myTUserDetail = JSON.parseObject(redisUser, MyTUserDetail.class);

return Result.successData(myTUserDetail);

}

@GetMapping("user/logout")

public Result logout(@RequestHeader("token")String token){

// 解析Jwt中的用户id

Integer userId = jwtUtil.getUsernameFromToken(token);

//删除redis中存储的用户数据

redisTemplate.delete(Integer.toString(userId));

return Result.success();

}

}

在UserController控制器中,由于登录方法比较复杂,我将登录方法重新在service中重写了,剩下的获取用户信息、用户注册、退出登录都直接在UseController中实现了;

service中重写的登录方法:  

@Service

public class UsersServiceImpl extends ServiceImpl implements IUsersService {

@Autowired

private RedisTemplate redisTemplate;

@Autowired

AuthenticationManager authenticationManager;

@Autowired

private JwtUtil jwtUtil;

@Override

public String login(DtoLogin dtoLogin) {

String codeRedis = redisTemplate.opsForValue().get(dtoLogin.getCodeKey());

if (!dtoLogin.getCodeValue().equals(codeRedis)){

throw new ResultException(400,"验证码错误");

}

// 验证码正确,删除redis中的验证码

redisTemplate.delete(dtoLogin.getCodeKey());

UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dtoLogin.getUsername(),dtoLogin.getPassword());

Authentication authenticate = authenticationManager.authenticate(authenticationToken);

if(authenticate==null){

throw new ResultException(400,"用户名或密码错误");

}

// 获取返回的用户信息

Object principal = authenticate.getPrincipal();

MyTUserDetail myTUserDetail=(MyTUserDetail) principal;

System.out.println(myTUserDetail);

// 使用Jwt生成token,并将用户的id传入

String token = jwtUtil.generateToken(myTUserDetail.getUsers().getId());

redisTemplate.opsForValue().

set(String.valueOf(myTUserDetail.getUsers().getId()), JSON.toJSONString(myTUserDetail),1, TimeUnit.DAYS);

return token;

}

}

由于我们还是用了验证码,所以在这个登录方法中先判断了验证码、如果验证码正确。那么在判断传回来的用户名和密码。如果都正确,那么用Jwt返回一个token,token中携带的是用户的id;

至此,我们所有的前后端代码都已经写完了。那么,让我们具体的实验一下;

运行:

由于我刚创建的表,还没有添加数据,那么我现在前端点击注册,写入几条用户信息;

写入信息之后,我使用刚注册过的用户来登录一下:

注册成功之后,就会进入到我们自定义个Layout.vue组件内:

现在,我点击“获取用户信息”按钮,因为这个路径我们并没有放行,那么他访问时就会被我们自定义的Jwt拦截器拦截,并验证它请求头中携带的token是否正确。如果正确,则放行。如果不正确,那么就会放行到登录拦截器中。

可以看到,在控制台中打印出了用户的信息。这是肯定的,因为它这次请求携带的token是正确的,那么如果我们修改一下token的值,他还能正常访问到用户信息这个接口吗?

我修改了请求头中的token信息,可以看到立马这个请求就被拦截了。并爆出了403错误;

现在,我点击“退出登录”按钮,它应该删除useToken中的token值,并且后端也会删除redis中的值,并且跳转到登录页面。后端也会删除redsi中存储的用户数据;

现在,我们所有的任务都已经完成了。

具体的前后端源码放在码云上面了,有需要的可以自行下载:

Vue-Security: 前后端分离的Security

我再整体理一下具体的思路:

前端发送请求后端,如果是登录请求,那么直接走登录接口即可,我将登录接口进行了方行,任何人都可以访问到登录接口,并且执行登录接口的逻辑;如果登录成功,会返回一个token,前后会将这个token存到useToken中,并且再以后的每次请求中都携带token;如果登录失败,返回一个报错信息即可。

如果前端发送的不是登录接口,但是前端携带可正确的token,那么会被我们自定义的Jwt拦截器拦截,并从中读取用户信息,放到security中共后续的拦截器使用;如果没有携带token,或者token不正确,那么后端会直接返回403的状态码提示;

后续:权限校验前后端分离,使用vue3整合SpringSecurity加JWT实现权限校验-CSDN博客

参考链接

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: