java前后端分离项目中使用shiro权限框架遇到的那些坑
前言
最近在做一个前后端分离的项目。前端使用vue,后端使用的是spring boot,因为需要做权限管理。就选择集成shiro框架。以前都是在传统项目中使用shiro。第一次在前后端分离的项目中使用shiro。给我带来了很大的困扰。遇到了很多麻烦。所以在此记录。方便以后查阅。也希望能让同样面临同样问题的人能节约点时间。
坑点总结
1.前后端分离项目没有部署在同一台服务器上,要面临跨域问题。
2.使用token 作为shiro认证标识
3.前后端分离项目中,未登录时用返回json代替重定向。
详解
1. 解决跨域问题
spring boot 跨域问题很好解决。使用下面代码。或者在网上搜索springboot解决跨域问题。很快便可以完成此步骤。
package com.common.config.cors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许任何域名使用
corsConfiguration.addAllowedOrigin("*");
// 允许任何头
corsConfiguration.addAllowedHeader("*");
// 允许任何方法(post、get等)
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 对接口配置跨域设置
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
2.使用token 作为shiro认证标识
一开始面临这个问题,感觉无比的复杂。shiro根据sessionId来判断是不是同一个用户发起的request请求。但是前后端分离的项目中。用户的每次请求都相当于新的请求。sessionId可能会发生变化。
我们需要的就是解决这种问题。最先想到的是使用token来代替session,让前端发送的请求都携带登录成功后返回的token令牌。(其实和session原理一样。就是以前的sessionId存储在cookie中,现在用token,将sessionId存储在了请求头中。)下面看代码。
package com.**.*.config;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;
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
* @author zzy
* @date 2020/11/18 11:23
*/
public class ShiroSession extends DefaultWebSessionManager {
private static final String AUTH_TOKEN = "authorization";
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
* @param response
* @return
*/
@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;
}
}
}
在shiro配置类中创建安全管理器的时候使用自定义ShiroSession来做会话管理。
@Bean
public SecurityManager getSecurityManager(CustomRealm realm) {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//设置自定义的realm
defaultWebSecurityManager.setRealm(realm);
//自定义的shiro session 缓存管理器
defaultWebSecurityManager.setSessionManager(sessionManager());
return defaultWebSecurityManager;
}
/**
* 自定义的 shiro session 缓存管理器,用于跨域等情况下使用 token 进行验证,不依赖于sessionId
* @return
*/
@Bean
public SessionManager sessionManager(){
//将我们继承后重写的shiro session 注册
ShiroSession shiroSession = new ShiroSession();
//如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
return shiroSession;
}
3.登录失败时,用返回json来代替重定向
一步一个坑。在解决完第二步骤的时候,发现shiro基本算是配置成功了。但是发现如果没有登录的时候访问具有登录权限的接口总是会报404错误。发现这些请求都是被重定向到了很目录下面的index.jsp页面。因为本地没有这个页面。所以引发404错误。在经过查阅资料后发现,进本都是通过配置过滤器来解决问题的。
代码如下。
package com.**.*.filter;
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import springfox.documentation.service.ResponseMessage;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author zzy
* @date 2020/11/19 11:09
*/
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("application/json; charset=utf-8");
PrintWriter out = resp.getWriter();
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", "100");
jsonObject.put("desc", "请前往登录页面");
try {
flushMsgStrToClient(response, jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
out.flush();
out.close();
return false;
}
public static void flushMsgStrToClient(ServletResponse response, Object object)
throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONObject.toJSONString(object));
response.getWriter().flush();
}
}
在配置类的过滤工厂中添加配置
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//1.创建过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
Map<String, Filter> filters = new HashMap<>();
MyFormAuthenticationFilter myFormAuthenticationFilter = new MyFormAuthenticationFilter();
filters.put("authc",myFormAuthenticationFilter);
filterFactory.setFilters(filters);
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//4.设置过滤器集合
/**
* 设置所有的过滤器:有顺序map
* key = 拦截的url地址
* value = 过滤器类型
*
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/System/SystemLogin","anon");
filterMap.put("/**","authc");
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
上一篇: 嵌入式原理实验代码集合