目录
- 前言
- 概念
- 实际演示
- 路由信息
- 初始访问登录界面
- 登录验证
- 验证过期
- vue实现
- 依赖引入
- main.js
- 获取和设置token工具类
- 登录方法
- 实体
- 登录方法
- axios请求
- router配置
- springboot实现
- 依赖引入
- JWT工具类
- 忽视jwt验证注解
- 拦截器逻辑
- 跨域&调用拦截器配置
- 登录接口&验证token接口
- 结语
前言
最近在研究SSO(单点登录)系统,对于内部是如何实现登录验证的产生了兴趣,而现在终于研究出它是如何验证成功的,接下来我将讲解如何通过vue和springboot实现Jwt验证登录
🌺🌹🥀🌺🥀🌹🌺🌹🥀🌺🥀🌹
概念
在正式开始之前,我同样会讲解一下概念
单点登录
:
单点登录(Single Sign-On, SSO)是一种
身份认证
和授权机制
,允许用户在多个应用程序或系统
中进行登录,并在登录后可以无需重新输入凭据就能访问其他应用程序。通过SSO,用户只需登录一次,即可获得对多个相关但独立的软件系统或资源的访问权限。
那么这篇文章,只会讲解如何实现身份认证,并不会讲解如何实现SSO
🟠🟡🔴🟠🟣🔵🟡🟠🟣
JWT
:
JWT全称为JSON Web Token,是一种开放标准(RFC 7519),定义了一种紧凑且自包含的方式,用于在各方之间安全地传输信息。它通常用于
在用户和服务之间传递身份认证信息和声明
。JWT通常被用作实现身份验证和授权的标准方法。
JWT的本质就是一个
字符串
,它是将用户信息保存到一个Json字符串中,然后进行编码后得到一个JWT token,并且这个JWT token带有签名信息,接收后可以校验是否被篡改
🌺🌹🥀🌺🥀🌹🌺🌹🥀🌺🥀🌹
实际演示
路由信息
在我的项目中,我的router页面有这些:
export const constantRoutes = [{path: '/login',component: () => import('@/views/login/index'),hidden: true},{path: '/main',component: () => import('@/views/main'),children: [{path: '',name: 'dashBorad',component: () => import('@/views/dashBorad')},{path: '/menuPage',name: 'menuPage',component: () => import('@/views/menuPage')},{path: '/userDataManage',name: 'userDataManage',component: () => import('@/views/user/userDataManage')}]},{path: '/404',component: () => import('@/views/404'),hidden: true},// 404 page must be placed at the end !!!{path: '*', redirect: '/404', hidden: true}
]
初始访问登录界面
初始我访问界面,会到登录界面
假如我在未登录的情况下想访问其他路由,会禁止:
自动会跳转到登录界面
登录验证
假如输入账号密码登录之后,才能进入到系统内:
这个时候能够切换到不同的界面:
并且能够调用后端接口查询数据:
验证过期
但是一旦jwt验证过期,为演示方便,这边将手动把token修改错误
此时再跳转界面和查数据都会,报错,且跳转到登录页:
🧡💚💛🧡💜🧡🧡💚💛🧡💜🧡
🌺🌷🌻🌼🌷🌺🌷🌻🌼🌷🌻🌼~~~~~~~~
🧡💚💛🧡💜🧡🧡💚💛🧡💜🧡
vue实现
🌴🌳🍀🌲🥀🍁
依赖引入
在我的项目中,涉及相关JWT验证的有如下:
// axios
npm i axios@1.5.0
// elementui
npm i element-ui -S
// router
npm i vue-router
// js-cookie
npm i js-cookie
🧡🧡🧡🧡🧡🧡🧡🧡🧡🧡🧡🧡
main.js
这时main.js的代码如下:
import Vue from 'vue'
import App from './App'
import router from './router'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
Vue.config.productionTip = falseVue.use(ElementUI)
new Vue({el: '#app',router,components: { App },template: '<App/>'
})
💛💛💛💛💛💛💛💛💛💛💛💛
获取和设置token工具类
这里写一个工具类专门获取和设置工具类
建一个token.js
import Cookies from 'js-cookie'
// 获取token的key,需要和后端一致
const TokenKey = 'Authorization'
// 获取token
export function getToken () {return Cookies.get(TokenKey)
}
// 设置token
export function setToken (token) {return Cookies.set(TokenKey, token)
}
// 移除token
export function removeToken () {return Cookies.remove(TokenKey)
}
💙💙💙💙💙💙💙💙💙💙💙💙
登录方法
因为我的登录方法存在别的逻辑,如验证码,记住我等等,因此,这里演示只给出最纯粹的登录逻辑
实体
// 设置用户名和密码登录loginForm: {username: 'admin',password: 'admin',},
登录方法涉及到引入
// 注意,这里文件的位置请根据自己实际项目文件位置进行修改
import { getToken, setToken } from '@/utils/token'
import {login} from '../../api/login'
🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸🌸
登录方法
点击按钮调用登录方法
submitLogin () {if (!this.loginForm.username) {this.$message.error('用户名不能为空!')return}if (!this.loginForm.password) {this.$message.error('密码不能为空!')return}login(this.loginForm.username, this.loginForm.password).then(res => {if (res.header.code !== 0) {this.$message.error(res.header.message)return}// 设置tokensetToken(res.value.token)// 根据自己实际项目跳转主界面this.$router.push('/main')})},
当这里登录之后会在cookie位置新增数据(F12):
💐💐💐💐💐💐💐💐💐💐💐💐💐
axios请求
新增axios的工具类,进行封装,在这里会在调用之前确认是否验证过期
request.js
: 新建js代码,代码如下:
import axios from 'axios'
import { Message, MessageBox, Notification } from 'element-ui'
import { getToken } from '@/utils/token'
import errorCode from '@/utils/errorCode'
import router from '../router/index'
import {removeToken} from './token'axios.defaults.headers['Content-Type'] = 'application/json;charset=utf-8'// 创建axios实例
const service = axios.create({// 配置后端请求路径,根据自己实际项目修改baseURL: process.env.VUE_APP_BASE_API,// 请求超时时间 withCredentials: true,timeout: 30000
})// 请求统一拦截处理
service.interceptors.request.use(config => {// 是否需要设置 tokenif (getToken()) {config.headers['Authorization'] = getToken() // 请求均需携带自定义token}return config
},
error => {// 请求失败console.log(error) // for debug// return Promise.reject(error)return Promise.reject(error)
}
)// 响应拦截器
service.interceptors.response.use(res => {console.log('res.data', res.data)// 未设置状态码则默认成功状态const code = res.data.header.code || 200// 获取错误信息const msg = errorCode[code] || res.data.header.message || errorCode['default']if (code === 401) {return new Promise((resolve, reject) => {MessageBox.confirm('登录状态已过期,请重新登录', '系统提示', {confirmButtonText: '重新登录',showCancelButton: false,type: 'warning'}).then(() => {removeToken()router.push('/login')resolve() // 手动解决 Promise,避免重复导航})})} else if (code === 500) {Message({message: msg,type: 'error'})return Promise.reject(new Error(msg))} else if (code !== 0 && code !== 200) {Notification.error({title: msg})// eslint-disable-next-line prefer-promise-reject-errorsreturn Promise.reject('error')} else {return res.data}
}, error => {let { message } = errorif (message === 'Network Error') {message = '服务端连接异常'} else if (message.includes('timeout')) {message = '系统接口请求超时'} else if (message.includes('Request failed with status code')) {message = '系统接口' + message.substr(message.length - 3) + '异常'}Message({message: message,type: 'error',duration: 5 * 1000})return Promise.reject(error)
}
)export default service
上述会捕获后端返回的
code
,做不同的事情,是否可调用接口
针对上述的errorCode
,为新建的封装报错代码js,根据自己需要可做可不做
errorCode.js
: 代码如下:
export default {'401': '认证失败,无法访问系统资源','403': '当前操作没有权限','404': '访问资源不存在','default': '系统未知错误,请反馈给管理员'
}
上述登录方法涉及到的axios的api如下
login.js
:
import request from '@/utils/request'// 登录方法
export function login (account, password) {const data = {account,password}return request({url: '/idle/login',method: 'post',data: data})
}// 验证token是否有效
export function verify (token) {let data = {token}return request({url: '/idle/verify',method: 'get',params: data})
}
router配置
跳转路由,拦截请求是否已经过期
新建router
文件夹,在其中建立index.js
代码如下:
import Vue from 'vue'
import Router from 'vue-router'
// eslint-disable-next-line standard/object-curly-even-spacing
import {getToken, removeToken } from '@/utils/token'
import {verify} from '@/api/login'
import { Message } from 'element-ui'Vue.use(Router)export const constantRoutes = [{path: '/login',component: () => import('@/views/login/index'),hidden: true},{path: '/main',component: () => import('@/views/main'),children: [{path: '',name: 'dashBorad',component: () => import('@/views/dashBorad')},{path: '/menuPage',name: 'menuPage',component: () => import('@/views/menuPage')},{path: '/userDataManage',name: 'userDataManage',component: () => import('@/views/user/userDataManage')}]},{path: '/404',component: () => import('@/views/404'),hidden: true},// 404 page must be placed at the end !!!{path: '*', redirect: '/404', hidden: true}
]const createRouter = () => {const router = new Router({mode: 'hash',scrollBehavior: () => ({y: 0}),routes: constantRoutes})router.beforeEach((to, from, next) => {let token = getToken()if (!token) {// 如果未登录并且不是去往登录页,则跳转到登录页if (to.path !== '/login') {next('/login')} else {next() // 如果是去往登录页,则直接放行}} else {// 已登录状态verify(token).then(res => {let isVerify = res.value// 判断是否token验证成功,验证成功则跳转要去的路由,否则报错,跳回登录界面if (isVerify) {next()} else {removeToken()setTimeout(() => { next('/login') }, 1500)}}).catch(() => {next('/login') // 异步操作失败后再手动重定向})}})return router
}const router = createRouter()export function resetRouter () {const newRouter = createRouter()router.matcher = newRouter.matcher // reset router
}export default router
通过以上
router.js
和request.js
,即可在跳转页面以及访问后端接口的时候进行拦截验证
🌼🌼🌼🌼🌻🌻🌻🌻🌻🌷🌷🌷🌷🌷🌷🌷🌼🌼🌼🌼🌻🌻🌻🌻🌻
springboot实现
依赖引入
同样,为了实现JWT,我们后端也需要做一些引入,注意:本次引入只涉及到JWT相关,其他自己项目相关请额外进行引入
<dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.4.0</version></dependency>
🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵🌵
JWT工具类
新建工具类,命名JwtTokenUtil
:
代码如下:
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import java.util.Date;public class JwtTokenUtil {//定义token返回头部public static final String AUTH_HEADER_KEY = "Authorization";//token前缀public static final String TOKEN_PREFIX = "Bearer ";//签名密钥public static final String KEY = "q3t6w9z$C&F)J@NcQfTjWnZr4u7x";//有效期默认为 2hourpublic static final Long EXPIRATION_TIME = 1000L * 60 * 60 * 2;/*** 创建TOKEN*/public static String createToken(String content) {return TOKEN_PREFIX + JWT.create().withSubject(content).withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME)).sign(Algorithm.HMAC512(KEY));}/*** 验证token*/public static String verifyToken(String token) throws Exception {try {return JWT.require(Algorithm.HMAC512(KEY)).build().verify(token.replace(TOKEN_PREFIX, "")).getSubject();} catch (TokenExpiredException e) {throw new Exception("token已失效,请重新登录", e);} catch (JWTVerificationException e) {throw new Exception("token验证失败!", e);}}public static Boolean verify(String token) throws Exception {try {JWT.require(Algorithm.HMAC512(KEY)).build().verify(token.replace(TOKEN_PREFIX, "")).getSubject();return true;} catch (Exception e) {return false;}}
}
忽视jwt验证注解
新建一个注解,用于忽视验证,比如登录,注册方法
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {boolean value() default true;
}
🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱🌱
拦截器逻辑
import cn.hutool.json.JSONObject;
import com.pearl.Interface.JwtIgnore;
import com.pearl.utils.JwtTokenUtil;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;@Slf4j
@Component
public class AuthenticationInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)throws Exception {response.setCharacterEncoding("UTF-8");response.setContentType("application/json; charset=utf-8");// 从http请求头中取出tokenfinal String token = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);//如果不是映射到方法,直接通过if (!(handler instanceof HandlerMethod)) {return true;}//如果方法有JwtIgnore注解,直接通过HandlerMethod handlerMethod = (HandlerMethod) handler;Method method = handlerMethod.getMethod();if (method.isAnnotationPresent(JwtIgnore.class)) {JwtIgnore jwtIgnore = method.getAnnotation(JwtIgnore.class);if (jwtIgnore.value()) {return true;}}// 执行认证if (StringUtils.isEmpty(token)) {JSONObject res = new JSONObject();res.put("code", 401);res.put("msg", "无token,请重新登录");res.put("data", false);PrintWriter out = response.getWriter();out.append(res.toString());return false;}if (!JwtTokenUtil.verify(token)) {JSONObject res = new JSONObject();res.put("code", 401);res.put("msg", "token验证失败,请重新登录");res.put("data", false);PrintWriter out = response.getWriter();out.append(res.toString());return false;}return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response,Object handler, Exception ex) throws Exception {}
}
🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳🌳
跨域&调用拦截器配置
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class GlobalWebMvcConfig implements WebMvcConfigurer {/*** 重写父类提供的跨域请求处理的接口*/@Overridepublic void addCorsMappings(CorsRegistry registry) {// 添加映射路径registry.addMapping("/**").allowedOriginPatterns("*") // 允许所有来源.allowCredentials(true) // 允许发送身份验证凭据.allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD").allowedHeaders("*").exposedHeaders("Server", "Content-Length", "Authorization", "Access-Token","Access-Control-Allow-Origin", "Access-Control-Allow-Credentials");}// 添加拦截器,我的项目的基础路径为sso,登录接口路径为/sso/idle/login// addPathPatterns是拦截所有路径,excludePathPatterns是排除需要拦截的路径@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new AuthenticationInterceptor()).addPathPatterns("/**").excludePathPatterns("/sso/idle/login");}
}
🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴🌴
登录接口&验证token接口
import com.pearl.Interface.JwtIgnore;
import com.pearl.entitys.beans.UserLoginData;
import com.pearl.entitys.dataBaseTable.User;
import com.pearl.responseEntity.Response;
import com.pearl.service.LoginService;
import com.pearl.utils.db.PrimeDB;
import java.sql.Connection;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/idle")
public class LoginController {
// 我的数据库连接类@Autowiredprivate PrimeDB primeDB;
// service层,@Resourceprivate LoginService loginService;/*** 登录*/@JwtIgnore@PostMapping("/login")public Response<Map<String, Object>> login(@RequestBody UserLoginData userDto,HttpServletResponse response)throws Exception {try (Connection conn = primeDB.create()) {Map<String, Object> map = loginService.login(conn, userDto, response);return new Response<>(0, map, "登录成功");} catch (Exception e) {return new Response<>(1, e.getMessage());}}@JwtIgnore@GetMapping("/verify")public Response<Boolean> verify(@RequestParam("token") String token) {try {return new Response<>(0, loginService.verify(token), "验证成功!");} catch (Exception e) {return new Response<>(1, false, "验证失败");}}
}
🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾🌾
其中,验证接口的逻辑很简单,单纯调用JWT工具类进行判断即可,而登录方法根据不同的项目,可能各有区别,因此登录逻辑给出来只有参考意义.如下是loginService代码:
import com.alibaba.fastjson.JSONObject;
import com.pearl.db.UserDao;
import com.pearl.entitys.beans.UserLoginData;
import com.pearl.entitys.beans.UserToken;
import com.pearl.entitys.dataBaseTable.User;
import com.pearl.utils.AesUtil;
import com.pearl.utils.AssertUtils;
import com.pearl.utils.JwtTokenUtil;
import java.sql.Connection;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;@Service
public class LoginService {public Map<String, Object> login(Connection conn, UserLoginData userLoginData) throws Exception {try {Map<String, Object> map = new HashMap<>();/*** 校验账号* */UserDao userDao = new UserDao(conn);AssertUtils.notNull(userLoginData, "请求参数不能为空!");AssertUtils.isError(StringUtils.isEmpty(userLoginData.getAccount()), "账号不能为空!");AssertUtils.isError(StringUtils.isEmpty(userLoginData.getPassword()), "密码不能为空!");User user = userDao.selectbyUserId(userLoginData.getAccount());AssertUtils.notNull(user, "该账号不存在!");// 判断账号是否失效AssertUtils.isError(user.getStatus() != 1, "账号:" + user.getUserId() + "已失效!请联系管理员恢复!");// 验证账密Boolean isTruePass = new AuthService().checkPassword(userLoginData.getPassword(), user.getPassword(), user.getSalt());AssertUtils.isError(!isTruePass, "用户名或密码错误!");//TODO 获取用户权限UserToken userToken = new UserToken();BeanUtils.copyProperties(user, userToken);String token = JwtTokenUtil.createToken(JSONObject.toJSONString(userToken));map.put("user", user);map.put("token", token);return map;} catch (Exception e) {throw new Exception(e.getMessage());}}public Boolean verify(String token) throws Exception {try {return JwtTokenUtil.verify(token);} catch (Exception e) {throw new Exception(e.getMessage());}}
}
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上我有封装响应实体,我的响应实体代码如下:
public class Response<T> {public Header header;public T value;public Response() {}public Response(T value) {this.header = new Header();this.value = value;}public Response(int code, Exception ex) {if (ex.getMessage() == null) {this.header = new Header(code, ex.toString());} else {this.header = new Header(code, ex.getMessage());}this.value = null;}public Response(int code, String message) {this.header = new Header(code, message);this.value = null;}public Response(int code, T value, Exception ex) {if (ex.getMessage() == null) {this.header = new Header(code, ex.toString());} else {this.header = new Header(code, ex.getMessage());}this.value = value;}public Response(int code, T value, String message) {this.header = new Header(code, message);this.value = value;}// 请求头,包含响应码和响应提醒信息public static class Header {public int code;public String message;public Header() {this.code = 0;this.message = "";}public Header(int code, String message) {this.code = code;this.message = message;}}
}
如上我的调用登录数据结构如下:
{"header": {"code": 0,"message": "登录成功"},"value": {"user": {"userId": "admin","avatar": null,"userName": "超级管理员","password": "t3zluLHlyip9A8TcXrR05Q==","email": null,"phone": null,"sex": null,"age": 0,"status": 1,"createTime": "2024-04-07 08:11:43","updateTime": "2024-04-07 08:11:43"},"token": "Bearer xxx"}
}
因此前端可获取token数据,进行赋值设置
🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲🌲
结语
以上,为vue+springboot实现JWT登录验证过程