shiro + jwt

1,前言

最近在做项目,做到权限模块,感觉登录功能也没这么简单。
查阅了一些文档加博客,大致明白了如何写这个模块。所以记录一下。

2,正文

一,POM

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

二,启动类

@SpringBootApplication
public class ShiroTest {
    
    
    public static void main(String[] args) {
    
    
        SpringApplication.run(ShiroTest.class);
    }
}

三,JwtUtil

jwt工具类,方便对jwt加密,解密,获取数据

package com.cql.shiro.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.io.UnsupportedEncodingException;
import java.util.Date;

public class JwtUtil {
    
    
    // 过期时间 24 小时
    private static final long EXPIRE_TIME = 60 * 24 * 60 * 1000;
    // 密钥
    private static final String SECRET = "shiro";

    /**
     * @param username 用户名
     * @return 加密的token
     */
    public static String createToken(String username) {
    
    
        try {
    
    
//            过期时间为一天
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);

            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            // 附带username信息
            return JWT.create()
                    .withClaim("userId", username)
                    //到期时间
                    .withExpiresAt(date)
                    //创建一个新的JWT,并使用给定的算法进行标记
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
    
    
            return null;
        }
    }

    /**
     * 校验 token 是否正确
     *
     * @param token    密钥
     * @param username 用户名
     * @return 是否正确
     */
    public static boolean verify(String token, String username) {
    
    
        try {
    
    
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            //在token中附带了username信息
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("userId", username)
                    .build();
            //验证 token
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
    
    
            return false;
        }
    }

    /**
     * 获得token中的信息,无需secret解密也能获得
     * @return token中包含的用户名
     */
    public static String getClaim(String claim,String token) {
    
    
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim(claim).asString();
        } catch (JWTDecodeException e) {
    
    
            return null;
        }
    }
}

四,JwtToken

package com.cql.shiro.pojo;

import org.apache.shiro.authc.AuthenticationToken;


public class JWTToken implements AuthenticationToken {
    
    
    private String token;

    public JWTToken(String token) {
    
    
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
    
    
        return token;
    }

    @Override
    public Object getCredentials() {
    
    
        return token;
    }
}

五,自定义Realm

自定义Realm

package com.cql.shiro.realm;

import com.cql.shiro.pojo.JWTToken;
import com.cql.shiro.pojo.User;
import com.cql.shiro.service.UserService;
import com.cql.shiro.util.JwtUtil;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.HashSet;
import java.util.Set;



@Component
public class CustomRealm extends AuthorizingRealm {
    
    

    @Autowired
    UserService userService;

    /**
     * 必须重写此方法,不然会报错,因为shiro不支持我们自定义的token
     */
    @Override
    public boolean supports(AuthenticationToken token) {
    
    
        return token instanceof JWTToken;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        System.out.println("————身份认证方法————");
        String token = (String) authenticationToken.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String userId = JwtUtil.getClaim("userId", token);
        if (userId == null || !JwtUtil.verify(token, userId)) {
    
    
            throw new AuthenticationException("token认证失败!");
        }
        User user = userService.getUserByUserId(userId);
        if (user == null) {
    
    
            throw new AuthenticationException("该用户不存在!");
        }

        return new SimpleAuthenticationInfo(token, token, "MyRealm");
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
    
        System.out.println("————权限认证————");
        String userId = JwtUtil.getClaim("userId", principals.toString());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //获得该用户角色
        String role = userService.getUserRole(userId);
        //每个角色拥有默认的权限
        String permission = userService.getUserPermission(userId);

        //设置该用户拥有的角色和权限

        info.addRole(role);
        info.addStringPermission(permission);
        return info;
    }
}

六,UserService

因为重点不是对数据库的增删改查,所以这部分直接写死了

package com.cql.shiro.service;

import com.cql.shiro.pojo.User;
import org.springframework.stereotype.Service;


@Service
public class UserService {
    
    

    //     验证用户名
    public User getUserByUserId(String userId) {
    
    
        if ("123".equals(userId)) {
    
    
            return null;
        } else {
    
    
            User user = new User();
            user.setInfo("测试信息");
            user.setUserId(userId);
            return user;
        }
    }

    //    获取角色
    public String getUserRole(String userId) {
    
    
        if ("123".equals(userId)) {
    
    
            return "teacher";
        } else {
    
    
            return "student";
        }
    }

    //    获取权限
    public String getUserPermission(String userId) {
    
    
        if ("123".equals(userId)) {
    
    
            return "vip1";
        } else {
    
    
            return "vip10";
        }
    }
}

七,JwfFilter

过滤,验证。请求头里面的token。(记住是请求头携带token,而不是请求参数,就是因为)

package com.cql.shiro.filter;

import com.cql.shiro.pojo.JWTToken;
import com.cql.shiro.pojo.R;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;


@Service
public class JwfFilter extends BasicHttpAuthenticationFilter {
    
    

    //判断是否有token,如果有,就对token进行验证,否则直接通过(游客或匿名用户不需要登录,所以不需要token)。
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)) {
    
    
            //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
            try {
    
    
                executeLogin(request, response);
                return true;
            } catch (Exception e) {
    
    
                //token 错误
                e.printStackTrace();
                response401(request,response,e.getMessage());
            }
        }
        //如果请求头不存在 Token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    /**
     * 判断用户是否想要登入。
     * 检测 header 里面是否包含 Token 字段
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("Token");
        return token != null;
    }

    /**
     * 执行登陆操作
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    
    
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Token");
        JWTToken jwtToken = new JWTToken(token);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    //    写回错误信息。因为是前后端分离,所以不是跳转页面,而是返回错误信息给前端
    private void response401(ServletRequest req, ServletResponse resp, String msg) {
    
    
        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
        httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        PrintWriter out = null;
        try {
    
    
            out = httpServletResponse.getWriter();
            String data = new ObjectMapper().writeValueAsString(R.error(msg));
            out.append(data);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        } finally {
    
    
            if (out != null) {
    
    
                out.close();
            }
        }
    }
}

八,ShiroConfig

package com.cql.shiro.config;

import com.cql.shiro.filter.JwfFilter;
import com.cql.shiro.realm.CustomRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;


@Configuration
public class ShiroConfig {
    
    

    /**
     * 先走 filter ,然后 filter 如果检测到请求头存在 token,则用 token 去 login,走 Realm 去验证
     */
    @Bean
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new LinkedHashMap<>();
        //设置我们自定义的JWT过滤器
        filterMap.put("jwt", new JwfFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        // 设置无权限时跳转的 url;
        //factoryBean.setUnauthorizedUrl("/unauthorized/无权限");
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 访问 /unauthorized/** 不通过JWTFilter
        //filterRuleMap.put("/unauthorized/**", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public DefaultWebSecurityManager securityManager(CustomRealm customRealm) {
    
    
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置自定义 realm.
        securityManager.setRealm(customRealm);
//        关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 添加注解支持
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    
    
        return new LifecycleBeanPostProcessor();
    }

}

九,controller

@RestController
public class LoginController {
    
    

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public R login(String userId){
    
    
        User user = userService.getUserByUserId(userId);
        if(user == null){
    
    
            return R.error("用户不存在");
        }else{
    
    
            return R.ok().data("token", JwtUtil.createToken(userId));
        }
    }
    
    @RequiresPermissions("vip10")
    @GetMapping("/testPermission")
    public R testPermission(){
    
    
        return R.ok().data("message","收到消息了");
    }
    
    @RequiresRoles("teacher")
    @GetMapping("/testRole")
    public R testRole(){
    
    
        return R.ok().data("message","收到消息了");
    }
}

十,异常捕获

@RestController
public class LoginController {
    
    

    @Autowired
    private UserService userService;

    //    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTI2NTY0NDZ9.tGfpqCVB8faAYSsmhVX5ZoZcAe0UaJ4aB9FI0KHSQV0
    @PostMapping("/login")
    public R login(String userId) {
    
    
        User user = userService.getUserByUserId(userId);
        if (user == null) {
    
    
            return R.error("用户不存在");
        } else {
    
    
            return R.ok().data("token", JwtUtil.createToken(userId));
        }
    }


    @RequiresPermissions("vip10")
    @PostMapping("/testPermission")
    public R testPermission() {
    
    
        return R.ok().data("message", "testPermission");
    }

    @RequiresRoles(logical = Logical.OR, value = {
    
    "teacher", "student"})
    @RequestMapping(value = "/testRole", method = RequestMethod.GET)
    public R testRole() {
    
    
        return R.ok().data("message", "testRole");
    }
}

十一,测试

在这里插入图片描述
在这里插入图片描述

获取token重新访问上面接口

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

三,关于token过期处理(思路)

1,前端

大家都知道在前后端是以token的形式交互,既然是token,那么肯定有它的过期时间,没有一个token是永久的,永久的token就相当于一串永久的密码,是不安全的,那么既然有刷新时间,问题就来了

  • 前后端交互的过程中token如何存储?
  • token过期时,前端该怎么处理
  • 当用户正在操作时,遇到token过期该怎么办?直接跳回登陆页面?

token如何存储

cookie的大小约4k,兼容性在ie6及以上 都兼容,在浏览器和服务器间来回传递,因此它得在服务器的环境下运行,而且可以设定过期时间,默认的过期时间是session会话结束。
localStorage的大小约5M,兼容性在ie7及以上都兼容,有浏览器就可以,不需要在服务器的环境下运行, 会一直存在,除非手动清除 。

对于这个问题,答案大致分为2种

  1. 存在 cookie
  2. 存在 localStorage

token过期时,前端该怎么处理

思路:token过期处理方式大概就是:

  1. 第一种:跳回登陆页面重新登陆
  2. 第二种:catch 401 ,然后重新获取 token

对于第一种,很简单在vue中我们可以在 axios 拦截器中这样写:

instance.interceptors.response.use(
  function (response) {
    
    
    // 对响应数据做点什么
    return response.data
  },
  function (error) {
    
    
    console.log(error)

    if (error.response) {
    
    
      if (error.response.status === 401) {
    
    
      	//401 token过期
            Message.error('登陆过期请重新登陆!')
            setToken('')
            //跳转会登录页面
            router.push({
    
    
              name: 'login'
            })
        }
      }
    }

    // 对响应错误做点什么
    return Promise.reject(error.response)
  }
)

对于第二种,如何重新获取 token,这就要涉及到后端的知识了

现代认证和/或授权解决方案已将令牌的概念引入其协议中。令牌是经过特殊处理的数据,它们可以提供足够的信息来授权用户执行操作,或者允许客户端获取有关授权过程的其他信息(然后完成它)。换句话说,令牌是允许执行授权过程的信息。客户端(或授权服务器以外的任何一方)是否可读取或解析此信息是由实现定义的。重要的是:客户端获取此信息,然后使用它来访问资源。JSON Web令牌(JWT)规范 定义了一种可以由实现表示公共令牌信息的方式。

JWT定义了一种方式,其中可以表示与认证/授权过程有关的某些公共信息。顾名思义,数据格式是JSON。JWT具有某些常见字段,例如主题,发行者,到期时间等。当与其他规范(如JSON Web签名(JWS)和JSON Web加密(JWE))结合使用时,JWT变得非常有用。这些规范不仅提供了授权令牌通常所需的所有信息,而且还提供了验证令牌内容的方法,使其不会被篡改(JWS)和加密信息以使其保持不透明的方式给客户(JWE)。数据格式的简单性(及其它优点)帮助JWT成为最常见的令牌类型之一。

我们将关注两种最常见的令牌类型:访问令牌和刷新令牌

  • 访问令牌携带必要的信息以直接访问资源。换句话说,当客户端将访问令牌传递给管理资源的服务器时,该服务器可以使用令牌中包含的信息来决定客户端是否被授权。访问令牌通常具有到期日期并且是短暂的。
  • 刷新令牌包含获取新访问令牌所需的信息。换句话说,每当访问令牌需要访问特定资源时,客户端可以使用刷新令牌来获得由认证服务器发布的新访问令牌。常见用例包括在旧的访问令牌过期后获取新访问令牌,或者首次访问新资源。刷新令牌也可以过期,但相当长寿。刷新令牌通常受到严格的存储要求,以确保它们不会泄露。它们也可以被授权服务器列入黑名单

服务器生成token的过程中,会有两个时间,一个是token失效时间,一个是token刷新时间,刷新时间肯定比失效时间长,当用户的 token 过期时,你可以拿着过期的token去换取新的token,来保持用户的登陆状态,当然你这个过期token的过期时间必须在刷新时间之内,如果超出了刷新时间,那么返回的依旧是 401

所以要实现无痛刷新token,我们应该这样

  1. 在axios的拦截器中加入token刷新逻辑
  2. 当用户token过期时,去向服务器请求新的 token
  3. 把旧的token替换为新的token
  4. 然后继续用户当前的请求
instance.interceptors.response.use(
  function (response) {
    
    
    // 对响应数据做点什么
    return response.data
  },
  function (error) {
    
    
    console.log(error)

    if (error.response) {
    
    
      if (error.response.status === 401) {
    
    
       
        // 如果当前路由不是login,并且用户有 “记住密码” 的操作
        // 那么去请求新 token
        if (router.currentRoute.name !== 'login') {
    
    
          if (getRemember() && getRefreshToken()) {
    
    

            return doRequest(error)
          } else {
    
    

            Message.error('登陆过期请重新登陆!')
            setToken('')
            router.push({
    
    
              name: 'login'
            })
          }
        }
      }
    }

    // 对响应错误做点什么
    return Promise.reject(error.response)
  }
)
/、刷新token,并且重新发送上一个请求
async function doRequest (error) {
    
    
  const data = await store.dispatch('refreshToken')
  let {
    
     token_type: tokenType, access_token: accessToken } = data

  let token = tokenType + accessToken
  let config = error.response.config
  
  config.headers.Authorization = token

  const res = await axios.request(config)

  return res
}

这里我们一定要用同步的方法来进行这一系列操作!!(比如 async / await)

2,后端

基本思路

  • 单个token的做法

    1. token(A)过期设置为15分钟
    2. 前端发起请求,后端验证token(A)是否过期;如果过期,前端发起刷新token请求,后端设置已再次授权标记为true,请求成功
    3. 前端发起请求,后端验证再次授权标记,如果已经再次授权,则拒绝刷新token的请求,请求成功
    4. 如果前端每隔72小时,必须重新登录,后端检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败
  • 授权token加上刷新token的做法:用户仅登录一次,用户改变密码,则废除token,重新登录

1.0实现

  1. 登录成功,返回access_tokenrefresh_token,客户端缓存此两种token
  2. 使用access_token请求接口资源,成功则调用成功;如果token超时(捕获401状态码),客户端
    携带refresh_token调用中间件接口获取新的access_token;
  3. 中间件接受刷新token的请求后,检查refresh_token是否过期。
    如过期,拒绝刷新,客户端收到该状态后,跳转到登录页;
    如未过期,生成新的access_token和refresh_token并返回给客户端(如有可能,让旧的refresh_token失效),客户端携带新的access_token重新调用上面的资源接口。
  4. 客户端退出登录或修改密码后,调用中间件注销旧的token(使access_token和refresh_token失效),同时清空客户端的access_token和refresh_toke。

2.0实现

场景: access_token访问资源 refresh_token授权访问 设置固定时间X必须重新登录

  1. 登录成功,后台jwt生成access_token(jwt有效期30分钟)refresh_token(jwt有效期15天),并缓存到redis(hash-key为token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token,也可以根据sub-key,废除部分设备的token。),设置过期时间为1个月,保证最终所有token都能删除),返回后,客户端缓存此两种token;

  2. 使用access_token请求接口资源,校验成功且redis中存在该access_token(未废除)则调用成功;如果token超时,中间件删除access_token(废除);客户端再次携带refresh_token调用中间件接口获取新的access_token;

  3. 中间件接受刷新token的请求后,检查refresh_token是否过期。
    如过期,拒绝刷新,删除refresh_token(废除); 客户端收到该状态后,跳转到登录页;
    如未过期,检查缓存中是否有refresh_token(是否被废除),如果有,则生成新的access_token并返回给客户端,客户端接着携带新的access_token重新调用上面的资源接口。

  4. 客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token和refresh_token(废除)),同时清空客户端侧的access_token和refresh_toke

  5. 如手机丢失,可以根据手机号人工废除指定用户设备关联的token。

  6. 以上3刷新access_token可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:失效,长时间未登录,频繁刷新)

部分代码

	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
		// 判断用户是否想要登入
		if (this.isLoginAttempt(request, response)) {
    
    
			try {
    
    
				// 进行Shiro的登录UserRealm
				this.executeLogin(request, response);
			} catch (Exception e) {
    
    
				// 认证出现异常,传递错误信息msg
				String msg = e.getMessage();
				// 获取应用异常(该Cause是导致抛出此throwable(异常)的throwable(异常))
				Throwable throwable = e.getCause();
				if (throwable != null && throwable instanceof SignatureVerificationException) {
    
    
					// 该异常为JWT的AccessToken认证失败(Token或者密钥不正确)
					msg = "token或者密钥不正确(" + throwable.getMessage() + ")";
				} else if (throwable != null && throwable instanceof TokenExpiredException) {
    
    
					// 该异常为JWT的AccessToken已过期,判断RefreshToken未过期就进行AccessToken刷新
					if (this.refreshToken(request, response)) {
    
    
						return true;
					} else {
    
    
						msg = "token已过期(" + throwable.getMessage() + ")";
					}
				} else {
    
    
					// 应用异常不为空
					if (throwable != null) {
    
    
						// 获取应用异常msg
						msg = throwable.getMessage();
					}
				}
				/**
				 * 错误两种处理方式 1. 将非法请求转发到/401的Controller处理,抛出自定义无权访问异常被全局捕捉再返回Response信息 2.
				 * 无需转发,直接返回Response信息 一般使用第二种(更方便)
				 */
				// 直接返回Response信息
				this.response401(request, response, msg);
				return false;
			}
		}
		return true;
	}
	/**
	 * 此处为AccessToken刷新,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
	 */
	private boolean refreshToken(ServletRequest request, ServletResponse response) {
    
    
		// 拿到当前Header中Authorization的AccessToken(Shiro中getAuthzHeader方法已经实现)
		String token = this.getAuthzHeader(request);
		// 获取当前Token的帐号信息
		String account = JwtUtil.getClaim(token, JwtConstant.ACCOUNT);
		// 判断Redis中RefreshToken是否存在
		if (redis.hasKey(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account)) {
    
    
			// Redis中RefreshToken还存在,获取RefreshToken的时间戳
			String currentTimeMillisRedis = redis.get(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account).toString();
			// 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
			if (JwtUtil.getClaim(token, JwtConstant.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
    
    
				// 获取当前最新时间戳
				String currentTimeMillis = String.valueOf(System.currentTimeMillis());
				// 读取配置文件,获取refreshTokenExpireTime属性
				// PropertiesUtil.readProperties("config.properties");
				// String refreshTokenExpireTime =
				// PropertiesUtil.getProperty("refreshTokenExpireTime");
				// 设置RefreshToken中的时间戳为当前最新时间戳,且刷新过期时间重新为30分钟过期(配置文件可配置refreshTokenExpireTime属性)
				redis.set(RedisConstant.PREFIX_SHIRO_REFRESH_TOKEN + account, currentTimeMillis,
						Integer.parseInt(refreshTokenExpireTime));
				// 刷新AccessToken,设置时间戳为当前最新时间戳
				token = JwtUtil.sign(account, currentTimeMillis);
				// 将新刷新的AccessToken再次进行Shiro的登录
				JwtToken jwtToken = new JwtToken(token);
				// 提交给UserRealm进行认证,如果错误他会抛出异常并被捕获,如果没有抛出异常则代表登入成功,返回true
				this.getSubject(request, response).login(jwtToken);
				// 最后将刷新的AccessToken存放在Response的Header中的Token字段返回
				HttpServletResponse httpServletResponse = (HttpServletResponse) response;
				httpServletResponse.setHeader("Token", token);
				return true;
			}
		}
		return false;
	}

猜你喜欢

转载自blog.csdn.net/saienenen/article/details/113701576