赞
踩
毕设做一个系统,其中涉及管理员、教师和学生三个角色,遂决定使用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上应该能找到
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。