Springboot+vue+shiro前后端分离项目解决权限验证提示没有认证(Authentation)的问题

毕设做一个系统,其中涉及管理员、教师和学生三个角色,遂决定使用Springboot+vue+shiro(这三个技术只是这个记录中涉及到的三个技术或框架)。但是使用shiro的过程中遇到了非常多的问题。最后解决的问题是一直提示当前subject没有authentication。
直接把报错信息放到网上查发现没有一个解决问题的。

先说明解决方案:

(1)注解不生效,在ShiroConfig里面配置两个bean:

	@Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
     
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
     
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); // 这里需要注入 SecurityManger 安全管理器
        return authorizationAttributeSourceAdvisor;
    }

(2)在某个请求上面加上@RequireRoles注解后,后台一直报错,报错信息大概是用户没有进行认证。但是我已经登陆过了,所以肯定认证过了。在网上查询无果后,看到了一篇博客,收到了启发,加上自己理解做以下操作:
①加上session管理器,具体见博客内容
②登录成功后,将shiro的sessionid传给前端,以后每一次请求都带上这个sessionid,后端shiro会自动进行验证。
加上这两个操作后,问题解决。
具体操作如下:
①增加一个配置文件ShiroSession

import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.util.StringUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/**
 * 目的: shiro 的 session 管理
 *      自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
 *      shiro默认使用 ServletContainerSessionManager 来做 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,
 *      调用 storeSessionId  方法保存sesionId 到 cookie中
 *      为了支持无状态会话,我们就需要继承 DefaultWebSessionManager
 *      自定义生成sessionId 则要实现 SessionIdGenerator
 *
 */
public class ShiroSession extends DefaultWebSessionManager {
     
    /**
     * 定义的请求头中使用的标记key,用来传递 token
     */
    private static final String AUTH_TOKEN = "authToken";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public ShiroSession() {
     
        super();
        //设置 shiro session 失效时间,默认为30分钟,这里现在设置为15分钟
        setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
    }

    /**
     * 获取sessionId,原本是根据sessionKey来获取一个sessionId
     * 重写的部分多了一个把获取到的token设置到request的部分。这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结
     * 果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
     * @param request ServletRequest
     * @param response ServletResponse
     * @return Serializable
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
     
        //获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
        String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);

        if (StringUtils.isEmpty(sessionId)){
     
            //如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
            return super.getSessionId(request, response);

        } else {
     
            //请求头中如果有 authToken, 则其值为sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            //sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        }
    }
}

②把上面的配置注册到ShiroConfig中

// 必须使用session管理器,才能够解决前后端分离shiro的subject未认证的问题
    @Bean
    public SessionManager sessionManager(){
     
        //将我们继承后重写的shiro session 注册
        ShiroSession shiroSession = new ShiroSession();
        //如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
        shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
        return shiroSession;
    }

③在登录验证的LoginController中,主动获取当前subject的sessionid,然后传给前端

// 封装用户数据,准备shiro登录
        Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken userToken = new UsernamePasswordToken(userid, password);
        try {
     
            // 进入shiro登录
            currentUser.login(userToken);
            // 将token,角色,shiro session id信息返回给客户端
            HashMap<String, Object> map = new HashMap<>();
            // shiro的sessionID
            String authToken = (String) currentUser.getSession().getId();
            map.put("authToken", authToken);
            
            // 这里的ResultVOUtil是我自己写的一个返回数据的文件,根据实际情况返回数据即可
            // 这里的ResultVOUtil是我自己写的一个返回数据的文件,根据实际情况返回数据即可
            
            return ResponseEntity.ok().body(ResultVOUtil.success(map));
        } catch (UnknownAccountException uae) {
     
            return ResponseEntity.ok().body(ResultVOUtil.error(1, "当前用户不存在"));
        } catch (IncorrectCredentialsException ice) {
     
            return ResponseEntity.ok().body(ResultVOUtil.error(2, "密码错误"));
        } catch (LockedAccountException lae) {
     
            return ResponseEntity.ok().body(ResultVOUtil.error(3, "用户被锁定,请联系管理员"));
        } catch (AuthenticationException ae) {
     
            return ResponseEntity.ok().body(ResultVOUtil.error(4, "未知错误,请联系管理员"));
        }

④前端登陆回调函数中接收到Shiro的当前subject的sessionid,保存到localstorage中

localStorage.setItem("authToken", res.data.authToken)

⑤每一次ajax请求,都带上authToken,具体在main.js文件中配置(当然要先导入ajax等等操作)。这里的逻辑是:每一次发送ajax请求之前,检查是否访问的是登录页面,如果不是,那么就需要携带token;检查localstorage中是否存在token,若存在,就获取两个token(一个是验证当前用户,是我自己实现的,另一个是Shiro的sessionid,也就是authToekn),如果没有token,说明之前尚未登录过,则回到首页,也就是登录页。

axios.interceptors.request.use(
  config => {
     
    // 给每个请求都加上token请求头 || config.url === '/checkLogin' && (localStorage.getItem('token') != null)
    if (config.url !== 'checkLogin') {
     
      if (localStorage.getItem('token')) {
     
        config.headers.token = localStorage.getItem('token');
        config.headers.authToken = localStorage.getItem('authToken');
      } else {
     
        this.$router.push('/');
      }
    }
    return config;
  },
  error => {
     
    return Promise.reject(error);
  }
);

最重要:说一下整个项目的环境,先判断情况是否一样再看对自己是否有帮助:

(1)前端使用vue,后端使用springboot+shiro,前后端分离
(2)前端发起ajax请求,后端对请求进行权限验证(我要的是角色验证,即使用@RequiresRoles注解)
(3)后端没有其他报错,但是当前端访问添加了注解的请求时,前端有两个请求(options和正常的post请求,具体懂的人都懂,跨域请求必然有这两个步骤),options请求正常,post请求在浏览器控制台提示this subject is annonymous…,差不多意思就是当前用户没有认证,后端控制台信息直接是空指针异常。

最后说自己的理解

发生这种异常的原因是前后端跨域,后端shiro获取到的session每一次都是不同的。这很像我没有使用shiro之前要对用户进行认证一样(要在请求头中携带一个token才能识别是当前用户,然后去redis中判断是否存在当前用户)。既然如此,在网上查阅到的博文的基础上,我在登陆成功后,像验证用户一样把shiro的sessionid传给前端,前端每次请求都带上就好了。问题于是迎刃而解。

其他优化

(1)异常捕获
如果用户没有权限,会直接抛出异常,我设置的setUnauthorizeUrl()也没有生效,所以需要自己捕获异常。具体在后端增加一项。601状态码是没有权限,602是权限验证失败(如果当前subject过期了可能会出现这个错误,我做了redis有效期验证的,所以没有遇到)

import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@ControllerAdvice
public class NoPermissionException {
     
    // 没有权限时抛出的异常
    @ResponseBody
    @ExceptionHandler(UnauthorizedException.class)
    public void handleShiroException(HttpServletResponse resp) throws IOException {
     
        resp.setStatus(601);
        resp.getWriter().append("U do not have the power to do this.");
    }

    // 权限校验失败时抛出的异常
    @ResponseBody
    @ExceptionHandler(AuthorizationException.class)
    public void AuthorizationException(HttpServletResponse resp) throws IOException {
     
        resp.setStatus(602);
        resp.getWriter().append("the power check is failed somehow, please logout and login and try again.");
    }
}

(2)前端对返回的数据预处理,识别601和602,在main.js中:

//异步请求后,判断token是否过期
axios.interceptors.response.use(
  response => {
     
    return response;
  },
  error => {
     
    if (error && error.response) {
     
		switch (error.response.status) {
     
        	case 601: Message.error('无权进行当前操作'); break;
			case 602: Message.error('权限验证失败,请退出登陆后重试');break;
			default: Message.error('出错,请联系管理员');
		}
	}else{
     
		error.message ='连接服务器失败!'
	}
    return error;
  }
)

自此问题基本上得到了解决,可能还有其他优化,暂时还没有遇到。

文末附上参考的博客链接:
前后端分离时后端shiro权限认证
SpringBoot集成Shiro注解不起作用解决
还有一篇是关于异常捕获处理的,我没收藏,就不附链接了,csdn上应该能找到

你可能感兴趣的:(shiro,springboot,vue)