场景:在前后端分离(SpringBoot + Vue)的项目中,部分网站需要在登陆之后才可以访问其他页面进行相关操作(例如:某某某后台管理系统),这时,在用户登录之后就需要后端给前端发送一个令牌,前端接收后保存在本地(Session Storage),往后前端发送的每个请求都需要携带该令牌,后端验证之后才可以响应数据给前端,这样就不会被没有登陆的人员恶意的访问后端,并篡改数据库的相关数据了。
上述场景说的令牌就是token了,在往常没有前后端分离的项目中,实现诸如此类的功能一般是使用 cookie 和 session 的,但在前后端分离的项目中,前端和后端是跨域的,这时候 cookie 和 session 就不起作用了,所以需要 token 的存在。
以下是对token的简单实现:
详细步骤:
一、创建管理员表admin
二、redis配置
1、导入依赖
<!-- 配置使用redis启动器,存储token -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、在 application.properties 中配置相关信息
# 配置redis
spring.redis.host=xxx.xxx.xx.xx # 安装了redis的服务器公网ip
spring.redis.port=6379 # redis使用的端口号,一般是6379
三、在SpringBoot中创建admin表对应的domain、controller、service、dao层
1、domain层代码
/**
* 管理员表对应的实体类
*/
public class Admin implements Serializable {
// 主键
private Integer id;
// 管理员昵称
private String adminName;
// 管理员登陆密码
private String password;
/**
* 相应的get()、set()方法,这里省略
*/
}
2、dao层及相应sql的xml代码
@Repository
public interface AdminMapper {
/**
* 对登陆的管理员进行验证
*/
Admin verifyAdmin(String adminName, String password);
}
xml代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="xxxx.xxxx.xxxx.mapper.AdminMapper">
<!-- 验证用户输入的账号密码是否正确 -->
<select id="verifyAdmin" resultType="Admin">
select * from admin where admin_name=#{adminName} and password=#{password}
</select>
</mapper>
3、service层接口及其实现类
/**
* 管理员的业务层接口
*/
public interface AdminService {
/**
* 验证管理员账号
* @param adminName
* @param password
* @return
*/
Admin verifyAdmin(String adminName, String password);
}
/**
* 管理员的业务层接口的实现类
*/
@Service
public class AdminServiceImpl implements AdminService {
@Autowired
private AdminMapper adminMapper;
/**
* 验证管理员账号
*
* @param adminName
* @param password
* @return
*/
@Override
public Admin verifyAdmin(String adminName, String password) {
return adminMapper.verifyAdmin(adminName, password);
}
}
4、controller层代码
/**
* 管理员控制类
*/
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private AdminService adminService;
/**
* 验证管理员的账号密码是否正确
* @param adminName
* @param password
* @return
*/
@PostMapping("/verify")
public Object verifyAdmin(String adminName, String password) {
JSONObject jsonObject = new JSONObject();
Admin admin = adminService.verifyAdmin(adminName, password);
if (admin != null) {
// 使用UUID作为token值
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
// 在uuid后拼接管理员id组成最后的token值(添加id是为了方便后续验证)
String token = uuid + admin.getId();
// 将用户的ID信息存入redis缓存,并设置两小时的过期时间
stringRedisTemplate.opsForValue().set("admin"+admin.getId(), token, 7200, TimeUnit.SECONDS);
jsonObject.put(Consts.CODE, 1);
jsonObject.put(Consts.MSG, "登陆成功");
jsonObject.put("token", token);
}else {
jsonObject.put(Consts.CODE, 0);
jsonObject.put(Consts.MSG, "账号或者密码错误");
}
// 向前端返回相应的json数据
return jsonObject;
}
}
这里多解释一下,这里生成的 token 值其实就是使用了 UUID 加上查询出来的管理员id,然后将其存储在 redis 中,并且其key值固定格式为 "admin"+admin.getId()
(如:admin1)。
这样可以让一个管理员对应 redis 里一条 key-value 键值对数据,这样在管理员多次登陆后数据在 redis 中都会被替代掉(因为添加时 key 值相等),避免在 redis 存储了之前 token 无效数据,并且在后续在拦截器中验证,获取到 token 值时,就可以得到管理员的 id,以此为依据在 redis 中查询其 token 值是否是伪造的。这时就可以启动后端接收前端的请求了。
四、前端
1、前端发送登录请求
前端发送登录请求成功后将后端返回的 token 值保存在浏览器的 Session Storage 中,代码如下:
// 拼接请求参数(管理员名称和密码)
let params = new URLSearchParams();
params.append("adminName", this.loginForm.adminName);
params.append("password", this.loginForm.password);
// 向后端发送验证请求
post(`admin/verify`, params).then(res => {
if(res.code == 1) {
// 登陆成功之后将后端返回的值保存在浏览器的 Session Storage 中
window.sessionStorage.setItem("token", res.token);
this.$router.push("/home");
this.$message.success("登陆成功")
}else {
this.$message.error("账号或者密码错误")
}
})
2、封装axios
import axios from 'axios'
import router from '../router';
axios.defaults.timeout = 15000; // 响应的超时时间
axios.defaults.withCredentials = true; // 支持跨域访问
// 为post请求设置请求头
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
axios.defaults.baseURL = "http://xxxxxxxxxxx:xxxx/"
// 发送请求之前先为请求头添加Authorization字段且值为token,以应对后端接口的验证
axios.interceptors.request.use(config => {
// 在浏览器的 Session Storage 中拿到 token 值
config.headers.Authorization = window.sessionStorage.getItem("token");
return config;
})
这样前端发送的每一个请求在其头部信息中都会携带我们自定义的 Authorization 属性,其值就是登陆时后端返回的 token 值。
五、拦截器
1、后端添加拦截器
后端添加拦截器以验证请求中携带的 token 值是否真实存在,代码如下:
/**
* 访问controller之前先进行token验证
*/
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 注意:放行浏览器的预检请求
if (request.getMethod().equals("OPTIONS")) {
return true;
}
// 获取请求头中携带的token值
String token = request.getHeader("Authorization");
// token验证
if(token!=null) {
// token值的最后一位数字是管理员的id,也是redis中存储token的key
String adminId = "admin" + token.substring(token.length() - 1);;
String value = stringRedisTemplate.opsForValue().get(adminId);
if (value.equals(token)) {
return true;
}
}
// 验证失败,拦截请求
return false;
}
}
拦截器根据token携带的管理员id值(key)在redis中查找相应的结果(value),再与请求携带的token值比较,一样则放行请求。
注意:上述代码 “放行浏览器的预检请求” 部分需要特别注意,缺少这段if判断代码,可能会引起前端的CORS错误,导致请求一直被拦截,无法通过,详情见本人的另一文章,点击前往
2、配置自定义拦截器
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private TokenInterceptor tokenInterceptor;
/**
* 配置token拦截器生效
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 配置自定义拦截器,使其生效,并且放行登陆请求
registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/admin/verify");
}
/**
* 解决因前后端的端口不一致导致的跨域问题
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET","POST","PUT","OPTIONS","DELETE","PATCH")
.allowCredentials(true)
.maxAge(3600);
}
}
到此基于 SpringBoot + Vue + redis 简单实现的 token 功能就小功告成了。
本文仅个人纪录学习所用,如有纰漏,欢迎指正。