✨✨个人主页:沫洺的主页
系列专栏: JavaWeb专栏 JavaSE专栏 Java基础专栏vue3专栏
MyBatis专栏Spring专栏SpringMVC专栏SpringBoot专栏
Docker专栏Reids专栏MQ专栏SpringCloud专栏
如果文章对你有所帮助请留下三连✨✨
简单的用户登录注册环境的搭建,目的是使用JWT(Json web token)来模拟分布式站点的单点登录(SSO)场景
这里模拟了三种token的情况
- 客户端使用用户名跟密码请求登录;
- 服务端收到请求,去验证用户名与密码;
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端;
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token;
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。
原文链接: 什么是 JWT -- JSON WEB TOKEN
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
传统的session认证:
- 当用户经过应用认证之后,应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。
- 如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
- 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。
基于token的鉴权机制:
- 基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。
JWT详解参考: JWT详解
总结来说就是服务无状态化,服务器不保存任何状态,而是客户端将状态带过来这种机制
Hutool工具: JWTUtil使用
JSON Web Token 入门教程
建表
CREATE TABLE `scm_user` ( `id` int NOT NULL AUTO_INCREMENT, `tel` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, `nickName` varchar(255) DEFAULT NULL, `lastUpdateTime` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;;
- 客户端使用用户名跟密码请求登录;
- 服务端收到请求,去验证用户名与密码;
- 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端;
application.yml
#数据库连接信息配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/db5?useSSL=false&useServerPrepStmts=true&allowPublicKeyRetrieval=true username: root password: 123456 druid: initial-size: 10 # 初始化时建立物理连接的个数 min-idle: 10 # 最小连接池数量 maxActive: 200 # 最大连接池数量 maxWait: 60000 # 获取连接时最大等待时间,单位毫秒 #映射文件所在位置 #mybatis: # mapper-locations: classpath:mapper/*Mapper.xml # #别名 # type-aliases-package: com.moming.entity mybatis-plus: config-locations: classpath:mapper/*Mapper.xml #别名 type-aliases-package: com.moming.entity #配置日志级别 logging: level: # root: error com.moming: info rest: controller: advice: base-packages: com.moming.controller #配置authority-spring-boot-starter相关信息 moming: authority: enable: true path-patterns: /api/** exclude-path-patterns: /api/user/* #设置token密钥 token: secret: moming
UserDto
@Data public class UserDto{ private Integer id; private String tel; private String nickName; private String password; }
UserQueryDto
@Data public class UserQueryDto{ private Integer id; private String tel; private String password; }
UserLoginDto
@Data public class UserLoginDto { private String tel; private String password; }
UserEntity
@Data public class UserEntity { private Integer id; private String tel; private String nickName; private String password; private LocalDateTime lastUpdateTime; }
UserMapper
@Mapper public interface UserMapper { List
select(UserQueryDto dto); int insert(UserEntity entity); int update(UserEntity entity); int delete(Integer id); } UserMapper.xml
insert into scm_user ( tel,nickName, password) values ( #{tel}, #{nickName}, #{password} ) update scm_user where id = #{id} tel = #{tel}, password = #{password}, delete from scm_user where id = #{id} UserService
@Service public class UserService { @Autowired private UserMapper userMapper; public Integer insert(UserDto dto){ //业务判断逻辑 UserEntity entity = BeanUtil.copyProperties(dto, UserEntity.class); return userMapper.insert(entity); } public Integer update(UserDto dto){ UserEntity entity = BeanUtil.copyProperties(dto, UserEntity.class); return userMapper.update(entity); } public List
select(UserQueryDto dto){ List entities = userMapper.select(dto); return BeanUtil.copyToList(entities,UserDto.class); } } UserController
@RestController @RequestMapping("/api/user") public class UserController { @Autowired private UserService userService; //密钥 @Value("${moming.token.secret}") private String TOKEN_SECRET; @PutMapping public Integer update(@RequestBody UserDto dto){ return userService.update(dto); } @PostMapping("/regist") public Integer insert(@RequestBody UserDto dto){ return userService.insert(dto); } @GetMapping public List
select(UserQueryDto dto){ return userService.select(dto); } @PostMapping("/login") public String login(@RequestBody UserLoginDto dto) throws Exception { //业务判断 //获取请求头数据是否为空 if(ObjectUtil.isEmpty(dto.getTel())){ throw new Exception("手机号不能为空"); } if(ObjectUtil.isEmpty(dto.getPassword())){ throw new Exception("密码不能为空"); } //不为空封装到对象中 UserQueryDto queryDto = new UserQueryDto(); queryDto.setTel(dto.getTel()); queryDto.setPassword(dto.getPassword()); //查询数据库中是否匹配登录信息 List userList = userService.select(queryDto); //如果查出来的是空的,则表示没有匹配上,登录失败 if(ObjectUtil.isEmpty(userList)){ throw new Exception("手机号或者密码错误"); } //匹配上,由于是数组对象,取第一个 UserDto user = userList.get(0); //创建JWT Map map = new HashMap () ; //用户id map.put("userId", user.getId()); //用户手机号 //String tel = user.getTel(); //tel = tel.substring(0,3)+"******"+tel.substring(9,11); //map.put("tel", tel); map.put("tel", user.getTel()); //用户别名 map.put("nickName", user.getNickName()); //过期时间 map.put("expire_time", System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 15); //生成token String token = JWTUtil.createToken(map, TOKEN_SECRET.getBytes()); return token; } } 当登录接口成功通过业务判断,会返回一个加密的token,格式如下
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHBpcmVfdGltZSI6MTY2ODIzMDgyNTIyMCwidGVsIjoiMTIzKioqKioqMTAiLCJ1c2VySWQiOjF9.t1E8YfUuMVWpCfAtIw_i4MWZftSpr8tmlGTHphuv-Do
在线解析JWT加密
- 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里;
vue token解析: 解析token中携带的数据
安装插件
npm install jwt-decode --save
在需要使用的地方引入
import jwtDecode from 'jwt-decode' const code = jwtDecode(res.data.data.accessToken) console.log(code)// 就可以解析成功了
部分核心代码如下
UserLogin.vue
SCM 用户登录登录 重置 注册 UserRegist.vue
SCM 用户注册注册 重置 App.vue
SCM管理系统 {{ userInfo.nickName }} 菜单一 菜单二 菜单三 退出系统 appStore.ts
import { defineStore } from 'pinia' export const appStore = defineStore({ id: 'scmapp', state: () => { return { userInfo:{ userId: 0, tel: "", nickName: "", token: "", }, menuCollapse: false, //定义一个editableTabs数组,里边放初始化元素 tabList: [{ title: '主页', name: 'home', close: false, }], //当前活动标签,默认显示name:'home'的元素 activeTabName: "home", } }, getters: { }, actions: { }, // 开启数据缓存 persist: { enabled: true, strategies: [ { key: 'scm_app', storage: localStorage, } ] } })
将用户登录不重要的信息通过pinia保存到本地存储Local Storage中,通过消息的订阅与发布这种通信模式,登录成功后UserLogin.vue作为发布者将token发给订阅者,通知App.vue订阅者去解析token
//发布者,发送消息 PubSub.publish('login-ok', token) //订阅者接收登录成功的消息事件 PubSub.subscribe('login-ok', (topic: string, data: any) => { // console.log(topic,data); let token = data; //解析token(JWT数据结构) const tokenObject:any = jwtDecode(token) //存入本地存储 userInfo.value.token = token userInfo.value.userId = tokenObject.userId userInfo.value.tel = tokenObject.tel userInfo.value.nickName = tokenObject.nickName })
JSON Web Token 入门教程
http/index.ts
import axios, { AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, } from "axios"; //弹窗图标和加载图标 import { ElMessage, ElLoading } from "element-plus"; // import { userInfo } from "os"; import { appStore } from "@/store/appStore" import { storeToRefs } from 'pinia' let { userInfo } = storeToRefs(appStore()); const state = { ok: 0,//请求成功状态码 401: "ERR_BAD_REQUEST" }; //返回数据规则 interface IResponseData
{ status: number; message?: string; data: T; code: string; } //默认配置,封装了一个实例对象 const config = { baseURL: "", timeout: 30 * 1000, withCredentials: true, }; let loading: any = null; //类似定义一个类 class Http { axiosInstance; //定义了一个axiosInstance属性,未来它放的是一个axios实例 constructor(config: any) { //实例化请求配置 this.axiosInstance = axios.create(config); // 添加请求拦截器 this.axiosInstance.interceptors.request.use( //在发送请求之前做些什么 (config: AxiosRequestConfig) => { //弄了一个加载的过度 loading = ElLoading.service({ lock: true, text: '加载中...', background: 'rgba(0, 0, 0, 0.7)', //覆盖整个屏幕 fullscreen: true }) //处理token if (userInfo.value.token != "") { let headers = config.headers as AxiosRequestHeaders headers.Authorization = userInfo.value.token } return config; }, (error: any) => { loading.close(); // 对请求错误做些什么 return Promise.reject(error); } ); // 添加响应拦截器 this.axiosInstance.interceptors.response.use(function (response) { // 对响应数据做点什么 loading.close(); let apiData = response.data; // console.log(apiData) // console.log(apiData.data) //将apiData的属性取出来 const { code, message, data } = apiData; //取出来之后处理属性 if (code === undefined) { return apiData; } else if (code === 0) { return data; } else { ElMessage.error(message) } return apiData.data; }, function (error) { // 对响应错误做点什么 loading.close(); return Promise.reject(error); }); } get (url: string, params?: object, data = {}): Promise > { return this.axiosInstance.get(url, { params, ...data }); } post (url: string, params?: object, data = {}): Promise > { return this.axiosInstance.post(url, params, data); } put (url: string, params?: object, data = {}): Promise > { return this.axiosInstance.put(url, params, data); } patch (url: string, params?: object, data = {}): Promise > { return this.axiosInstance.patch(url, params, data); } delete (url: string, params?: object, data = {}): Promise > { return this.axiosInstance.delete(url, { params, ...data }); } } //类似new了一个实例 export default new Http(config); 请求拦截器
将token放到请求头的Authorization中
// 添加请求拦截器 this.axiosInstance.interceptors.request.use( //在发送请求之前做些什么 (config: AxiosRequestConfig) => { //弄了一个加载的过度 loading = ElLoading.service({ lock: true, text: '加载中...', background: 'rgba(0, 0, 0, 0.7)', //覆盖整个屏幕 fullscreen: true }) //处理token if (userInfo.value.token != "") { //拿到请求头 let headers = config.headers as AxiosRequestHeaders //赋值 headers.Authorization = userInfo.value.token } return config; }, (error: any) => { loading.close(); // 对请求错误做些什么 return Promise.reject(error); } );
- 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token;
- 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据。
拦截器创建参考: 自定义authority-spring-boot-starter
引入坐标
com.moming authority-spring-boot-starter 14-SNAPSHOT application.yml
#配置authority-spring-boot-starter相关信息 moming: authority: enable: true path-patterns: /api/** exclude-path-patterns: /api/user/* #设置token密钥 token: secret: moming
UserInfo
@Data public class UserInfo { private Integer userId; private String tel; private String nickName; }
UserInfoLocal
public class UserInfoLocal { //线程独有变量 private static ThreadLocal
threadLocal = new InheritableThreadLocal<>(); //给线程脑门上设置给变量 public static void set(UserInfo userInfo){ threadLocal.set(userInfo); } public static UserInfo get(){ return threadLocal.get(); } } AuthorityAutoConfiguration
@ConditionalOnProperty(prefix = AuthorityAutoConfiguration.AUTHORITY_PRE,value = "enable", havingValue = "true", matchIfMissing = false) @Import(WebConfig.class) public class AuthorityAutoConfiguration { public final static String AUTHORITY_PRE="moming.authority"; }
AuthorityProperties
@Data @ConfigurationProperties(prefix = AuthorityProperties.AUTHORITY_PRE) public class AuthorityProperties { public final static String AUTHORITY_PRE="moming.authority"; /* 是否开启 */ private boolean enable ; /* 可以自定义配置拦截的路径 */ private String pathPatterns="/api/**"; /* 配置不拦截的路径 */ private String excludePathPatterns=""; }
AuthorityInteceptor拦截器通过密钥验证请求头传过来的Authorization(token)是否合法
public class AuthorityInteceptor implements HandlerInterceptor { //密钥 private String TOKEN_SECRET; public AuthorityInteceptor(String tokenSecret){ this.TOKEN_SECRET = tokenSecret; } @Override public boolean preHandle(HttpServletRequest request, javax.servlet.http.HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); //token为空时 if(StrUtil.isEmpty(token)){ //response.sendError(response.SC_UNAUTHORIZED); response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\": 401,\"message\": \"token为空或已过期,请重新登录\"}"); return false; } //验证token //将系统提示的异常抛出,使用自定义的提示信息message //校验是否正确(合法) boolean isTrue = false; try { //验证token,与密钥是否 isTrue = JWTUtil.verify(token, TOKEN_SECRET.getBytes()); } catch (Exception ex){ ex.printStackTrace(); } //验证不通过 if (isTrue == false) { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("{\"code\":401,\"message\":\"您的token不正确或已过期\"}"); return false; } //token验证通过解析token final JWT jwt = JWTUtil.parseToken(token); //System.out.println(jwt.getPayload().toString()); //解析后获取PAYLOAD转换成UserInfo对象 UserInfo userInfo = JSONUtil.toBean(jwt.getPayload().toString(), UserInfo.class); //给本地线程脑门上加个属性userInfo //目的是通过获取线程脑门上的属性userInfo来获取登录用户的信息 UserInfoLocal.set(userInfo); //当前线程名称 //System.out.println("111-"+Thread.currentThread().getName()); return true; } }
WebConfig
@Import(AuthorityProperties.class) @Order(Ordered.HIGHEST_PRECEDENCE) public class WebConfig implements WebMvcConfigurer { @Autowired private AuthorityProperties authorityProperties; //密钥 @Value("${moming.token.secret}") private String TOKEN_SECRET; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthorityInteceptor(TOKEN_SECRET)) .addPathPatterns(authorityProperties.getPathPatterns()) .excludePathPatterns(authorityProperties.getExcludePathPatterns()); } }
通过对当前线程脑门上贴个东西,在controller中就可以获取到当前线程脑门上的东西
如下
//改 @PutMapping public int update(@RequestBody ProductDto productDto){ //获取当前线程脑门上的东西userInfo UserInfo userInfo = UserInfoLocal.get(); //给更新人赋值 productDto.setLastUpdateBy(userInfo.getNickName()); return productService.update(productDto); } //插 @PostMapping public int insert(@RequestBody ProductDto productDto){ //获取线程脑门上的变量,给更新人赋值 UserInfo userInfo = UserInfoLocal.get(); productDto.setLastUpdateBy(userInfo.getNickName()); return productService.insert(productDto); }
这样在执行添加,编辑这些操作时就能获取当前登录用户的别名
如下图所示