前言
本章内容建立在Springboot 基础框架搭建好的基础上进行整合jwt+redis 实现token登录认证
大致流程
- 前端 :前端拿到登录名和密码,使用JSEncrypt实现rsa将密码进行加密(这个部分网上有很多,不了解的自行去查询,有时间本人也会更新),然后传到后端;
- 后端 :拿到密码通过后台的私钥进行解密,然后通过用户名查询到用户信息(用户名唯一),通过用户信息的状态来判断登录结果。
- 前端 :若后端验证成功则根据规则生成Token,并存入redis,且向前端返回token,前端则将token和用户存到localstorage,客户端再次发送请求数据将携带token。
- 后端 :通过拦截器拦截请求,在请求头中拿到token,验证redis中是否有token,且是否过期,如果token不存在或者过期,则提示身份验证未通过,或者验证信息失效,否则则请求成功
注:在HTML5中,新加入了一个localStorage特性,这个特性主要是用来作为本地存储来使用的,解决了cookie存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小,这个在不同的浏览器中localStorage会有所不同
知识点
一、Token
Token产生的背景:客户端频繁的向服务器请求数据,服务器频繁的去数据库查询用户名和密码进行比对,判断用户名和密码正确与否,并作出相应的提示,这样的情况下服务器压力较大,而Token的目的就是为了减轻服务器的压力,减少频繁的查询数据库,使得服务器更加的健壮。
2.Token的定义:Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。(换一种说法,甚至可以理解为洗浴会所的手牌,第一次进入会所,前台通过一定操作,会给你一个手牌,这个手牌即标志着你的身份,你随后的消费都可以通过这个手牌进行)
二、JWT
1.JWT全称为json web token,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。说白了就是一大串字符串。本质上来说 JWT 也是 token。详情参考这篇文章:漫谈JWT 。值得一提的是JWT本身没有安全性可言,所以存储用户信息,尤其是敏感数据是一件很可怕的事情,建议不要存放这一类信息。详细生成JWT的代码我会贴到下面。
三、Redis
注:这里只对Redis 作简单介绍,说明Token与Redis的关系,以及为什么使用Redis存储Token
详细请参考:Redis【入门】就这一篇!
1.redis定义:redis是一个开源的、使用C语言编写的、支持网络交互的、可基于内存也可持久化的Key-Value数据库。
2.Redis 在 Java Web 主要有两个应用场景:
存储 缓存 用的数据;
需要高速读/写的场合使用它快速读/写;
3.Token 为什么要存储到Redis?
本人也查阅了很多资料,总结下来无非三点:
1.Token 具有时效性,简单来说就是有效期,超过了这个期限Token 就会失效,需要用户重新登录。但是怎么在项目中实现这个时效性,这个时候就用到Redis,Redis天然支持支持设置过期时间以及通过一些第三方提供包的API到达自动续时效果。
2.Redis采用NoSQL技术,可以支持每秒十几万此的读/写操作,其性能远超数据库,在请求数量较多的时候,Redis也可以从容应对
3.用户登录信息一般不需要长效储存,放在内存中,可以减少数据库的压力
实现部分
引入依赖
<!--Jedis是Redis官方推荐的Java连接开发工具。-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- shiro 借用加密密码 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<!-- Jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.4.RELEASE</version>
</dependency>
详细代码
1.用户注册
@Override
@Transactional
public ReturnBody insertAdmin(SysAdmin admin) throws Exception {
//SysAdmin 为管理员类
admin.setAppName(Constants.PROJECT_NAME);//插入项目名
String password = admin.getUserPassword();
if(StringUtil.isEmpty(admin.getUserPassword())) {
password ="123456";//默认密码为123456
}
PasswordHash passwordHash = PasswordUtil.encrypt(password);//采用shrio加密方式(密码加盐)PasswordUtil继承DigestUtil(Apache DigestUtils线程安全的类)
admin.setUserPassword(passwordHash.getHexEncoded());//HexEncoded为加密过后的密文
SysUser user = new SysUser(admin);
ReturnBody result = userService.insertOrUpdateAuth(user);//判断用户邮箱手机号是否已经注册,用户名是否已经存在,这部分逻辑自己编写
ResultUtil.throwException(result);//如果请求失败,则抛出异常,
userMapper.insert(user);//插入用户表
admin.setUserId(user.getUserId());//mybatis-plus 自动生成主键,获得主键
this.save(admin);//插入管理员表
userSaltMapper.insert(new SysUserSalt(admin.getUserId(), passwordHash.getSalt()));//插入盐值与用户关联表(每个用户都有一个特定的盐值增强安全性)
Map<String, Object> map = new HashMap<String, Object>();
map.put("user_id", user.getUserId());
sysUserRoleMapper.deleteByMap(map);
List<String> roleIds = admin.getRoleId();
if (StringUtil.isNotEmpty(roleIds)){
int a = userRoleMapper.insertdeRoles(admin.getUserId(),roleIds);
}//插入用户角色关联信息
/*
* 根据admin的userId关联创建的附件列表
*/
ReturnBody body = new ReturnBody(admin);
((SysAdmin) body.getData()).setUserPassword(password);
return body;
}
PassHash类及PasswordUtil 加密方法
PasswordUtil 加密方法
/**
* 盐值加密
* @param source
* @return
*/
public static PasswordHash encrypt(Object source) {
return new PasswordHash(source);
}
/**
* 密码加密配置
*/
public class PasswordHash {
/**
* 加密方式
*/
private String algorithmName;
/**
* 加密次数
*/
private int hashIterations;
/**
* 原密码
*/
private Object source;
/**
* 盐值
*/
private String salt;
/**
* 加密后的密文
*/
private String hexEncoded;
/**
*
* @param source
*/
public PasswordHash(Object source) {
this.algorithmName = "md5";//加密方式
this.hashIterations = 996;//加密次数
this.salt = UUIDUtil.getUUID();//盐值为随机数
this.source = source;//密码
this.hexEncoded = new SimpleHash(algorithmName, source, salt, hashIterations).toHex();//加密后的密文
}
@Override
public String toString() {
return "PasswordHash [algorithmName=" + algorithmName + ", hashIterations=" + hashIterations + ", source="
+ source + ", salt=" + salt + ", hexEncoded=" + hexEncoded + "]";
}
}
2.用户登录
/**
* 方法作用: 获取令牌(统一用户登录入口)
* @param user
* @return
* @throws Exception
*/
@ApiOperation(value = "获取令牌(统一用户登录入口)" , notes="获取令牌(统一用户登录入口)")
@PostMapping(value="/login")
public String accessToken(@RequestBody Token token) throws Exception {
String password = null;
if (StringUtil.isNotEmpty(token.getPassword())){
password = RSACoder.decrypt(token.getPassword());//解码 BASE64解码
token.setPassword(password);
}
token = authService.selectByUserName(token);//通过用户名查到用户信息方便与传过来的token进行比对
if (null==token) {
ValueUtil.isError(ReturnCode.USERNAME_WETHER);
//用户名不存在,抛出异常自行封装
}else if(!PasswordUtil.checkPassword(password,token.getSalt(),token.getUserPassword())) {
//password为用户输入的密码字符串,salt为用户插入之初设置的盐值,token.getUserPassword为数据库中通过密码加盐的方式形成的密文
//帐号或密码错误,抛出异常自行封装
ValueUtil.isError(ReturnCode.LOGIN_FAILED);
}else if(StringUtil.isEmpty(token.getHomeUrl())) {
//判断用户是否设置主页,方便用户登录成功后跳转登录界面
ValueUtil.isError(ReturnCode.HOMEPAGE_NULL);
} else {
//设置用户权限(权限放面会陆续更新)
token.setResSet(authService.selectResourceByUserId(token.getUserId()));
//用户类型
token.setSelectTypes(authService.selectTypes(token.getUserId()));
token.setAppKey(appKey); //用户所属项目
token.setIp(getUserIp());//用户登录id地址
String tokenCode = tokenManager.putToken(token);//创建token
setResponseTokenCode(tokenCode);
token = tokenManager.queryToken(tokenCode);//查看token
token.setToken(tokenCode);
}
return ValueUtil.toJson(token);//返回token
}
##这里封装了一个Token管理器便于方便管理token
Token管理器
public class TokenManager {
/**
*
*/
@Autowired
private RedisClientTokenUtil redisClient;//Redis操作Token封装类
/**
* ios端设备标识前缀
*/
private static final String APPKEY_PREFIX_IOS = "IOS";
/**
* android端设备标识前缀
*/
private static final String APPKEY_PREFIX_ANDROID = "ANDROID";
/**
* moblie端设备标识前缀
*/
private static final String APPKEY_PREFIX_MOBILE = "MOBILE";
/**
* web端设备标识前缀
*/
private static final String APPKEY_PREFIX_WEB = "WEB";
/**
* ios终端token有效期
*/
private static final int EXPIRE_SECOND_IOS = 14 * 24 * 60 * 60;
/**
* android终端token有效期
*/
private static final int EXPIRE_SECOND_ANDROID = 14 * 24 * 60 * 60;
/**
* MOBILE终端token有效期
*/
private static final int EXPIRE_SECOND_MOBILE = 14 * 24 * 60 * 60;
/**
* web终端token有效期
*/
private static final int EXPIRE_SECOND_WEB = 1 * 30 * 60;
/**
* 管理员终端token有效期
*/
private static final int ADMINISTRATOR_WEB = 3 * 60 * 60;
/**
* 其他终端token有效期
*/
private static final int EXPIRE_SECOND_OTHER = EXPIRE_SECOND_WEB;
/**
* 获取token
*
* @param token
* @return
* @throws Exception
*/
public String putToken(Token token) throws Exception {
String tokenCode = null;
if (token != null) {
token.setExpireSecond(createExpireSecond(token));//延长token 有效期
tokenCode = createTokenCode(token);//创建token
String key = getKey(tokenCode);//项目名+tokenCode
redisClient.set(key, token, token.getExpireSecond());
}
return tokenCode;
}
/**
* 校验token
*
* @param tokenCode
* @return
* @throws Exception
*/
public boolean checkToken(String tokenCode) throws Exception {
if (!StringUtils.isEmpty(tokenCode)) {
String key = getKey(tokenCode);
if (redisClient.exists(key)) {
Token token = redisClient.get(key);
// 重置有效期
redisClient.setExpire(key, token.getExpireSecond());
return true;
}
}
return false;
}
/**
* 查看token
*
* @param tokenCode
* @return Token
* @throws Exception
*/
public Token queryToken(String tokenCode) throws Exception {
String key = getKey(tokenCode);
Token token = redisClient.get(key);
if (token != null) {
redisClient.setExpire(key, token.getExpireSecond());
}
if (StringUtil.isEmpty(token)){
ValueUtil.isError(ReturnCode.UNAUTHORIZED);
}
return token;
}
/**
* 初始化token有效期
*
* @param token
* @return
*/
private int createExpireSecond(Token token) {
int expireSecond = EXPIRE_SECOND_OTHER;
if (token != null) {
String appKey = token.getAppKey();
if (!StringUtils.isEmpty(appKey)) {
String prefix = appKey;
switch (prefix) {
case APPKEY_PREFIX_ANDROID:
expireSecond = EXPIRE_SECOND_ANDROID;
break;
case APPKEY_PREFIX_IOS:
expireSecond = EXPIRE_SECOND_IOS;
break;
case APPKEY_PREFIX_MOBILE:
expireSecond = EXPIRE_SECOND_MOBILE;
break;
case APPKEY_PREFIX_WEB:
expireSecond = EXPIRE_SECOND_WEB;
}
} else {
if (token.getUserType() == 1) {
expireSecond = ADMINISTRATOR_WEB;
}
}
}
return expireSecond;
}
/**
* 根据tokenCode获取key
*
* @param tokenCode
* @return
*/
private String getKey(String tokenCode) {
return Constants.TOKEN_PREFIX + tokenCode;
}
/**
* 生成tokenCode
*
* @param token
* @return
*/
private String createTokenCode(Token token) {
String tokenCode = null;
if (StringUtil.isNotEmptyObjects(token.getUserId(),token.getUserName())) {
tokenCode=UserUtils.createToken(token.getUserId(),token.getUserName(),token.getExpireSecond());
}
return tokenCode;
}
//createToken 方法直接写在这里
public static String createToken(String userId, String userName, int expireSecond) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成签名密钥
byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(RSAUtil.privateKey);//RSAUtil.privateKey为私钥
Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder()
.claim("userId", userId) // 设置载荷信息
.claim("userName",userName)
.signWith(signatureAlgorithm, signingKey);
//生成JWT
return builder.compact();
}
}
密码校验
/**
* 密码校验
* @param source
* @param salt
* @param password
* @return
*/
public static boolean checkPassword(Object source, String salt, String password) {
//判断参数非空 ,自行封装 此处省略
return password.equals(encryptString(source, salt));//再次加密 比对加密结果
}
/**
* 盐值加密
* @param source
* @return
*/
public static String encryptString(Object source, String salt) {
return new PasswordHash(source, salt).getHexEncoded();
}
注:本文章为本人学习记录总结