shiro+jwt+redis - 无状态token登录(二)授权篇

3 篇文章 0 订阅

上文总览篇中,相信大家已经对接下来要做的事情有了总体思路及印象。总言之我们要做的就只有两件事,一是授权,二即是鉴权。
  让我们先从授权开始,何为授权?在这里简单地来讲就是要颁发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的清除主要是做两件事:
  1. 清除AccessToken
  2. 清除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;
    }

五、演示说明

  1. 登录成功,返回用户以及AccessToken信息
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值