上文总览篇中,相信大家已经对接下来要做的事情有了总体思路及印象。总言之我们要做的就只有两件事,一是授权,二即是鉴权。
让我们先从授权开始,何为授权?在这里简单地来讲就是要颁发token。何时颁发?毫无疑问,无非就是在登录/注册成功之后。
至于上文中提到的根据RefreshToken自动刷新AccessToken,我将之归置为token刷新,代码实现于后续篇章说明。
Here we go.
一、Maven配置
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.3</version>
</dependency>
二、Application配置
spring:
aop:
# proxy-target-class属性值决定是基于接口的还是基于类的代理被创建。属性值被设置为true,那么基于类的代理将起作用(这时需要cglib库)。
# 如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用。
proxy-target-class: true
auto: true
# 配置数据库连接池
datasource:
primary:
jdbc-url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=xxx
username: xx
password: xxxxx
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
db2:
jdbc-url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=xxx
username: xx
password: xxxxx
driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
## Redis配置 - start
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间(毫秒)
timeout: 5000
## Redis配置 - end
## rabbitmq配置 - start
rabbitmq:
host: 127.0.0.1
port: 5672
username: xxx
password: xxx
virtual-host: dev
## rabbitmq配置 - end
#返回JSON的全局时间格式 -start
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#返回JSON的全局时间格式 -start
## servlet文件上传 -start
servlet:
multipart:
#最大上传单个文件大小:默认1M
max-file-size: 1024MB
# 最大置总上传的数据大小 :默认10M
max-request-size: 1024MB
## servlet文件上传 -end
## 阿里短信配置 - start
aliyun:
sms:
accessKeyId: xxxx
accessKeySecret: xxxx
##用户注册验证码
register_template_code: xxx
##短信测试验证码
sms_test_template_code: xxx
##登陆确认验证码
login_template_code: xxx
##登陆异常验证码
logerr_template_code: xxx
##修改密码验证码
up_template_code: xxx
##身份验证验证码
auth_template_code: xxx
##活动确认验证码
activ_confirm_template_code: xxx
##信息变更验证码
info_change_template_code: xxx
sign_name: xxxx
## 阿里短信配置 - end
##oss 阿里云图片上传 - start
oss:
key: xxxx
secret: xxxx
endpoint: xxxx
bucket-name: xxxx
userInfoPic-path: xxxx
##oss 阿里云图片上传 - end
## 阿里视频点播配置 - start
vod:
accessKeyId: xxxx
accessKeySecret: xxxx
## 阿里视频点播配置 - end
## 阿里视频直播播配置 - start
live:
key: xxxx
oss: xxxx
## 阿里视频直播配置 - end
## 阿里支付配置 - start 沙箱配置
#pay:
#appId: xxxx
#rsaPrivateKey: xxxx
#alipayPublicKey: xxxx
#notifyUrl: xxxx
#returnUrl: xxxx
#signType: xxxx
#charset: xxxx
#gatewayurl: xxxx
## 阿里支付配置 - end
## 腾讯Im配置 - start
tencentyun:
sdk_appid: xxxx
secretkey: xxxx
## 腾讯Im配置 - end
#短信万能码开启状态 0 开启 1 关闭#
almightyCode:
status: 0
## 其它配置 - start
config:
# JWT认证加密私钥(Base64加密)
encrypt-jwtKey: xxxx
# PC端AccessToken过期时间(秒)
accessToken-expireTime: 604800
# PC端RefreshToken过期时间(604800秒/7天)
refreshToken-expireTime: 604800
## 其它配置 - end
三、颁发Token
- token的颁发并未有什么难度,主要是生成AccessToken放置于Header给前端。再生成RefreshToken保存于服务端即可。此处使用redis保存。
1. Controller:注册登陆控制器
/**
* 用户登录
* @param loginForm 表单数据
* @return 成功或者失败
*/
@ApiOperation("用户登录")
@PostMapping("/login")
public ResultVo actionLogin(@ApiParam(value = "用户登录表单数据")@Validated @RequestBody LoginForm loginForm,
HttpServletResponse response, HttpServletRequest request) {
//当前客户端ip地址
loginForm.setClientIP(getClientIP());
return ResultVoUtil.success(userService.actionLogin(loginForm, response, request));
}
2.service:注册登陆业务实现类
/**
* 用户登录
* @param loginForm 表单参数
* @return 成功或者失败
*/
@Override
public LoginResponseVo actionLogin(LoginForm loginForm, HttpServletResponse response, HttpServletRequest request) {
log.info("YUserServiceImpl.actionLogin params ==>> {}",loginForm);
// 从请求头获取token
String acctoken = request.getHeader(CommonConstant.TOKEN);
// 登录类型(0:密码登陆;1:验证码登陆).
String loginType = loginForm.getLoginType();
// 手机号码
String account = StringUtils.trim(loginForm.getMobile());
// 验证码
String verifyCode = StringUtils.trim(loginForm.getVerifyCode());
// 密码
String password = StringUtils.trim(loginForm.getPassword());
// 用户动作
String action = loginForm.getAction();
// 设备类型
String userAgent = loginForm.getUserAgent();
// jwt储存信息
JwtData jwtData = new JwtData();
String encode = "";
if (!StringUtils.isEmpty(account)) {
encode = DesUtil.desEncodeCBC(CommonConstant.KEY,account);
}
YUser user = baseMapper.selectUserInfoByUserCodeOrMobile(encode);
log.info("YUserServiceImpl.actionLogin param user is -> {}", user);
if (user == null) {
throw new FailureException("帐户不存在");
}
// 获取数据库未加盐密码
String dbPassword = user.getPassword();
if (!StringUtils.isEmpty(dbPassword)) {
user.setSalt(ShiroKit.getRandomSalt(5));
user.setSaltPassword(ShiroKit.md5(password,user.getSalt()));
user.setUpdatedIp(loginForm.getClientIP());
user.setUpdatedAt(new Date());
baseMapper.updatePasswordByUid(user.getSaltPassword(),user.getSalt(),user.getUpdatedIp()
,user.getUpdatedAt(),user.getUid());
}
// 用户状态(状态 1:正常 2:停用 3:注销)
Integer isActive = user.getIsActive();
boolean key = redisClient.hasKey(RedisKeyPrefix.SOURCE_LOGIN_MESSCODE + ":" + account + ":" + verifyCode);
// 密码登陆
if (loginType.equals(ZERO)) {
// 获取数据库盐
String salt = user.getSalt();
// 获取数据库加盐密码
String mdPassWord = user.getSaltPassword();
boolean res = Md5Util.isEqual(password, salt, mdPassWord);
if (StringUtils.isEmpty(account) || StringUtils.isEmpty(password)) {
throw new FailureException("用户名或密码不能为空");
} else if (!res) {
throw new FailureException("用户名和密码错误");
} else if (isActive != ONE) {
throw new FailureException("用户被删除或注销");
}
}else {
if (StringUtils.isEmpty(account) || StringUtils.isEmpty(verifyCode)) {
throw new FailureException("用户名或验证码不能为空");
} else if (!key) {
throw new FailureException("验证码错误,请重新输入");
} else if (isActive != ONE) {
throw new FailureException("用户被删除或注销");
}
}
// 设置RefreshToken,时间戳为当前时间戳,直接设置即可(不用先删后设,会覆盖已有的RefreshToken)
String currentTimeMillis = String.valueOf(System.currentTimeMillis());
String token = "";
jwtData.setAccount(account);
jwtData.setUserCode(account);
jwtData.setUid(user.getUid());
// 登录类型(0-密码登陆 1-验证码登录)
//======================== 手机号码登录=======================//
if (!StringUtils.isEmpty(acctoken)) {
// 清除RefreshToken
redisClient.hdel(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,user.getUid());
log.info("-> {}", acctoken);
// 清除access_token
redisClient.hdel(RedisConstant.PREFIX_SHIRO_ACCESS_TOKEN + account,acctoken);
}
// 通过账号查询token
Map<Object, Object> hmget = redisClient.hmget(RedisConstant.PREFIX_SHIRO_ACCESS_TOKEN + account);
Set<Map.Entry<Object, Object>> entries = hmget.entrySet();
for (Map.Entry<Object, Object> entry : entries) {
acctoken = (String) entry.getKey();
}
// 清除RefreshToken
redisClient.hdel(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,user.getUid());
// 清除access_token
redisClient.hdel(RedisConstant.PREFIX_SHIRO_ACCESS_TOKEN + account,acctoken);
// 生成access_token
token = JwtUtil.sign(jwtData, currentTimeMillis);
// 缓存PREFIX_SHIRO_REFRESH_TOKEN
redisClient.hset(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,user.getUid(), currentTimeMillis,
Integer.parseInt(refreshTokenExpireTime));
// 缓存PREFIX_SHIRO_ACCESS_TOKEN
redisClient.hset(RedisConstant.PREFIX_SHIRO_ACCESS_TOKEN + account,token, JSON.toJSONString(user),
Integer.parseInt(accessTokenExpireTime));
// 从Header中Authorization返回AccessToken,时间戳为当前时间戳
response.setHeader("Authorization", token);
response.setHeader("Access-Control-Expose-Headers", "Authorization");
user.setMobphone(DesUtil.desDecodeCBC(CommonConstant.KEY,user.getMobphone()));
user.setRealname(DesUtil.desDecodeCBC(CommonConstant.KEY,user.getRealname()));
UserVo userVo = new UserVo(user);
// 插入日志
YUserLoginLog userLoginLog = new YUserLoginLog();
userLoginLog.setUid(user.getUid());
userLoginLog.setLoginBill(token);
userLoginLog.setAction(action);
userLoginLog.setLoginPlatform(userAgent);
userLoginLog.setLoginIp(loginForm.getClientIP());
userLoginLog.setLoginTime(new Date());
int status = userLoginLogMapper.insert(userLoginLog);
HashMap<String, Object> hashMap = new HashMap<>();
hashMap.put("user",user);
hashMap.put("token",token);
hashMap.put("userAgent",userAgent);
hashMap.put("loginIp",loginForm.getClientIP());
hashMap.put("loginTime",new Date());
String toJSONString = JSON.toJSONString(hashMap);
//发送rabbitmq消息
sendMessage.sendMsg(UserMQ.USER_PC_LAST_LOGIN_INFO,toJSONString);
return LoginResponseVo.builder().userVo(userVo).token(token).build();
}
四、清除Token
- 没有买卖就没有伤害,有登录就会有退出。token的清除主要是做两件事:
- 清除AccessToken
- 清除RefreshToken
1. BaseController:基础控制器
/**
* 从请求头获取token
*
* @param request
* @return
*/
public String getToken(HttpServletRequest request) {
return request.getHeader(CommonConstant.TOKEN);
}
1.Controller:注册登陆控制器
/**
* 退出登录
* @param logoutForm 表单数据
* @return 成功或者失败
*/
@RequiresAuthentication
@ApiOperation("退出登录")
@PostMapping("/logout")
public ResultVo actionLogout(@ApiParam(value = "退出登录表单数据")@Validated @RequestBody LogoutForm logoutForm, HttpServletRequest request){
//当前客户端ip地址
logoutForm.setClientIP(getClientIP());
//从请求头获取token
String accessToken = getToken(request);
return ResultVoUtil.success(userService.logout(logoutForm,accessToken));
}
2.service:注册登陆业务实现类
/**
* 退出登录
* @param token 访问令牌
* @return 成功或者失败
*/
@Override
public boolean logout(LogoutForm logoutForm, String token) {
log.info("YUserServiceImpl。logout() param is -> {}", token);
// 获取当前Token的帐号信息
String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
String uid = JwtUtil.getClaim(token, JwtConstant.UID);
if (StringUtils.isBlank(account)) {
throw new FailureException("token失效或不正确");
}
//插入日志
YUserLoginLog userLoginLog = new YUserLoginLog();
userLoginLog.setUid(uid);
userLoginLog.setLoginBill(token);
userLoginLog.setAction(logoutForm.getAction());
userLoginLog.setLoginPlatform(logoutForm.getUserAgent());
userLoginLog.setLoginIp(logoutForm.getClientIP());
userLoginLog.setLoginTime(new Date());
int status = userLoginLogMapper.insert(userLoginLog);
if (status == 1) {
// 清除RefreshToken
redisClient.hdel(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account,uid);
//清除access_token
redisClient.hdel(RedisConstant.PREFIX_SHIRO_ACCESS_TOKEN + account,token);
return true;
}
return false;
}
五、演示说明
- 登录成功,返回用户以及AccessToken信息