【案例实战】分布式应用下登录检验解决方案(JWT)
创始人
2024-01-25 01:21:36
0

文章目录

        • 1.需求背景以及JWT简介
        • 2.创建Maven项目,搭建SpringBoot项目
        • 3.容器化急速部署MySQL
        • 4.数据库表准备
        • 5.SpringBoot整合MySQL+MyBatisPlus
        • 6.MyBatisPlus逆向工程自动生成
        • 7.SpringBoot整合JWT
        • 8.开发测试接口
        • 9.开发登录接口
        • 10.开发登录拦截器
        • 11.启动验证

1.需求背景以及JWT简介

  • 现实场景中,有些功能是需要登录才能访问的,比如购物车,个人订单等等。登录功能是最常见的功能。

(1)什么是JWT

  • JWT 是一个开放标准,它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。

  • 简单来说: 就是通过一定规范来生成token,然后可以通过解密算法逆向解密token,这样就可以获取用户信息

  • 优点

    • 生产的token可以包含基本信息,比如id、用户昵称、头像等信息,避免再次查库
    • 存储在客户端,不占用服务端的内存资源
  • 缺点

    • token是经过base64编码,所以可以解码,因此token加密前的对象不应该包含敏感信息,如用户权限,密码等

    • 如果没有服务端存储,则不能做登录失效处理,除非服务端改秘钥

  • JWT格式组成 头部、负载、签名

    • header+payload+signature
      • 头部:主要是描述签名算法
      • 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
      • 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
  • 关于jwt客户端存储

    • 可以存储在cookie,localstorage和sessionStorage里面

(2)用户登录流程图

在这里插入图片描述

2.创建Maven项目,搭建SpringBoot项目

(1)添加maven依赖

    org.springframework.bootspring-boot-starter-parent2.6.7 org.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-testtestorg.projectlomboklombok1.18.20compile

(2)创建yml配置文件

server:port: 8001
spring:application:name: login-server

(3)创建运行主类

@SpringBootApplication
public class LoginApplication {public static void main(String[] args) {SpringApplication.run(LoginApplication.class, args);}
}

3.容器化急速部署MySQL

(1)创建目录

mkdir -p /usr/local/docker/mysql/conf
mkdir -p /usr/local/docker/mysql/logs
mkdir -p /usr/local/docker/mysql/data

(2)容器启动mysql服务

docker run -p 3306:3306 --name mysql \ 
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:8.0
#查看容器
docker ps

在这里插入图片描述

(3)可视化工具连接

在这里插入图片描述

4.数据库表准备

(1)创建数据库user库

在这里插入图片描述

(2)创建测试用户表

在这里插入图片描述

  • 创建表sql脚本
/*Navicat Premium Data TransferSource Server         : mysql_testSource Server Type    : MySQLSource Server Version : 80027Source Host           : 192.168.139.100:3306Source Schema         : userTarget Server Type    : MySQLTarget Server Version : 80027File Encoding         : 65001Date: 15/11/2022 09:14:11
*/SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for user-- ----------------------------DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (`id` int(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID',`username` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '用户名',`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '密码',`phone` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '手机号',`name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '用户姓名',`sex` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '用户性别',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',`age` int(0) NULL DEFAULT NULL COMMENT '年龄',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;-- ------------------------------ Records of user-- ----------------------------SET FOREIGN_KEY_CHECKS = 1;

(3)创建测试登录用户

  • **注意:**这里只是做演示,正常企业密码不会设置明文的,按照企业自己的加密方式去加密密码,我们现在主要是为了开发登录鉴权这一套流程。
INSERT INTO `user`.`user`(`id`, `username`, `password`, `phone`, `name`, `sex`, `create_time`, `age`) VALUES (1, 'lixiang', '1234567890', '13830567835', '李祥', '男', '2022-11-15 09:19:26', 18);

5.SpringBoot整合MySQL+MyBatisPlus

(1)添加maven依赖

        com.baomidoumybatis-plus-boot-starter3.4.0mysqlmysql-connector-javacom.alibabadruid-spring-boot-starter1.1.18

(2)配置yml文件

  datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://192.168.139.100:3306/user?allowPublicKeyRetrieval=true&characterEncoding=UTF-8&allowMultiQueries=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456type: com.alibaba.druid.pool.DruidDataSourcedruid:initial-size: 8max-active: 20max-wait: 60000min-evictable-idle-time-millis: 30000

(3)启动主类添加MapperScan()注解

@MapperScan("com.lixiang.mapper")

(4)启动验证

在这里插入图片描述

6.MyBatisPlus逆向工程自动生成

(1)加入maven依赖

        com.baomidoumybatis-plus-generator3.4.1org.apache.velocityvelocity-engine-core2.0

(2)运行代码

package com.lixiang.db;import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;/*** @description mybatis自定生成工具*/
public class MyBatisPlusGenerator {public static void main(String[] args) {//1. 全局配置GlobalConfig config = new GlobalConfig();// 是否支持AR模式config.setActiveRecord(true)// 作者.setAuthor("lixiang")// 生成路径,最好使用绝对路径,window路径是不一样的//TODO  TODO  TODO  TODO.setOutputDir("D:\\IDEAWork\\springboot-login\\src\\test\\java")// 文件覆盖.setFileOverride(true)// 主键策略.setIdType(IdType.AUTO).setDateType(DateType.ONLY_DATE)// 设置生成的service接口的名字的首字母是否为I,默认Service是以I开头的.setServiceName("%sService")//实体类结尾名称.setEntityName("%sDO")//生成基本的resultMap.setBaseResultMap(true)//不使用AR模式.setActiveRecord(false)//生成基本的SQL片段.setBaseColumnList(true);//2. 数据源配置DataSourceConfig dsConfig = new DataSourceConfig();// 设置数据库类型dsConfig.setDbType(DbType.MYSQL).setDriverName("com.mysql.cj.jdbc.Driver")//TODO 修改数据库对应的配置.setUrl("jdbc:mysql://121.36.81.39:3306/user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai").setUsername("root").setPassword("123456");//3. 策略配置globalConfiguration中StrategyConfig stConfig = new StrategyConfig();//全局大写命名stConfig.setCapitalMode(true)// 数据库表映射到实体的命名策略.setNaming(NamingStrategy.underline_to_camel)//使用lombok.setEntityLombokModel(true)//使用RestController注解.setRestControllerStyle(true)// 生成的表, 支持多表一起生成,以数组形式填写//TODO  TODO  TODO  TODO.setInclude("user");//4. 包名策略配置PackageConfig pkConfig = new PackageConfig();pkConfig.setParent("com.lixiang").setMapper("mapper").setService("service").setController("controller").setEntity("model").setXml("mapper");//5. 整合配置AutoGenerator ag = new AutoGenerator();ag.setGlobalConfig(config).setDataSource(dsConfig).setStrategy(stConfig).setPackageInfo(pkConfig);//6. 执行操作ag.execute();System.out.println("=======  相关代码生成完毕  ========");}
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

(3)启动验证

在这里插入图片描述

7.SpringBoot整合JWT

(1)添加maven依赖

            io.jsonwebtokenjjwt0.7.0

(2)编写登录用户类

/*** 登录user实体bean* @author lixiang* @since 2022-01-13*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser {/*** 主键*/private Long id;/*** 用户名*/private String username;/*** 姓名*/private String name;/*** 手机号*/private String phone;/*** 用户性别*/private String sex;/*** 年龄*/private Integer age;}

(3)编写JWTUtil

/*** JWT工具类* @author lixiang* @since 2022-01-13*/
@Slf4j
public class JWTUtil {/*** token过期时间,正常是7天*/private static final long EXPIRE = 1000L * 60 * 60 * 24 * 7;/*** 加密的密钥*/private static final String SECRET = "lixiang.com";/*** 令牌前缀*/private static final String TOKEN_PREFIX = "LONGIN-TEST";/*** subject 颁布地址*/private static final String SUBJECT = "lixiang";/*** 根据用户信息生成token* @param loginUser* @return*/public static Map geneJsonWebToken(LoginUser loginUser){if(loginUser == null){throw new NullPointerException("loginUser对象为空");}Date endDate = new Date(System.currentTimeMillis() + EXPIRE);String token = Jwts.builder().setSubject(SUBJECT).claim("age",loginUser.getAge()).claim("id",loginUser.getId()).claim("name",loginUser.getName()).claim("phone",loginUser.getPhone()).claim("sex",loginUser.getSex()).setIssuedAt(new Date()).setExpiration(endDate).signWith(SignatureAlgorithm.HS256,SECRET).compact();token = TOKEN_PREFIX+token;Map map = new HashMap<>();map.put("accessToken",token);map.put("accessTokenExpires",endDate);return map;}/*** 检验token方法* @param token* @return*/public static Claims checkJWT(String token){try{return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();}catch (Exception e){log.info("JWT token解密失败");return null;}}}

(4)测试JWT,编写测试方法

public class Main {public static void main(String[] args) {LoginUser loginUser = LoginUser.builder().age(18).id(1L).name("李祥").phone("13820934720").sex("男").username("lixiang").build();Map objectMap = JWTUtil.geneJsonWebToken(loginUser);System.out.println("LoginUser加密:");objectMap.forEach((k,v)->{System.out.println("---key:"+k+",value:"+v);});String accessToken = String.valueOf(objectMap.get("accessToken"));System.out.println("Token解密:");Claims claims = JWTUtil.checkJWT(accessToken);System.out.println("---name:"+claims.get("name"));System.out.println("---age:"+claims.get("age"));System.out.println("---phone:"+claims.get("phone"));System.out.println("---username:"+claims.get("username"));System.out.println("---sex:"+claims.get("sex"));}
}

在这里插入图片描述

8.开发测试接口

  • 开发两个接口,我们的目的是一个用于不需要登录就能访问,一个需要登录才能访问

  • 查看商品信息列表,查看个人订单信息两个接口

  • 开发测试的UserController,这块全部用的测试接口,主要是给大家演示效果,现在我们想让商品列表的接口可以随便访问,订单列表的接口只有用户登录之后才能访问。

/*** @description 测试Controller* @author lixiang*/
@RestController
@RequestMapping("/user")
public class UserController {/*** 查询商品列表* @return*/@GetMapping("/product_list")public Object getProductList(){return getResult(200,"查询商品列表");}/*** 查询订单列表* @return*/@GetMapping("/order_list")public Object getOrderList(){return getResult(200,"查询订单列表");}/*** 测试返回结果* @param code* @param msg* @return*/private Object getResult(int code, String msg) {Map result = new HashMap<>();result.put("code",code);result.put("msg",msg);return result;}}
  • 测试

在这里插入图片描述

在这里插入图片描述

  • 两个接口访问正常,但是对于订单接口我们是想增加Token检验,才会给予访问,这块我们就需要写一个拦截器,但是我们现在应该先去开发一下登录的接口。

9.开发登录接口

  • 这块我们采用手机号和密码登录

(1)创建登录请求类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginReq {/*** 手机号*/private String phone;/*** 密码*/private String password;}

(2)创建login方法在UserServie

public interface UserService {/*** 登录方法* @param req* @return*/Map login(LoginReq req);}

(3)login实现类编写

@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic Map login(LoginReq req) {UserDO user = userMapper.selectOne(new QueryWrapper().eq("phone", req.getPhone()));Map result = new HashMap<>();//判断是否已经注册的if (user == null) {//未注册result.put("code",10000);result.put("msg","用户未注册");return result;}if (req.getPassword().equals(user.getPassword())) {//登录成功,生成token,UUID生成token,存储到redis中并设置过期时间LoginUser loginUser = LoginUser.builder().build();BeanUtils.copyProperties(user, loginUser);return JWTUtil.geneJsonWebToken(loginUser);}result.put("code",10000);result.put("msg","密码错误");return result;}
}

(4)编写Controller

    @Autowiredprivate UserService userService;/*** 登录* @return*/@PostMapping("/login")public Object login(@RequestBody LoginReq req){return userService.login(req);}

(5)测试登录接口

在这里插入图片描述

10.开发登录拦截器

  • 登录接口开发完成了,那么我们需要开发一个登录拦截器。
/*** 全局登录拦截器* @author lixiang*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {public static ThreadLocal threadLocal = new ThreadLocal<>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//从请求头中拿tokenString accessToken = request.getHeader("token");//从请求参数中拿tokenif (accessToken == null){accessToken = request.getParameter("token");}if(accessToken!=null && !accessToken.equals("")){//不为空,判断是否登录过期Claims claims = JWTUtil.checkJWT(accessToken);if (claims == null){sendJsonMessage(response, "账号已过期");return false;}Long userId = Long.valueOf(claims.get("id").toString());String headImg = (String) claims.get("username");String name = (String) claims.get("name");String phone = (String) claims.get("phone");String sex = (String) claims.get("sex");Integer age = (Integer) claims.get("age");//设置LoginUser对象属性,建造者模式LoginUser loginUser = LoginUser.builder().name(name).username(headImg).id(userId).phone(phone).sex(sex).age(age).build();//通过threadLocal共享用户登录信息threadLocal.set(loginUser);return true;}sendJsonMessage(response, "账号未登录");return false;}/**** @param response* @param msg*/private void sendJsonMessage(HttpServletResponse response, String msg) {Map result = new HashMap<>();result.put("code",10000);result.put("msg",msg);ObjectMapper objectMapper = new ObjectMapper();response.setContentType("application/json;charset=utf-8");PrintWriter writer = null;try {writer = response.getWriter();writer.print(objectMapper.writeValueAsString(result));response.flushBuffer();} catch (IOException e) {log.warn("响应json数据给前端异常");}finally {if(writer!=null){writer.close();}}}
}

(2)配置接口拦截

/*** 登录拦截配置类* @author lixiang*/
@Configuration
@Slf4j
public class InterceptorConfig implements WebMvcConfigurer {@Beanpublic LoginInterceptor loginInterceptor() {return new LoginInterceptor();}@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor()).addPathPatterns("/user/*")//排查不拦截的路径.excludePathPatterns("/user/login","/user/product_list");}
}

配置拦截器使用户登录的接口和商品列表查询的接口不进行token验证,将用户的信息放在ThreadLocal中保证每个线程独立内存空间。

11.启动验证

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

至此,整个登录整合JWT功能已经开发完成,这块其实还可以根据自己的业务去返回一个RefreshToken,Token过期刷新的token。

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...