文章目录
- 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
- header+payload+signature
-
关于jwt客户端存储
- 可以存储在cookie,localstorage和sessionStorage里面
(2)用户登录流程图
2.创建Maven项目,搭建SpringBoot项目
(1)添加maven依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>compile</scope>
</dependency>
</dependencies>
(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 Transfer
Source Server : mysql_test
Source Server Type : MySQL
Source Server Version : 80027
Source Host : 192.168.139.100:3306
Source Schema : user
Target Server Type : MySQL
Target Server Version : 80027
File Encoding : 65001
Date: 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依赖
<!--mybatis plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.18</version>
</dependency>
(2)配置yml文件
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.139.100:3306/user?allowPublicKeyRetrieval=true&characterEncoding=UTF-8&allowMultiQueries=true&useUnicode=true&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
druid:
initial-size: 8
max-active: 20
max-wait: 60000
min-evictable-idle-time-millis: 30000
(3)启动主类添加MapperScan()注解
@MapperScan("com.lixiang.mapper")
(4)启动验证
6.MyBatisPlus逆向工程自动生成
(1)加入maven依赖
<!-- 代码自动生成依赖 begin -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!-- 代码自动生成依赖 end-->
(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依赖
<!-- JWT相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
(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<String,Object> 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<String,Object> 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<String, Object> 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<String,Object> 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<String, Object> login(LoginReq req);
}
(3)login实现类编写
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public Map<String, Object> login(LoginReq req) {
UserDO user = userMapper.selectOne(new QueryWrapper<UserDO>().eq("phone", req.getPhone()));
Map<String,Object> 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
@Autowired
private 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<LoginUser> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//从请求头中拿token
String accessToken = request.getHeader("token");
//从请求参数中拿token
if (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<String,Object> 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 {
@Bean
public LoginInterceptor loginInterceptor() {
return new LoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
.addPathPatterns("/user/*")
//排查不拦截的路径
.excludePathPatterns("/user/login","/user/product_list");
}
}
配置拦截器使用户登录的接口和商品列表查询的接口不进行token验证,将用户的信息放在ThreadLocal中保证每个线程独立内存空间。
11.启动验证
至此,整个登录整合JWT功能已经开发完成,这块其实还可以根据自己的业务去返回一个RefreshToken,Token过期刷新的token。