shiro的前后端分离模式
前言:在上一篇《shiro的简单认证和授权》中介绍了shiro的搭建,默认情况下,shiro是通过设置cookie,使前端请求带有“JSESSION”cookie,后端通过获取该cookie判断用户是否登录以及授权。但是在前后端分离模式中,前后端不在同一个域内,后端无法自动给请求添加cookie,因而默认的模式不能实现正常的认证授权逻辑。
我们可以将subject的sessionId通过登录成功的请求返回给前端,前端获取sessionId后在每个请求的header中带上sessionId(token),后端通过继承DefaultWebSessionManager类,实现自定义的session管理器,改写getSession的方法,改在header中获取sessionId进行后续认证与授权。
以下例子只是demo,不注重正式开发细节,例如返回值封装,请求Restful风格等等,正式项目中注意甄别
1.登录成功,将sessionId返回给前端
/**
* 登录接口
* @param username
* @param password
* @return
*/
@GetMapping("/login")
public String login(String username,String password){
// 获取当前主体
Subject subject = SecurityUtils.getSubject();
// 构建登录token,login方法进入中进入我们自定义的realm的doGetAuthenticationInfo方法
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);
subject.login(usernamePasswordToken);
String token = (String) SecurityUtils.getSubject().getSession().getId();
return "登陆成功,token为:"+token;
}
2.前端携带token请求
前端将获取到的token写到sessionStorage或者localStorage,然后请求时添加到请求头中,例如axios请求例子
axios({
method:"GET",
url:"http://127.0.0.1:8080/testIndex",
headers:{
'Content-Type':' application/json',
'token':'bd9ae5c3-570f-4ed2-ac57-8f935655ef55', // token从localStorage中获取
},
withCredentials: true,
}).then((r)=>{
console.log(r)
})
3.继承DefaultWebSessionManager
Shiro中DefaultWebSessionManager管理shiro的session会话,包括设置cookie,获取前端携带的shiro的sessionId等等。我们可以重写其方法,实现自己的逻辑,最后添加到SecurityManager中,例如重写getSessionId方法,改为从header中获取sessionId。
public class CustomSessionManager extends DefaultWebSessionManager {
private static final String HEADER_TOKEN = "token";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public CustomSessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader(HEADER_TOKEN);
if (!StringUtils.isEmpty(id)) {
request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled());
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return id;
} else {
return super.getSessionId(request, response);
}
}
}
4. 在ShiroConfig中加入SecurityManager
在ShiroConfig中,创建自定义的SessionManager实例,然后添加到SecurityManager
/**
* sessionManager
*/
public DefaultWebSessionManager sessionManager() {
// DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionDAO(redisSessionDAO());
sessionManager.setSessionIdCookieEnabled(false);
return sessionManager;
}
/**
* SecurityManager
*/
@Bean
public SecurityManager securityManager(Realm myRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
securityManager.setRealm(myRealm);
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(cacheManager());
return securityManager;
}
5.跨域设置
前后端分离肯定离不开跨域设置,跨域有多重设置方法,这里不赘述
/**
* 解决跨域问题
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600)
.allowedHeaders(CorsConfiguration.ALL);
// .allowedHeaders("*");
}
}
6.添加过滤器
这里是一个坑,我以为完成以上步骤就可以实现前后端分离,但是测试后发现,有一些请求符合预期,但是有一些请求却报跨域,甚至乎不是寻常的报跨域
浏览器控制台输出
如果是跨域的话,按道理讲应该所有请求都会报跨域,但是login请求没有报
那么就应该跟shiro的FilterChainDefinitionMap有关,只有拦截做验证的请求才会报错。以为是菜狗,查了好久,都没有找到正确答案。然后在调试时发现报错的请求不是我写的get请求,而是options请求。
事实上在这之前我只听说过options,并没去了解options请求的作用,看到报差后才去百度一下
Options 请求是 HTTP 协议中的一种请求方法,它用于获取服务器支持的请求方法和其他选项。Options 请求通常不会包含实际的请求数据,而是用于获取服务器的元数据信息。
Options 请求的主要作用包括:
获取服务器支持的请求方法:Options 请求可以获取服务器支持的请求方法,例如 GET、POST、PUT、DELETE 等。这对于客户端来说非常有用,因为它可以确定服务器是否支持特定的请求方法。
检查服务器的能力:Options 请求可以检查服务器的能力,例如是否支持跨域请求、是否支持特定的 HTTP 头、是否支持特定的内容类型等。
探测服务器:Options 请求可以用于探测服务器,例如确定服务器的版本、操作系统、服务器软件等信息。这对于安全测试和漏洞扫描非常有用。
Options 请求通常被视为一种安全的请求方法,因为它不会对服务器产生实际的影响。但是,服务器可能会限制 Options 请求的使用,例如限制请求频率或限制请求的来源。因此,在使用 Options 请求时,应该遵守服务器的规定,并确保请求是合法和必要的。
原来options这么重要,那报的跨域肯定就是这个options搞的鬼了。当然就算知道问题所在,我这菜狗也是想不到解决方案的,但至少会百度。
然后就查到以下文章
[SpringBoot+Shiro放行OPTIONS请求,解决跨域问题] https://blog.csdn.net/qq_41289165/article/details/121529166
第一步,新建一个Filter继承Shiro的UserFilter,里面的preHandle方法对options请求直接放行。还有其他逻辑看注释,这里不赘述
@Slf4j
public class ShiroUserFilter extends UserFilter {
/**
* 在访问过来的时候检测是否为OPTIONS请求,如果是就直接返回true
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
if (httpRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
log.info("OPTIONS放行");
setHeader(httpRequest,httpResponse);
return true;
}
return super.preHandle(request,response);
}
/**
* 该方法会在验证失败后调用,这里由于是前后端分离,后台不控制页面跳转
* 因此重写改成传输JSON数据
*/
@Override
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
setHeader((HttpServletRequest) request,(HttpServletResponse) response);
PrintWriter out = response.getWriter();
//自己控制返回的json数据
Map map = new HashMap();
map.put("code",500);
map.put("message","Shiro验证失败");
out.println(map);
out.flush();
out.close();
}
/**
* 为response设置header,实现跨域
*/
private void setHeader(HttpServletRequest request, HttpServletResponse response){
//跨域的header设置
response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Methods", request.getMethod());
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
//防止乱码,适用于传输JSON数据
response.setHeader("Content-Type","application/json;charset=UTF-8");
response.setStatus(HttpStatus.OK.value());
}
}
第二步,将该Filter添加到FilterFactoryBean中
在ShiroConfig中的shiroFilterFactoryBean方法中,添加shiroFilter.getFilters().put("authc", new ShiroUserFilter());
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// 添加自定义filter
shiroFilter.getFilters().put("authc", new ShiroUserFilter());
// 认证失败(用户没登录时)会重定向到这个loginUrl
shiroFilter.setLoginUrl("/");
// 授权失败重定向路径
shiroFilter.setUnauthorizedUrl("/noPermission");
// 定义一个LinkHashMap,保存认证的路径和规则
Map<String,String> map = new LinkedHashMap();
// key为uri,anon为标识符,标识不需要验证
map.put("/login","anon");
// authc表示Authentication,即是需要认证
map.put("/admin/**","authc");
// key “/**"表示所有uri,”/**“必须写在最后,如果不写该项,默认放行所有没配置的uri
map.put("/**","authc");
// 将该map设置进shiroFilter
shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}
7.测试
至此,前后端分离实现完毕
options请求正常
验证通过的请求能正常访问接口
补充
以上已经实现shiro的前后端分离功能,这里再补充下,将shiro默认的set-cookie行为去掉,只需要设置SessionManager的SessionIdCookieEnabled为false即可,如下在ShiroConfig中的SessionManager
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionIdUrlRewritingEnabled(false);
sessionManager.setSessionDAO(redisSessionDAO());
// 将shiro默认的set-cookie行为去掉
sessionManager.setSessionIdCookieEnabled(false);
return sessionManager;
}
至此,全篇结束