毕设做一个系统,其中涉及管理员、教师和学生三个角色,遂决定使用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上应该能找到