Token验证
学妹最近写项目,卡在了token认证上面,正好之前写过类似的,就乘此契机整理一份。
整个模块主要完成token认证、授权以及使用redis进行缓存。
很多内容都在代码里面的注释中,本文主要讲细节实现,在食用之前,请务必要对token验证流程有一个大致的印象。
话不多说,下面开锤!
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring-cloud-demo</artifactId>
<groupId>org.example</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-demo-auth</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>spring-cloud-demo-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
首先定义一个Token实体类
package com.demo.commons.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author: yokna
* @apiNote: token基础类
* @date: 2021/1/14
* @time: 9:58
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Token implements Serializable {
//String存储token值
private String token;
//设置过期时间
private Long exp;
//刷新token
private String refreshToken;
//具体的用户实体类
private User user;
}
来看看user类里面存的都有哪些信息
package com.demo.commons.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @author: yokna
* @apiNote:
* @date: 2021/1/14
* @time: 10:40
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
// 表id 真实姓名 用户姓名 密码 角色
public class User implements Serializable {
private Integer id;
private String realname;
private String username;
private String password;
private String role;
}
UserMapper
有了user,必然会有userMapper嘛,我用的是MybatisPlus,它封装了一些基本的查询,简单的查询不需要写sql语句,只需要传一个HashMap值,或者构造一个查询对象,特别方便。
package com.demo.auth.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.demo.commons.domain.User;
import org.springframework.stereotype.Repository;
/**
* @author: yokna
* @apiNote:
* @date: 2021/1/14
* @time: 14:38
*/
@Repository
public interface UserMapper extends BaseMapper<User> {
}
接下来是tokenUtils
package com.demo.auth.util;
import com.demo.commons.domain.Commons;
import com.demo.commons.domain.Token;
import com.demo.commons.domain.User;
import com.demo.commons.utils.RedisUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.tomcat.util.security.MD5Encoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
/**
* @author: yokna
* @apiNote: 用于token的工具类
* @date: 2021/1/14
* @time: 9:55
*/
@Component
public class TokenUtils {
/**
* 存储时间
*/
private static final long DEAD_TIME = 1000 * 60 * 31;
//注入redisUtils 用redis来存储token
@Autowired
private RedisUtils redisUtils;
/**
* 创建token,返回一个token对象,token对象里有四个属性,分别是tokenStr、过期时间、刷新token、以及user对象
*所以我们在创建token时需要对这四个属性赋值
* @param user 传入的用户对象
* @return token
*/
public Token createToken(User user) {
//创建tokenStr
String tokenStr = createTokenStr(user);
//创建refreshToken,并且通过MD5加密加盐,MD5Encoder.encode()需要传一个byte[]二进制数组,此时我们将用户名+系统当前时间+token秘钥作为参数穿进去,然后截取0-16位,getbytes是以系统默认的编码格式获取String
String refreshToken = MD5Encoder.encode((user.getUsername() + "@" + DateUtils.nowTime() + "#" + Commons.REFRESH_TOKEN_SECRET).substring(0, 16).getBytes());
//上面我们得到了两个最重要的参数,tokenStr以及refreshToken,加上传参user及静态常量DEAD_TIME,就可以构建一个token
Token token = new Token(tokenStr, System.currentTimeMillis() + DEAD_TIME, refreshToken, user);
//然后用redisUtils将token存储起来,后两个参数是token在redis中的过期时间
redisUtils.storeValue(tokenStr, token, TimeUnit.MILLISECONDS, DEAD_TIME);
return token;
}
/**
* 刷新token
*
* @param accessToken token字符串
* @param refreshToken 刷新token 的字符串
* @return token
*/
public Token refreshToken(String accessToken, String refreshToken) {
//获取未过期的tokenStr,redis中存储的是K-V K是tokneStr V是token对象转换的json(我猜是json,具体是什么,不清楚)
Token token = (Token) redisUtils.getValue(accessToken);
//如果是空,就返回空,说明用户没有token,或者之前的token过期,在redis中被删掉
if (token == null) return token;
//判断refreshToken是否一致
if (!StringUtils.isEmpty(refreshToken) && refreshToken.equals(token.getRefreshToken())) {
//重新创建token字符串
String tokenStr = createTokenStr(token.getUser());
//刷新token字符串
token.setToken(tokenStr);
//刷新过期时间
token.setExp(System.currentTimeMillis() + DEAD_TIME);
//重新存储
redisUtils.storeValue(tokenStr, token, TimeUnit.MILLISECONDS, DEAD_TIME);
//删除之前的token
redisUtils.delByKey(accessToken);
}
return token;
}
/**
* 鉴权
*
* @return boolean
*/
public boolean accessToken(String accessToken, String role) {
//获取未过期的token
Token token = (Token) redisUtils.getValue(accessToken);
if (token != null && token.getUser().getRole().equals(role)) {
return true;
}
return false;
}
private String createTokenStr(User user) {
//通过Base64加密tokenStr,生成tokenStr
return new String(
Base64.getEncoder().encodeToString(
(user.getUsername() + "@" + DateUtils.nowTime() + " #" + Commons.SECRET + "$" + user.getRole()).getBytes())
);
}
}
redisUtils工具类主要用来初始化redis、存储数据、取数据。
package com.demo.commons.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author: yokna
* @apiNote: 用于redis 的工具类
* @date: 2021/1/14
* @time: 9:48
*/
@Component
public class RedisUtils {
//注入redis封装好的工具
@Autowired
private RedisTemplate redisTemplate;
/**
* 重置redis
*/
//基本是固定写法
public void initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
redisTemplate = new RedisTemplate <String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.afterPropertiesSet();
}
/**
* 删除
*
* @param key 关键字
*/
public void delByKey(String key) {
redisTemplate.delete(key);
}
// ---------------key val
/**
* 存储key val
*
* @param key 关键字
* @param target 目标数据
* @param timeUnit 时间类型
* @param time 时间
*/
public void storeValue(String key, Object target, TimeUnit timeUnit, long time) {
redisTemplate.opsForValue().setIfAbsent(key, target, time, timeUnit);
}
/**
* 获取value
*
* @param key 关键字
* @return Object
*/
public Object getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
}
下面就是tokenService
package com.demo.auth.service.impl;
import com.demo.auth.mapper.UserMapper;
import com.demo.auth.service.TokenService;
import com.demo.auth.util.TokenUtils;
import com.demo.commons.domain.Commons;
import com.demo.commons.domain.Token;
import com.demo.commons.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
/**
* @author: yokna
* @apiNote:
* @date: 2021/1/14
* @time: 14:56
*/
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private UserMapper userMapper;
@Autowired
private TokenUtils tokenUtils;
@Override
public Token authLogin(String username, String password) {
//登录,username与password
HashMap<String,Object> map = new HashMap<>();
//构造mybatisplus的查询map
map.put("username",username);
List<User> list = userMapper.selectByMap(map);
User user = list.get(0);
//判断相关重要数据,抛两个异常--用户不存在或者用户密码错误
if (user == null) {
throw new RuntimeException(Commons.USER_NOT_FOUND);
} else if (!user.getPassword().equals(password)) {
throw new RuntimeException(Commons.PASSWORD_ERROR);
}
//经过上面的判断过滤后,到这里的用户是合法用户了,为了安全,把密码置空
user.setPassword(null);
//获取token
return tokenUtils.createToken(user);
}
@Override
public String grantTypeCode(String buildName) {
return null;
}
@Override
public Token refreshToken(String accessToken, String refreshTokenStr) {
return tokenUtils.refreshToken(accessToken, refreshTokenStr);
}
}
最后是我们的Controller层
package com.demo.auth.controller;
import com.demo.auth.service.TokenService;
import com.demo.commons.domain.Result;
import com.demo.commons.domain.Token;
import com.demo.commons.utils.ResultUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: yokna
* @apiNote:
* @date: 2021/1/14
* @time: 15:07
*/
@RestController
@RequestMapping("/auth")
public class AuthTokenController {
@Autowired
private TokenService tokenService;
@RequestMapping(value = "/token/password", method = {RequestMethod.GET, RequestMethod.POST})
public Result passwordGrantType(String username, String password) {
try {
Token token = tokenService.authLogin(username, password);
return ResultUtils.success(token);
} catch (RuntimeException e) {
String errorMessage = e.getMessage();
return ResultUtils.fail(errorMessage, null);
}
}
@RequestMapping(value = "/token/refresh_token", method = {RequestMethod.GET, RequestMethod.POST})
public Result refreshToken(String accessToken, String refreshToken) {
Token token = null;
if ((token = tokenService.refreshToken(accessToken, refreshToken)) != null) {
return ResultUtils.success(token);
}
return ResultUtils.fail(token);
}
}