vue3整合SpringSecurity加JWT實(shí)現(xiàn)登錄認(rèn)證
前段時(shí)間寫(xiě)了一篇spring security的詳細(xì)入門,但是沒(méi)有聯(lián)系實(shí)際。
所以這次在真實(shí)的項(xiàng)目中來(lái)演示一下怎樣使用springsecurity來(lái)實(shí)現(xiàn)我們最常用的登錄校驗(yàn)。本次演示使用現(xiàn)在市面上最常見(jiàn)的開(kāi)發(fā)方式,前后端分離開(kāi)發(fā)。前端使用vue3進(jìn)行構(gòu)建,用到了element-plus組件庫(kù)、axios封裝、pinia狀態(tài)管理、Router路由跳轉(zhuǎn)等技術(shù)。后端還是spring boot整合springsecurity+JWT來(lái)實(shí)現(xiàn)登錄校驗(yàn)。
本文適合有一定基礎(chǔ)的人來(lái)看,如果你對(duì)springsecurity安全框架還不是很了解,建議你先去看一下我之前寫(xiě)過(guò)的spring security框架的快速入門:
springboot3整合SpringSecurity實(shí)現(xiàn)登錄校驗(yàn)與權(quán)限認(rèn)證(萬(wàn)字超詳細(xì)講解)
技術(shù)棧版本:vue3.3.11、springboot3.1.5、spring security6.x
業(yè)務(wù)流程:
可以看到整個(gè)業(yè)務(wù)的流程還是比較簡(jiǎn)單的,那么接下來(lái)就基于這個(gè)業(yè)務(wù)流程來(lái)進(jìn)行我們具體代碼的編寫(xiě)和實(shí)現(xiàn);
前端:
新建一個(gè)vue項(xiàng)目,并引入一些具體的依賴;我們本次項(xiàng)目用到的有:element-plus、axios、pinia狀態(tài)管理、Router路由跳轉(zhuǎn)(注意我們?cè)陧?xiàng)目中使用到的pinia要引入持久化插件)
在vue項(xiàng)目中新建兩個(gè)組件:Login.vue(登錄組件,負(fù)責(zé)登錄頁(yè)面的展示)、Layout.vue(布局頁(yè)面,負(fù)責(zé)整體項(xiàng)目的布局,登錄成功之后就是跳轉(zhuǎn)到這個(gè)頁(yè)面)
路由的定義:在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登錄組件為默認(rèn)的組件,并定義Layout組件;
useToken的狀態(tài)封裝:在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'; // 先建一個(gè)api const api = axios.create({ baseURL: "http://localhost:8888", timeout: 5000 }); // 發(fā)送請(qǐng)求前攔截 api.interceptors.request.use( config =>{ const useToken = useTokenStore(); if(useToken.token){ console.log("請(qǐng)求頭toekn=====>", useToken.token); // 設(shè)置請(qǐng)求頭 // config.headers['token'] = useToken.token; config.headers.token = useToken.token; } return config; }, error =>{ return Promise.reject(error); } ) // 響應(yīng)前攔截 api.interceptors.response.use( response =>{ console.log("響應(yīng)數(shù)據(jù)", response); if(response.data.code !=200){ ElMessage.error(response.data.message); } return response; }, error =>{ return Promise.reject(error); } ) export default api;
在請(qǐng)求前攔截,主要是為了在請(qǐng)求頭中新增token。在request.ts中引入了useToken,并判斷如果token不為空,那么在請(qǐng)求頭中新增token。
在響應(yīng)前也進(jìn)行了一次攔截,如果后端返回的狀態(tài)碼不為200,那么就打印出錯(cuò)誤信息;
接下來(lái)就可以在Login.vue中進(jìn)行我們的登錄邏輯的具體編寫(xiě)了(我直接將組件內(nèi)容進(jìn)行復(fù)制了,也不是什么太難的東西,主要還是element-plus的表單):
<template> <div class="background" style="font-family:kaiti" > <!-- 注冊(cè)表單 --> <el-dialog v-model="isRegister" title="用戶注冊(cè)" width="30%"> <el-form label-width="120px" v-model="registerForm"> <el-form-item label="用戶名"> <el-input type="text" v-model="registerForm.username" > <template #prefix> <el-icon><Avatar /></el-icon> </template> </el-input> </el-form-item> <el-form-item label="密碼"> <el-input type="password" v-model="registerForm.password" > <template #prefix> <el-icon><Lock /></el-icon> </template> </el-input> </el-form-item> <el-form-item> <el-button type="primary" @click="registerAdd" >提交</el-button> <el-button @click="isRegister = false">取消</el-button> </el-form-item> </el-form> </el-dialog> <!-- 登陸框 --> <div class="login-box"> <el-form label-width="100px" :model="loginFrom" style="max-width: 460px" :rules="Loginrules" ref="ruleFormRef" > <el-form-item label="用戶名" prop="username"> <el-input v-model="loginFrom.username" clearable > <template #prefix> <el-icon><Avatar /></el-icon> </template> </el-input> </el-form-item> <el-form-item label="密碼" prop="password"> <el-input v-model="loginFrom.password" show-password clearable type="password" > <template #prefix> <el-icon><Lock /></el-icon> </template> </el-input> </el-form-item> <el-form-item label="驗(yàn)證碼" prop="codeValue"> <el-input v-model="loginFrom.codeValue" style="width: 100px;" clearable > </el-input> <img :src="codeImage" @click="getCode" style="transform: scale(0.9);"/> </el-form-item> <el-button type="success" @click="getLogin(ruleFormRef)" style="transform: translateX(50px)" class="my-button">登錄</el-button> <el-button type="primary" @click="isRegister=true" class="my-button">注冊(cè)</el-button> </el-form> </div> </div> </template> <script lang="ts" setup> import { ref,onMounted,reactive } from 'vue'; import { useRouter } from 'vue-router'; import { ElMessage } from 'element-plus'; import useTokenStore from '@/stores/useToken' import api from '@/utils/request' import type { FormInstance, FormRules } from 'element-plus' const ruleFormRef = ref<FormInstance>() const loginFrom=ref({ username:'', password:'', codeKey:'', codeValue:'' }) const Loginrules=reactive({ username: [ { required: true, message: '請(qǐng)輸入用戶名', trigger: 'blur' } ], password: [ { required: true, message: '請(qǐng)輸入密碼', trigger: 'blur' }, { min: 6, max: 12, message: '長(zhǎng)度在 6 到 12 個(gè)字符', trigger: 'blur'} ], codeValue: [ { required: true, message: '請(qǐng)輸入驗(yàn)證碼', trigger: 'blur' } ] }) const registerForm=ref({ username:'', password:'' }) const codeImage=ref('') const isRegister=ref(false) const tokenStore = useTokenStore(); const router = useRouter() const getLogin = async(formEl: FormInstance | undefined) => { if (!formEl) return await formEl.validate((valid, fields) => { if (valid) { console.log('submit!') } else { ElMessage('請(qǐng)輸入完整信息') return; } }) let {data}=await api.post('/user/login',loginFrom.value) if(data.code==200){ ElMessage('登錄成功') console.log(data); tokenStore.token=data.data router.replace({name:'layout'}) }else{ ElMessage('登錄失敗') } } const getCode=async()=>{ let {data}=await api.get('/getCaptcha') loginFrom.value.codeKey=data.data.codeKey codeImage.value=data.data.codeValue } const registerAdd=async()=>{ let {data}=await api.post('/user/register',registerForm.value) if(data.code==200){ ElMessage('注冊(cè)成功') isRegister.value=false }else{ ElMessage('注冊(cè)失敗') isRegister.value=false } } // 頁(yè)面加載完成獲取驗(yàn)證碼 onMounted(()=>{ getCode() }) </script>
這個(gè)頁(yè)面中,我還加入了一個(gè)圖形驗(yàn)證碼。還有一個(gè)注冊(cè)的表單。其他的就和普通的登錄一樣了;
這個(gè)頁(yè)面的最終效果如圖:
Layout.vue頁(yè)面中,我們只進(jìn)行兩個(gè)方法的測(cè)試;一個(gè)是獲取當(dāng)前用戶的具體信息,一個(gè)是退出登錄的按鈕;
<template> <div class="common-layout"> <el-container> <el-header height="100px"> 頭部 <el-button type="primary" @click="getUserInfo">獲取用戶信息</el-button> <el-button type="success" @click="Logout">退出登錄</el-button> </el-header> <el-container> <el-aside width="200px"> 菜單欄 </el-aside> <el-main> 展示區(qū) </el-main> </el-container> </el-container> </div> </template> <script lang="ts" setup name="Layout"> import { ref } from 'vue' import api from '@/utils/request' import {ElMessage} from 'element-plus' import { useRouter } from 'vue-router' import useToeknStore from '@/stores/useToken' const router = useRouter() const Logout =async () => { let data= api.get("/user/logout") if(data.data.code==200){ ElMessage.success('退出成功') // 清除token useToeknStore().removeToken router.replace({name:'login'}) } else{ ElMessage.error('退出失敗') } } const getUserInfo = async() => { let data=await api.get("/user/info") console.log('@',data); } </script>
數(shù)據(jù)庫(kù):
我新建一個(gè)數(shù)據(jù)表,用于登錄校驗(yàn):
CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, status INT DEFAULT 0 );
這張表中只有簡(jiǎn)單的用戶名,密碼,和用戶是否過(guò)期等字段;
后端:
新建一個(gè)spring boot項(xiàng)目,并導(dǎo)入以下的依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.3.0</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.18</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.21</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
后端使用MybatisPlus做用戶的增、刪、改、查等?;A(chǔ)的controller、service、mapper,我就不再這里進(jìn)行贅述了;
新建一個(gè)類MyTUserDetail ,繼承UserDetail:
@Data public class MyTUserDetail implements Serializable, UserDetails { private static final long serialVersionUID = 1L; private Users Users; @JsonIgnore //json忽略 @Override public Collection<? extends GrantedAuthority> 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; } }
新建一個(gè)類MyUserDetailServerImpl,實(shí)現(xiàn)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<User>(). eq(username != null, User::getUsername, username)); if (tUser == null) { throw new UsernameNotFoundException("用戶名不存在"); } MyTUserDetail myTUserDetail=new MyTUserDetail(); myTUserDetail.setUser(user); return myTUserDetail; } }
新建一個(gè)JwtUtils的工具類,來(lái)生成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是否過(guò)期 * */ 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; } } }
新建一個(gè)Jwt的攔截類,繼承一個(gè)OncePerRequestFilter類,用來(lái)在每次請(qǐng)求前攔截請(qǐng)求,并從中獲取token,并判斷這個(gè)token是否是我們用戶表中的token;
如果是,那么將用戶信息存儲(chǔ)到security中,這樣后面的過(guò)濾器就可以獲取到用戶信息了,如果不是,那么直接放行。我們會(huì)將這個(gè)攔截器加入到UsernamePasswordAuthenticationFilter過(guò)濾器之前。
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private RedisTemplate<String,String> redisTemplate; @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { //獲取請(qǐng)求頭中的token String token = request.getHeader("token"); System.out.println("前端的token信息=======>"+token); //如果token為空直接放行,由于用戶信息沒(méi)有存放在SecurityContextHolder.getContext()中所以后面的過(guò)濾器依舊認(rèn)證失敗符合要求 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(),后面的過(guò)濾器就可以獲得用戶信息了。這表明當(dāng)前這個(gè)用戶是登錄過(guò)的,后續(xù)的攔截器就不用再攔截了 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,null); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); filterChain.doFilter(request,response); } }
security配置類的設(shè)置:
(由于我們本次采用前后端分離的方式來(lái)進(jìn)行開(kāi)發(fā),所以不在需要使用spring security默認(rèn)提供的formLogin
方法)
formLogin
方法是 Spring Security 中用于配置基于表單的登錄認(rèn)證的一種方式。它通常用于傳統(tǒng)的 Web 應(yīng)用程序,其中前端頁(yè)面由后端動(dòng)態(tài)生成,并且用戶在頁(yè)面中輸入用戶名和密碼來(lái)進(jìn)行登錄。在這種情況下,Spring Security 負(fù)責(zé)處理登錄請(qǐng)求、驗(yàn)證用戶身份、生成會(huì)話等操作。
但是,在前后端分離的開(kāi)發(fā)模式中,前端和后端是完全分離的,前端負(fù)責(zé)渲染界面和處理用戶交互,后端負(fù)責(zé)提供 API 接口和數(shù)據(jù)服務(wù)。因此,通常不會(huì)使用 formLogin
方法,因?yàn)槲覀兊那岸瞬粫?huì)通過(guò)后端渲染的頁(yè)面來(lái)進(jìn)行登錄。后端只需要返回一些相應(yīng)的數(shù)據(jù)和狀態(tài),有關(guān)頁(yè)面的跳轉(zhuǎn)和渲染是由前端(vue3)來(lái)實(shí)現(xiàn)的。
@Configuration @EnableWebSecurity public class MyServiceConfig { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; /* * security的過(guò)濾器鏈 * */ @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()); }); //自定義過(guò)濾器放在UsernamePasswordAuthenticationFilter過(guò)濾器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Autowired private MyUserDetailServerImpl myUserDetailsService; /* * 驗(yàn)證管理器 * */ @Bean public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder){ DaoAuthenticationProvider provider=new DaoAuthenticationProvider(); //將編寫(xiě)的UserDetailsService注入進(jìn)來(lái) provider.setUserDetailsService(myUserDetailsService); //將使用的密碼編譯器加入進(jìn)來(lái) 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的配置類中,設(shè)置了跨域問(wèn)題、攔截器鏈的配置(并將一些需要放行的接口放行,將我們自定義的Jwt攔截器加入了security攔截鏈)、密碼編譯器、AuthenticationManager 驗(yàn)證管理等等一系列配置;
Usercontroller控制器:
@RestController @RequestMapping("/user") public class UsersController { @Autowired private IUsersService userService; @Autowired private PasswordEncoder passwordEncoder; @Autowired private RedisTemplate<String,String> redisTemplate; @Autowired private JwtUtils jwtUtils; @PostMapping("/login") public Result<String> 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<String,String> 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); //清除SpringSecurity上下文 SecurityContextHolder.clearContext(); //刪除redis中存儲(chǔ)的用戶數(shù)據(jù) redisTemplate.delete(Integer.toString(userId)); return Result.success(); } }
在UserController控制器中,由于登錄方法比較復(fù)雜,我將登錄方法重新在service中重寫(xiě)了,剩下的獲取用戶信息、用戶注冊(cè)、退出登錄都直接在UseController中實(shí)現(xiàn)了;
service中重寫(xiě)的登錄方法:
@Service public class UsersServiceImpl extends ServiceImpl<UsersMapper, Users> implements IUsersService { @Autowired private RedisTemplate<String,String> 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,"驗(yàn)證碼錯(cuò)誤"); } // 驗(yàn)證碼正確,刪除redis中的驗(yàn)證碼 redisTemplate.delete(dtoLogin.getCodeKey()); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(dtoLogin.getUsername(),dtoLogin.getPassword()); Authentication authenticate = authenticationManager.authenticate(authenticationToken); if(authenticate==null){ throw new ResultException(400,"用戶名或密碼錯(cuò)誤"); } // 獲取返回的用戶信息 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; } }
由于我們還是用了驗(yàn)證碼,所以在這個(gè)登錄方法中先判斷了驗(yàn)證碼、如果驗(yàn)證碼正確。那么在判斷傳回來(lái)的用戶名和密碼。如果都正確,那么用Jwt返回一個(gè)token,token中攜帶的是用戶的id;
至此,我們所有的前后端代碼都已經(jīng)寫(xiě)完了。那么,讓我們具體的實(shí)驗(yàn)一下;
運(yùn)行:
由于我剛創(chuàng)建的表,還沒(méi)有添加數(shù)據(jù),那么我現(xiàn)在前端點(diǎn)擊注冊(cè),寫(xiě)入幾條用戶信息;
寫(xiě)入信息之后,我使用剛注冊(cè)過(guò)的用戶來(lái)登錄一下:
注冊(cè)成功之后,就會(huì)進(jìn)入到我們自定義個(gè)Layout.vue組件內(nèi):
現(xiàn)在,我點(diǎn)擊“獲取用戶信息”按鈕,因?yàn)檫@個(gè)路徑我們并沒(méi)有放行,那么他訪問(wèn)時(shí)就會(huì)被我們自定義的Jwt攔截器攔截,并驗(yàn)證它請(qǐng)求頭中攜帶的token是否正確。如果正確,則放行。如果不正確,那么就會(huì)放行到登錄攔截器中。
可以看到,在控制臺(tái)中打印出了用戶的信息。這是肯定的,因?yàn)樗@次請(qǐng)求攜帶的token是正確的,那么如果我們修改一下token的值,他還能正常訪問(wèn)到用戶信息這個(gè)接口嗎?
我修改了請(qǐng)求頭中的token信息,可以看到立馬這個(gè)請(qǐng)求就被攔截了。并爆出了403錯(cuò)誤;
現(xiàn)在,我點(diǎn)擊“退出登錄”按鈕,它應(yīng)該刪除useToken中的token值,并且后端也會(huì)刪除redis中的值,并且跳轉(zhuǎn)到登錄頁(yè)面。后端也會(huì)刪除redsi中存儲(chǔ)的用戶數(shù)據(jù);
現(xiàn)在,我們所有的任務(wù)都已經(jīng)完成了。
具體的前后端源碼放在碼云上面了,有需要的可以自行下載:
我再整體理一下具體的思路:
前端發(fā)送請(qǐng)求后端,如果是登錄請(qǐng)求,那么直接走登錄接口即可,我將登錄接口進(jìn)行了方行,任何人都可以訪問(wèn)到登錄接口,并且執(zhí)行登錄接口的邏輯;如果登錄成功,會(huì)返回一個(gè)token,前后會(huì)將這個(gè)token存到useToken中,并且再以后的每次請(qǐng)求中都攜帶token;如果登錄失敗,返回一個(gè)報(bào)錯(cuò)信息即可。
如果前端發(fā)送的不是登錄接口,但是前端攜帶可正確的token,那么會(huì)被我們自定義的Jwt攔截器攔截,并從中讀取用戶信息,放到security中共后續(xù)的攔截器使用;如果沒(méi)有攜帶token,或者token不正確,那么后端會(huì)直接返回403的狀態(tài)碼提示;
后續(xù):權(quán)限校驗(yàn)
前后端分離,使用vue3整合SpringSecurity加JWT實(shí)現(xiàn)權(quán)限校驗(yàn)-CSDN博客
到此這篇關(guān)于vue3整合SpringSecurity加JWT實(shí)現(xiàn)登錄認(rèn)證的文章就介紹到這了,更多相關(guān)vue3 SpringSecurity登錄認(rèn)證內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中使用require動(dòng)態(tài)獲取圖片地址方式
這篇文章主要介紹了vue中使用require動(dòng)態(tài)獲取圖片地址方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06手把手教你Vue3實(shí)現(xiàn)路由跳轉(zhuǎn)
Vue Router是Vue.js的官方路由器,它與Vue.js核心深度集成,使使用Vue.js構(gòu)建單頁(yè)應(yīng)用程序變得輕而易舉,下面這篇文章主要給大家介紹了關(guān)于Vue3實(shí)現(xiàn)路由跳轉(zhuǎn)的相關(guān)資料,需要的朋友可以參考下2022-08-08Vue組件中常見(jiàn)的props默認(rèn)值陷阱問(wèn)題
這篇文章主要介紹了避免Vue組件中常見(jiàn)的props默認(rèn)值陷阱,本文通過(guò)問(wèn)題展示及解決方案給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09vue中的eventBus會(huì)不會(huì)產(chǎn)生內(nèi)存泄漏你知道嗎
這篇文章主要為大家詳細(xì)介紹了vue中的eventBus會(huì)不會(huì)產(chǎn)生內(nèi)存泄漏,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能夠給你帶來(lái)幫助2022-02-02vue3中defineEmits與defineProps的用法實(shí)例
這篇文章主要介紹了vue3中defineEmits/defineProps的用法實(shí)例,需要的朋友可以參考下2023-12-12vue3封裝el-pagination分頁(yè)組件的完整代碼
這篇文章主要介紹了vue3封裝el-pagination分頁(yè)組件的完整代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-02-02Vue+LogicFlow+Flowable實(shí)現(xiàn)工作流
本文主要介紹了Vue+LogicFlow+Flowable實(shí)現(xiàn)工作流,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-12-12vue.js將unix時(shí)間戳轉(zhuǎn)換為自定義時(shí)間格式
這篇文章主要介紹了vue.js將unix時(shí)間戳轉(zhuǎn)換為自定義時(shí)間格式的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01使用idea創(chuàng)建vue項(xiàng)目的圖文教程
Vue.js是一套構(gòu)建用戶界面的框架,只關(guān)注視圖層,它不僅易于上手,還便于與第三方庫(kù)或既有項(xiàng)目整合,下面這篇文章主要給大家介紹了關(guān)于使用idea創(chuàng)建vue項(xiàng)目的相關(guān)資料,需要的朋友可以參考下2022-08-08