Shiro整合JWT+Token过期刷新,全都帮你整好了

最近使用SpringBoot集成Shiro,JWT快速搭建了一个后台系统,Shiro前面已经使用过,JWT(JSON Web Tokens)是一种用于安全的传递信息而采用的一种标准。Web系统中,我们使用加密的Json来生成Token在服务端与客户端无状态传输,代替了之前常用的Session。
系统采用Redis作为缓存,解决Token过期更新的问题,同时集成SSO登录,完整过程这里来总结一下。

0. JWT登录主要流程:

  1. 登录时,密码验证通过,取当前时间戳生成签名Token,放在Response Header的Authorization属性中,同时在缓存中记录值为当前时间戳的RefreshToken,并设置有效期。
  2. 客户端请求每次携带Token进行请求。
  3. 服务端每次校验请求的Token有效后,同时比对Token中的时间戳与缓存中的RefreshToken时间戳是否一致,一致则判定Token有效。
  4. 当请求的Token被验证时抛出TokenExpiredException异常时说明Token过期,校验时间戳一致后重新生成Token并调用登录方法。
  5. 每次生成新的Token后,同时要根据新的时间戳更新缓存中的RefreshToken,以保证两者时间戳一致。

1. Shiro配置

首先是Shiro的配置,定义两个类ShiroChonfig以及ShiroRealm用来配置Shiro,以及验证部分。
这里重要的是关闭Session,因为我们使用JWT来传输安全信息。自定义缓存管理器,同时我们要添加一个JwttFilter,将所有的请求交由它处理。

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

    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public static DefaultAdvisorAutoProxyCreator getLifecycleBeanPostProcessor() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

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

    @Bean
    public DefaultWebSecurityManager  securityManager(ShiroRealm shiroRealm,ShiroCacheManager shiroCacheManager){
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();

        securityManager.setRealm(shiroRealm);

        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

		//自定义缓存管理
        securityManager.setCacheManager(shiroCacheManager);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        // 添加jwt过滤器
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", jwtFilter());
        filterMap.put("logout", new SystemLogoutFilter());
        shiroFilter.setFilters(filterMap);

        //拦截器
        Map<String,String> filterRuleMap = new LinkedHashMap<>();
        filterRuleMap.put("/logout", "logout");
        filterRuleMap.put("/**", "jwt");
        shiroFilter.setFilterChainDefinitionMap(filterRuleMap);

        return shiroFilter;
    }

    @Bean
    public JwtFilter jwtFilter(){
        return new JwtFilter();此处为AccessToken
    }
}

用户验证以及权限验证的地方,用户验证多加了一个校验,就是我们当前请求的token中包含的时间戳与缓存中的RefreshToken对比,一致才验证通过。

@Service
public class ShiroRealm extends AuthorizingRealm {
	@Autowired
	private IBpUserService userService;
	@Autowired
	private IBpRoleService roleService;
	@Autowired
	private IBpAuthorityService bpAuthorityService;
	@Autowired
	private CacheClient cacheClient;
	
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof JwtToken;
	}

	/**
	 * 用户名信息验证
	 * @param auth
	 * @return
	 * @throws AuthenticationException
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth)
	        throws AuthenticationException {
		String token = (String)auth.getPrincipal();
		String account  = JwtUtil.getClaim(token,SecurityConsts.ACCOUNT);

		if (account == null) {
			throw new AuthenticationException("token invalid");
		}

		BpUser bpUserInfo = userService.findUserByAccount(account);
		if (bpUserInfo == null) {
			throw new AuthenticationException("BpUser didn't existed!");
		}

		String refreshTokenCacheKey = SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account;
		if (JwtUtil.verify(token) && cacheClient.exists(refreshTokenCacheKey)) {
			String currentTimeMillisRedis = cacheClient.get(refreshTokenCacheKey);
			// 获取AccessToken时间戳,与RefreshToken的时间戳对比
			if (JwtUtil.getClaim(token, SecurityConsts.CURRENT_TIME_MILLIS).equals(currentTimeMillisRedis)) {
				return new SimpleAuthenticationInfo(token, token, "shiroRealm");
			}
		}
		throw new AuthenticationException("Token expired or incorrect.");
	}

	/**
	 * 检查用户权限
	 * @param principals
	 * @return
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
	    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

		String account = JwtUtil.getClaim(principals.toString(), SecurityConsts.ACCOUNT);
		BpUser bpUserInfo = userService.findUserByAccount(account);

		//获取用户角色
	    List<BpRole> bpRoleList = roleService.findRoleByUserId(bpUserInfo.getId());
	    //获取权限
		List<Object> bpAuthorityList = bpAuthorityService.findByUserId(bpUserInfo.getId());
	    for(BpRole bpRole : bpRoleList){
	        authorizationInfo.addRole(bpRole.getName());
	        for(Object auth: bpAuthorityList){
	            authorizationInfo.addStringPermission(auth.toString());
	        }
	    }
	    return authorizationInfo;
	}
}

这里我们定义了一些常量,其中有请求头包含的Token的属性,以及放入缓存中的Key

public class SecurityConsts {
    public static final String LOGIN_SALT = "storyweb-bp";
    //request请求头属性
    public static final String REQUEST_AUTH_HEADER="Authorization";

    //JWT-account
    public static final String ACCOUNT = "account";

    //Shiro redis 前缀
    public static final String PREFIX_SHIRO_CACHE = "storyweb-bp:cache:";

    //redis-key-前缀-shiro:refresh_token
    public final static String PREFIX_SHIRO_REFRESH_TOKEN = "storyweb-bp:refresh_token:";

    //JWT-currentTimeMillis
    public final static String CURRENT_TIME_MILLIS = "currentTimeMillis";
}

2. JWT 配置

这里我们有几个参数放在配置文件中:

token:
  #  token过期时间,单位分钟
  tokenExpireTime: 120
  #  RefreshToken过期时间,单位:分钟, 24*60=1440
  refreshTokenExpireTime: 1440
  #  shiro缓存有效期,单位分钟,2*60=120
  shiroCacheExpireTime: 120
  #  token加密密钥
  secretKey: storywebkey
@ConfigurationProperties(prefix = "token")
@Data
public class JwtProperties {
    //token过期时间,单位分钟
    Integer tokenExpireTime;
    //刷新Token过期时间,单位分钟
    Integer refreshTokenExpireTime;
    //Shiro缓存有效期,单位分钟
    Integer shiroCacheExpireTime;
    //token加密密钥
    String secretKey;
}

当然了,你需要在SpringBoot的Application启动类中,加入注解:
@EnableConfigurationProperties({JwtProperties.class})

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;
    }
}

接下来是Jwt的Fiter,集成自Shiro的BasicHttpAuthenticationFilter,这里的注释比较详细。

public class JwtFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    @Autowired
    CacheClient cacheClient;
    @Autowired
    JwtProperties jwtProperties;

    /**
     * 检测Header里Authorization字段
     * 判断是否登录
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader(SecurityConsts.REQUEST_AUTH_HEADER);
        return authorization != null;
    }

    /**
     * 登录验证
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader(SecurityConsts.REQUEST_AUTH_HEADER);

        JwtToken token = new JwtToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);

        // 绑定上下文
        String account = JwtUtil.getClaim(authorization, SecurityConsts.ACCOUNT);
        UserContext userContext= new UserContext(new LoginUser(account));

        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
     */
    private boolean refreshToken(ServletRequest request, ServletResponse response) {
        // 获取AccessToken(Shiro中getAuthzHeader方法已经实现)
        String token = this.getAuthzHeader(request);
        // 获取当前Token的帐号信息
        String account = JwtUtil.getClaim(token, SecurityConsts.ACCOUNT);
        String refreshTokenCacheKey = SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account;
        // 判断Redis中RefreshToken是否存在
        if (cacheClient.exists(refreshTokenCacheKey)) {
            // 获取RefreshToken时间戳,及AccessToken中的时间戳
            // 相比如果一致,进行AccessToken刷新
            String currentTimeMillisRedis = cacheClient.get(refreshTokenCacheKey);
            String tokenMillis=JwtUtil.getClaim(token, SecurityConsts.CURRENT_TIME_MILLIS);

            if (tokenMillis.equals(currentTimeMillisRedis)) {

                // 设置RefreshToken中的时间戳为当前最新时间戳
                String currentTimeMillis = String.valueOf(System.currentTimeMillis());
                Integer refreshTokenExpireTime = jwtProperties.refreshTokenExpireTime;
                cacheClient.set(refreshTokenCacheKey, currentTimeMillis,refreshTokenExpireTime*60l);

                // 刷新AccessToken,为当前最新时间戳
                token = JwtUtil.sign(account, currentTimeMillis);

                // 使用AccessToken 再次提交给ShiroRealm进行认证,如果没有抛出异常则登入成功,返回true
                JwtToken jwtToken = new JwtToken(token);
                this.getSubject(request, response).login(jwtToken);

                // 设置响应的Header头新Token
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setHeader(SecurityConsts.REQUEST_AUTH_HEADER, token);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", SecurityConsts.REQUEST_AUTH_HEADER);
                return true;
            }
        }
        return false;
    }

    /**
     * 是否允许访问
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                this.executeLogin(request, response);
            } catch (Exception e) {
                String msg = e.getMessage();
                Throwable throwable = e.getCause();
                if (throwable != null && throwable instanceof SignatureVerificationException) {
                    msg = "Token或者密钥不正确(" + throwable.getMessage() + ")";
                } else if (throwable != null && throwable instanceof TokenExpiredException) {
                    // AccessToken已过期
                    if (this.refreshToken(request, response)) {
                        return true;
                    } else {
                        msg = "Token已过期(" + throwable.getMessage() + ")";
                    }
                } else {
                    if (throwable != null) {
                        msg = throwable.getMessage();
                    }
                }
                this.response401(request, response, msg);
                return false;
            }
        }
        return true;
    }

    /**
     * 401非法请求
     * @param req
     * @param resp
     */
    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();

            Result result = new Result();
            result.setResult(false);
            result.setCode(Constants.PASSWORD_CHECK_INVALID);
            result.setMessage(msg);
            out.append(JSON.toJSONString(result));
        } catch (IOException e) {
            LOGGER.error("返回Response信息出现IOException异常:" + e.getMessage());
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }
}

这里再重复一下:当请求验证Token时抛出TokenExpiredException异常后,校验缓存中的RefreshToken的时间戳是否与当前请求Token时间戳一致,倘若一致,则重新生成Token,以当前时间戳更新缓存中的RefreshToken时间戳;倘若不一致,则以Json格式直接响应401未登录错误。
采用前后端分离的方式,我们的401就需要直接返回JSON格式的响应。

@Component
public class JwtUtil {

    @Autowired
    JwtProperties jwtProperties;

    @Autowired
    private static JwtUtil jwtUtil;

    @PostConstruct
    public void init() {
        jwtUtil = this;
        jwtUtil.jwtProperties = this.jwtProperties;
    }

    /**
     * 校验token是否正确
     * @param token
     * @return
     */
    public static boolean verify(String token) {
        String secret = getClaim(token, SecurityConsts.ACCOUNT) + jwtUtil.jwtProperties.secretKey;
        Algorithm algorithm = Algorithm.HMAC256(secret);
        JWTVerifier verifier = JWT.require(algorithm)
                .build();
        verifier.verify(token);
        return true;
    }

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

    /**
     * 生成签名,5min后过期
     * @param account
     * @param currentTimeMillis
     * @return
     */
    public static String sign(String account, String currentTimeMillis) {
        // 帐号加JWT私钥加密
        String secret = account + jwtUtil.jwtProperties.getSecretKey();
        // 此处过期时间,单位:毫秒
        Date date = new Date(System.currentTimeMillis() + jwtUtil.jwtProperties.getTokenExpireTime()*60*1000l);
        Algorithm algorithm = Algorithm.HMAC256(secret);

        return JWT.create()
                .withClaim(SecurityConsts.ACCOUNT, account)
                .withClaim(SecurityConsts.CURRENT_TIME_MILLIS, currentTimeMillis)
                .withExpiresAt(date)
                .sign(algorithm);
    }
}

3. 绑定当前上下文用户

用户登录后,在业务里想要获取当前登录用户信息,一是可以在登录时缓存用户信息,二是少量信息从token里拿,这里当每次验证请求成功后,我们都将当前用户信息绑定到当前的上下文中,这里我只提取了账号。

@Data
public class LoginUser implements Serializable {
    private static final long serialVersionUID = 1L;

    public Long userId;          // 主键ID
    public String account;      // 账号
    public String name;         // 姓名

	public LoginUser() {
	}

	public LoginUser(String account) {
		this.account=account;
	}

	public LoginUser(Long userId, String account, String name) {
		this.userId = userId;
		this.account = account;
		this.name = name;
	}
}


public class UserContext implements AutoCloseable {
    static final ThreadLocal<LoginUser> current = new ThreadLocal<>();

    public UserContext(LoginUser user) {
        current.set(user);
    }

    public static LoginUser getCurrentUser() {
        return current.get();
    }

    public void close() {
        current.remove();
    }
}

4. 缓存

缓存这里的实现,可以自己完善,这里只实现了部分的方法。

@Service
public class ShiroCacheManager implements CacheManager {
    @Autowired
    CacheClient cacheClient;

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new ShiroCache<K,V>(cacheClient);
    }
}

/**
 * 重写Shiro的Cache保存读取
 * @param <K>
 * @param <V>
 */
public class ShiroCache<K,V> implements Cache<K,V> {

    private CacheClient cacheClient;

    public ShiroCache(CacheClient cacheClient) {
        this.cacheClient = cacheClient;
    }

    /**
     * 获取缓存
     * @param key
     * @return
     * @throws CacheException
     */
    @Override
    public Object get(Object key) throws CacheException {
        String tempKey= this.getKey(key);
        if(cacheClient.exists(tempKey)){
            return cacheClient.getObject(tempKey);
        }
        return null;
    }

    /**
     * 保存缓存
     * @param key
     * @param value
     * @return
     * @throws CacheException
     */
    @Override
    public Object put(Object key, Object value) throws CacheException {
        return cacheClient.setObject(this.getKey(key), value);
    }

    /**
     * 移除缓存
     * @param key
     * @return
     * @throws CacheException
     */
    @Override
    public Object remove(Object key) throws CacheException {
        String tempKey= this.getKey(key);
        if(cacheClient.exists(tempKey)){
            cacheClient.del(tempKey);
        }
        return null;
    }

    @Override
    public void clear() throws CacheException {}

    @Override
    public int size() {
		//@TODO
        return 20;
    }

    @Override
    public Set<K> keys() {
        return null;
    }

    @Override
    public Collection<V> values() {
        Set keys = this.keys();
        List<V> values = new ArrayList<>();
        for (Object key : keys) {
            values.add((V)cacheClient.getObject(this.getKey(key)));
        }
        return values;
    }

    /**
     * 根据名称获取
     * @param key
     * @return
     */
    private String getKey(Object key) {
        return SecurityConsts.PREFIX_SHIRO_CACHE + JwtUtil.getClaim(key.toString(), SecurityConsts.ACCOUNT);
    }
}
//shiro工具类
public class ShiroKit {
    public final static String hashAlgorithmName = "MD5";

    //循环次数
    public final static int hashIterations = 1024;

    /**
     * shiro密码加密工具类
     *
     * @param credentials 密码
     * @param saltSource 密码盐
     * @return
     */
    public static String md5(String credentials, String saltSource) {
        ByteSource salt = new Md5Hash(saltSource);
        return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations).toString();
    }
}

5. 登录

@Controller
@RequestMapping(value="/user")
public class LoginController {

    @Autowired
    IBpUserService bpUserService;

    /**
     * 登录
     * @param user
     * @return
     */
    @SuppressWarnings("unchecked")
    @RequestMapping(value="/login")
    @ResponseBody
    public Result login(HttpServletResponse response,@RequestBody User user) {
        return bpUserService.login(user,response);
    }
}

//Service类
@Service
public class BpUserServiceImpl extends ServiceImpl<BpUserMapper, BpUser> implements IBpUserService {
    @Autowired
    CacheClient CacheClient;

     /**
     * 用户登录
     * @param user
     * @return
     */
    @Override
    public Result login(User user, HttpServletResponse response) {
        Assert.notNull(user.getUsername(), "用户名不能为空");
        Assert.notNull(user.getPassword(), "密码不能为空");

        BpUser userBean = this.findUserByAccount(user.getUsername());

        if(userBean==null){
            return new Result(false, "用户不存在", null, Constants.PASSWORD_CHECK_INVALID);
        }

        //域账号直接提示账号不存在
        if ("1".equals(userBean.getDomainFlag())) {
            return new Result(false, "账号不存在", null, Constants.PASSWORD_CHECK_INVALID);
        }

        String encodePassword = ShiroKit.md5(user.getPassword(), SecurityConsts.LOGIN_SALT);
        if (!encodePassword.equals(userBean.getPassword())) {
            return new Result(false, "用户名或密码错误", null, Constants.PASSWORD_CHECK_INVALID);
        }

        //账号是否锁定
        if ("0".equals(userBean.getStatus())) {
            return new Result(false, "该账号已被锁定", null, Constants.PASSWORD_CHECK_INVALID);
        }

        //验证成功后处理
        this.loginSuccess(userBean.getAccount(),response);

        //登录成功
        return new Result(true, "登录成功", null ,Constants.TOKEN_CHECK_SUCCESS);
    }

    /**
     * 登录后更新缓存,生成token,设置响应头部信息
     * @param account
     * @param response
     */
    private void loginSuccess(String account, HttpServletResponse response){

        String currentTimeMillis = String.valueOf(System.currentTimeMillis());

        // 清除可能存在的Shiro权限信息缓存
        String tokenKey=SecurityConsts.PREFIX_SHIRO_CACHE + account;
        if (cacheClient.exists(tokenKey)) {
            cacheClient.del(tokenKey);
        }

        //更新RefreshToken缓存的时间戳
        String refreshTokenKey= SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account;
        if (cacheClient.exists(refreshTokenKey)) {
            cacheClient.set(refreshTokenKey, currentTimeMillis, jwtProperties.getRefreshTokenExpireTime()*60*60l);
        }else{
            cacheClient.set(refreshTokenKey, currentTimeMillis, jwtProperties.getRefreshTokenExpireTime()*60*60l);
        }

        //生成token
        JSONObject json = new JSONObject();
        String token = JwtUtil.sign(account, currentTimeMillis);
        json.put("token",token );

        //写入header
        response.setHeader(SecurityConsts.REQUEST_AUTH_HEADER, token);
        response.setHeader("Access-Control-Expose-Headers", SecurityConsts.REQUEST_AUTH_HEADER);
    }
}

登录成功后,我们在生成Token的同时,将当前时间戳以RefreshToken为Key存入Redis,用于Token过期时的校验及刷新。

当我们在业务中需要访问上下文用户时,可以这样获取:
UserContext.getCurrentUser().getAccount()


6. 注销登录状态

采用前后端分离的方式,当用户注销后,后端依然是以Json方式返回,因此,我们通过过滤器处理请求,注销完成返回Json结果。
再前面,我们已经添加了自定义的过滤器SystemLogoutFilter到Shiro的ShiroFilterFactoryBean中,这里只要实现就可以了。

public class SystemLogoutFilter extends LogoutFilter {
    private static final Logger logger = LoggerFactory.getLogger(SystemLogoutFilter.class);

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        try {
            subject.logout();
        } catch (Exception ex) {
            logger.error("退出登录错误",ex);
        }

        this.writeResult(response);
        //不执行后续的过滤器
        return false;
    }

    private void writeResult(ServletResponse response){
        //响应Json结果
        PrintWriter out = null;
        try {
            out = response.getWriter();
            Result result = new Result(true,null,null,Constants.TOKEN_CHECK_SUCCESS);
            out.append(JSON.toJSONString(result));
        } catch (IOException e) {
            logger.error("返回Response信息出现IOException异常:" + e.getMessage());
        } finally {
            if (out != null) {
                out.close();
            }
        }
    }
}

7. 添加依赖

把依赖放到最后,因为这个不需要说。

<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-ehcache</artifactId>
	<version>1.4.0</version>
</dependency>
<!--JWT-->
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.4.1</version>
</dependency>
<!--Redis-->
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.0</version>
</dependency>

后续补充

  1. 关于本篇中Token刷新方案做了一些修改,详见 采用JWT有效期内刷新Token方案,解决并发请求问题
  2. 问的朋友比较多,于是就将项目后端代码上传至GitHub,地址:https://github.com/sunnj/story-admin
  3. 本项目的前端仓库地址:https://github.com/sunnj/story-admin-console

参考资料

  1. JSON Web Token 入门教程
  2. Shiro+JWT+Spring Boot Restful简易教程
  3. SpringBoot + Shiro + JWT集成Redis缓存(Jedis)

58 thoughts on “Shiro整合JWT+Token过期刷新,全都帮你整好了

    1. CacheClient 是一个Redis通用操作类,用自己的替换掉就可以了

  1. 你好,为什么tokenMillis.equals(currentTimeMillisRedis)为报空指针?

    1. tokenMillis为null说明没有从你的token中获取到currentTimeMillis信息,检查下你生成token的地方是否有传入

        1. refreshTokenCacheKey = SecurityConsts.PREFIX_SHIRO_REFRESH_TOKEN + account
          refreshTokenCacheKey不会为null,cacheClient是你的Redis工具类,如果你的exists方法是static的应该不会空指针,
          不是static,那你的cacheClient就应该是需要注入的,用@Autowired标记下。

          1. = =就是加了注解了还是为空指针很奇怪

          1. 设置了jwtFilter 后 所有请求都会走这个, 上面设置的anon 没生效,请问是什么原因啊

  2. 感觉注销登录状态要手动去清理 redis里面的refreshtoken,这一步是不是没写啊

  3. 我是真是服了,为什么我的jwt过滤器不能注入redis工具类?@autowired根本不能注入,即便在类上加@Component注解依然是null。非spring管理的类调用spring容器的bean像你这样会报空指针异常

    1. 不知道你为什么可以这样运行,我对spring内部原理也不太懂,人都要被搞死了,唉

    2. 因为过滤器不由spring管理,如果你想加的话,可以在使用前注入

  4. 设置了jwtFilter 后 所有请求都会走这个, 上面设置的anon 没生效,请问是什么原因啊

    1. 可以删掉ShiroConfig类中的public JwtFilter jwtFilter(){return new JwtFilter();}方法上的注解@Bean试下。

      1. 删掉后,anon确实能生效,但是不知道为何jwtFilter中的所有通过@Autowired注解的实体都注入不进来? 麻烦博主解答下。

  5. 有些方法需要验证,有些方法不需要验证,这个怎么区分并验证

  6. 你好运行到String account = JwtUtil.getClaim(token, SecurityConsts.ACCOUNT);就会报错请问为什么呢?

    1. 这里是提取token中的信息,可以贴出错误信息具体分析看是什么问题。

  7. token超时之后程序没走到refreshToken里,发现是Throwable throwable = e.getCause(); throwable 的类型是AuthenticationException,else if (throwable != null && throwable instanceof TokenExpiredException)是false,博主这里是怎么搞得?

    1. 应该token过期后,在verifier.verify(token);验证时抛出过期异常导致。
      文末提供了另一种token有效期内刷新方案,以及项目的git地址供君参考。

  8. 登录验证没有调用subject.login方法的话,那realm中的doGetAuthenticationInfo方法存在的意义是什么?

    1. realm中的doGetAuthenticationInfo这里主要用于验证token的有效性。登录时的token是新生成的,因此没有调用subject.login方法。当然你也可以调用:

      Subject subject = SecurityUtils.getSubject();
              AuthenticationToken token= new JwtToken(strToken);
              subject.login(token);
      
  9. doGetAuthorizationInfo(PrincipalCollection principalCollection)
    一个请求,这个方法为什么走了10遍呢

    1. 我也遇到同样的问题,当doGetAuthorizationInfo报错,就会运行两次doGetAuthorizationInfo方法,不知道是为什么

      1. doGetAuthorizationInfo 不知道你说的报错是为什么报错?父类中的onAccessDenied方法中倒是会再次调用executeLogin,而我们在JwtFilter中只重写了isAccessAllowed,并没有重写onAccessDenied,是会导致executeLogin被重新调用。我重写了onAccessDenied,代码已经提交,你可以重新pull测试下。

  10. 大佬,我在方法上加@RequiresAuthentication这个注解可以正常使用,但是如果使用@RequiresPermissions(“system:role:list”)这种的就会报异常,求指导啊

    1. 得看报什么异常?
      需要权限访问的Aciton是会进入doGetAuthorizationInfo方法的,当然如果配置了缓存,也会直接从缓存读取,而不调用这个方法。

  11. 不好意思,登录名和密码是多少?能否讲一下postman怎么测试?

    1. admin/111111
      项目前端地址:https://github.com/sunnj/story-admin-console
      postman只需要在构造请求参数时在请求头header中带上名为Authorization的Token内容即可。

      1. 您好,前台下载运行报了这个错:
        PS D:\java2019\react-devtools-master> npm run dev
        npm ERR! missing script: dev

        npm ERR! A complete log of this run can be found in:
        npm ERR! C:\Users\admin\AppData\Roaming\npm-cache_logs\2019-09-09T14_07_47_230Z-debug.log

          1. 不好意思,我执行了的,不过报的这些信息(我重又执行了一次)
            D:\java2019\react-devtools-master>npm install
            npm WARN ajv-errors@1.0.1 requires a peer of ajv@>=5.0.0 but none is installed. You must install peer dependencies yourself.
            npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@1.2.9 (node_modules\fsevents):
            npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.9: wanted {“os”:”darwin”,”arch”:”any”} (current: {“os”:”win32″,”arch”:”x64″})

            audited 32464 packages in 8.857s
            found 76 vulnerabilities (66 low, 7 moderate, 2 high, 1 critical)
            run npm audit fix to fix them, or npm audit for details

            再执行 npm audit fix 一直没有反应。

          2. 执行完install就行,不需要执行提示里的fix。
            我重新下载了代码,跑起来没有问题。

    2. 您好,后台访问http://localhost:9430/
      出现 欢迎来到 STORY-ADMIN 页面。我又重新下载了前端。运行后,前端url:http://localhost:9428/login的登录界面也出来了,用户和密码是系统默认的。但是点击登录出现提示信息:Request failed with status code 400。
      请看看是为什么?

        1. 装上redis,前端可以登录了,数据也出来了,不过还报:Request failed with status code 404。
          不知什么原因?

connor进行回复 取消回复

电子邮件地址不会被公开。 必填项已用*标注