关于JWT的讲解请参考:SpringCloud第14讲:(番外篇)JWT
一、项目演示
没有登陆直接请求列表接口,系统会要求先进行登录
登录成功后请求列表接口,可以正常响应数据
二、后台开发
2.1、pom.xml
添加redis、jwt坐标
<!--jwt坐标-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
2.2、application.yml
配置redis
server:
port: 8070
spring:
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.8:3306/test_plus?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true
username: root
password: Aa123123.
jackson:
date-format: yyyy-MM-dd HH:mm:ss
redis:
port: 6379 # Redis服务器连接端口
host: 127.0.0.1 # Redis服务器地址
database: 0 # Redis数据库索引(默认为0)
password: # Redis服务器连接密码(默认为空)
timeout: 5000ms # 连接超时时间(毫秒)
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
mybatis-plus:
type-aliases-package: demo.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
table-prefix: t_
id-type: auto
mapper-locations: classpath:mappers/*.xml
2.3、RedisConfig
解决redisTemplate保存数据出现乱码问题
package demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
//创建RedisTemplate对象
RedisTemplate<String, Object> template = new RedisTemplate<>();
//设置连接工厂
template.setConnectionFactory(connectionFactory);
//创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//设置key的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
//设置value的序列化
template.setValueSerializer(jsonRedisSerializer);
template.setHashKeySerializer(jsonRedisSerializer);
//返回
return template;
}
}
2.4、实体层开发
package demo.entity;
import lombok.Data;
@Data
public class User {
private Long id;
private String userName;
private String passwd;
}
package demo.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class Article {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String title;
private String logo;
private String descn;
private Date createTime;
private Long cid;
}
2.5、数据库访问层开发
UserMapper
package demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import demo.entity.User;
import org.springframework.stereotype.Repository;
@Repository
public interface UserMapper extends BaseMapper<User> {
}
ArticleMapper
package demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import demo.entity.Article;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
@Repository
public interface ArticleMapper extends BaseMapper<Article> {
}
2.6、控制层
package demo.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import demo.entity.Article;
import demo.mapper.ArticleMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ArticleController {
@Autowired
private ArticleMapper articleMapper;
@GetMapping("/list")
public Page<Article> findAll(Long pageIndex, Long pageSize){
Page<Article> page = articleMapper.selectPage(new Page(pageIndex, pageSize), null);
return page;
}
}
在登陆成功之后,生成token并保存到redis
package demo.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import demo.entity.User;
import demo.mapper.UserMapper;
import demo.utils.Cast;
import demo.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
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.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Autowired(required = false)
private RedisTemplate<String, String> redisTemplate;
@Autowired
private UserMapper userMapper;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public Object login(@RequestBody User vo){
String token = null;
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper
.eq("user_name", vo.getUserName())
.eq("passwd", vo.getPasswd());
User usr = userMapper.selectOne(queryWrapper);
if(usr != null){
try {
//登陆成功,生成token保存到reids中
token = jwtUtils.createJwtToken(usr.getUserName());
redisTemplate.boundValueOps(Cast.JWT_TOKEN_PREFIX+usr.getUserName()).set(token, 30, TimeUnit.MINUTES);
} catch (Exception e) {
e.printStackTrace();
}
}
return token;
}
}
2.7、工具类
公有常量类
package demo.utils;
/**
* 公有常量
*/
public class Cast {
public static final String JWT_HEADER_KEY = "x-jwt-token"; //请求头中的jwt
public static final String JWT_TOKEN_PREFIX = "login:token:"; //保存在redis中用于校验jwt的key的前缀
}
Jwt工具类
package demo.utils;
import com.nimbusds.jose.*;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import lombok.SneakyThrows;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* jwt工具类
*/
@Component
public class JwtUtils {
//使用uuid生成密钥
private final String secret= UUID.randomUUID().toString();
//用户数据的key
private final String usernameKey="usernameKey";
/**
* 生成token
* @param username 用户名
* @return
*/
public String createJwtToken(String username) throws Exception {
//创建头部对象
JWSHeader jwsHeader =
new JWSHeader.Builder(JWSAlgorithm.HS256) // 加密算法
.type(JOSEObjectType.JWT) // 静态常量
.build();
//创建载荷
Map<String,Object> map=new HashMap<String,Object>();
map.put(usernameKey, username);
Payload payload= new Payload(map);
//创建签名器
JWSSigner jwsSigner = new MACSigner(secret);//密钥
//创建签名
JWSObject jwsObject = new JWSObject(jwsHeader, payload);// 头部+载荷
jwsObject.sign(jwsSigner);//再+签名部分
//生成token字符串
return jwsObject.serialize();
}
/**
* 验证jwt token是否合法
* @param jwtStr
* @return
*/
@SneakyThrows
public boolean verify(String jwtStr) {
JWSObject jwsObject=JWSObject.parse(jwtStr);
JWSVerifier jwsVerifier=new MACVerifier(secret);
return jwsObject.verify(jwsVerifier);
}
/**
* 从token中解析出用户名
* @param jwtStr
* @return
*/
@SneakyThrows
public String getUserNameFormJwt(String jwtStr){
JWSObject jwsObject=JWSObject.parse(jwtStr);
Map<String,Object> map=jwsObject.getPayload().toJSONObject();
return (String) map.get(usernameKey);
}
}
2.8、拦截器
在拦截器中拦截HttpRequest请求,执行以下业务逻辑
- 1、判断请求头中是否携带token
- 2、判断token是否有效
- 3、判断token中保存的用户信息是否有效
- 4、判断请求头的token和redis中的token是否一致
- 5、请求拦截或token续期
package demo.interceptor;
import demo.entity.User;
import demo.mapper.UserMapper;
import demo.utils.Cast;
import demo.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
public class MyInterceptor extends WebMvcConfigurerAdapter {
@Autowired(required = false)
private RedisTemplate<String, String> redisTemplate;
@Autowired
private UserMapper userMapper;
@Autowired
private JwtUtils jwtUtils;
@Override
public void addInterceptors(InterceptorRegistry registry) {
HandlerInterceptor handlerInterceptor = new HandlerInterceptor() {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
boolean isOk = false;
String uri = request.getRequestURI();
//静态资源放行
if(uri.contains("js") || uri.contains("html") || uri.contains("css") || uri.contains("login") || uri.equals("/favicon.ico") || uri.equals("/error")){
isOk = true;
} else { //用户鉴权
String token = request.getHeader(Cast.JWT_HEADER_KEY);
//验证请求头中的token是否合法
if(token != null && jwtUtils.verify(token)){
// token合法则取出token中保存的账号
String account = jwtUtils.getUserNameFormJwt(token);
//根据账号验证redis中保存的token是否存在
if(redisTemplate.hasKey(Cast.JWT_TOKEN_PREFIX+account)){
String jwtToken = (String) redisTemplate.opsForValue().get(Cast.JWT_TOKEN_PREFIX+account);
//判断是否是同一个token
if(token.equals(jwtToken)){
//对token进行续期
redisTemplate.opsForValue().set(Cast.JWT_TOKEN_PREFIX+account, jwtToken, 30, TimeUnit.MINUTES);
isOk = true;
}
}
}
if(!isOk){
response.setContentType("application/json;charset=utf8");
PrintWriter writer = response.getWriter();
writer.print("{\"code\":403, \"msg\":\"没有访问权限\"}");
isOk = false;
}
}
return isOk;
}
};
registry.addInterceptor(handlerInterceptor);
}
}
三、前端开发
3.1、登陆页面login.html
登陆成功后将token保存到localStorage中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="assets/bootstrap-3.3.7-dist/css/bootstrap.min.css" rel="stylesheet">
<script src="assets/jquery-3.5.1.min.js"></script>
<script src="assets/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
<script src="assets/vue.min-v2.5.16.js"></script>
<script src="assets/axios.min.js"></script>
</head>
<body>
<div class="container" id="app">
<div class="row">
<div class="col-md-4 col-md-push-4">
<h3 style="margin-top: 30px;">用户登录</h3>
<div style="margin-top: 30px;">
<label for="uname">用户名:</label>
<input id="uname" class="form-control" type="text" v-model="userName">
</div>
<div style="margin-top: 15px;">
<label for="upasswd">密码:</label>
<input id="upasswd" class="form-control" type="password" v-model="passwd">
</div>
<div style="margin-top: 15px;">
<button class="btn btn-primary" @click="doLogin">登录</button>
</div>
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
userName: null,
passwd: null
},
methods: {
doLogin(){
axios.post("/user/login", {
userName: this.userName,
passwd: this.passwd
}).then(res => {
console.log(res.data)
let token = res.data;
if(token == false){
alert("用户名或密码错误!");
} else {
localStorage.setItem("token", res.data)
window.location.href = "index.html"
}
});
}
}
});
</script>
</body>
</html>
3.2、新闻列表页index.html
在列表页请求后台接口,需要在header中携带token
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link href="assets/bootstrap-3.3.7-dist/css/bootstrap.min.css" rel="stylesheet">
<script src="assets/jquery-3.5.1.min.js"></script>
<script src="assets/bootstrap-3.3.7-dist/js/bootstrap.min.js"></script>
<script src="assets/vue.min-v2.5.16.js"></script>
<script src="assets/axios.min.js"></script>
</head>
<body>
<div class="container" id="app">
<table class="table table-striped">
<caption>文章列表</caption>
<thead>
<tr>
<th align="center">编号</th>
<th align="center">标题</th>
<th align="center">描述</th>
<th align="center">发布时间</th>
<th align="center">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="art in articleList">
<td>{{art.id}}</td>
<td>{{art.title}}</td>
<td>{{art.descn}}</td>
<td>{{art.createTime}}</td>
<td align="center">
<button class="btn btn-link" style="margin-right: 10px;">修改</button>
<button class="btn btn-link">删除</button>
</td>
</tr>
</tbody>
</table>
<ul class="pagination" v-for="p in pageCnt">
<li v-if="p == pageIndex" class="active"><a href="#" @click="doGo(p)">{{p}}</a></li>
<li v-else="p == pageIndex"><a href="#" @click="doGo(p)">{{p}}</a></li>
</ul>
</div>
<script>
new Vue({
el: '#app',
data: {
articleList: null,
//用于分页
pageIndex: 1, //页码
pageSize: 3, //每页显示的条数
pageTotal: 0, //总条数
pageCnt: 0 //总页数
},
methods: {
requestArticleList(){
axios.get("/list", {
params: {
pageIndex: this.pageIndex,
pageSize: this.pageSize
},
headers: {'x-jwt-token': localStorage.getItem("token")}
}
).then(res => {
console.log(res.data)
if(res.data.code == 403){
alert("请先登录");
window.location.href = "login.html"
} else {
this.articleList = res.data.records
this.pageCnt = res.data.pages
this.pageTotal = res.data.total
this.pageIndex = res.data.current
this.pageSize = res.data.size
}
})
},
doGo(p){
this.pageIndex = p
this.requestArticleList()
}
},
created: function () {
this.requestArticleList()
}
})
</script>
</body>
</html>
四、附录
4.1、完整pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>page-helper-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>page-helper-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- Spring&SpringMVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<!-- mysql-connector -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.32</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jwt坐标-->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.11.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>