近期项目需要前后端分离,由于前后端分离后原来的适用的shiro配置无法满足现有系统要求。同时在前后端项目分离的项目中存在的跨域问题,cookies不再使用,通过token方式实现用户登陆鉴权。
下面记录在整个过程中涉及的几个大问题:1、跨域问题 2、sessionId问题 3、302鉴权问题
1、springboot跨域问题解决
package net.sinorock.aj.common.config;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* @Description
* @Project aj-parent
* @PageName net.sinorock.aj.common.config
* @ClassName CorsConfig
* @author MengyuWu
* @date 2019-8-229:59
*/
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer CORSConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("*")
.allowedHeaders("*")
//设置是否允许跨域传cookie
.allowCredentials(true)
//设置缓存时间,减少重复响应
.maxAge(3600);
}
};
}
@Bean
public FilterRegistrationBean corsFilter() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
final CorsConfiguration config = new CorsConfiguration();
// 允许cookies跨域
config.setAllowCredentials(true);
// #允许向该服务器提交请求的URI,*表示全部允许,在SpringMVC中,如果设成*,会自动转成当前请求头中的Origin
config.addAllowedOrigin("*");
// #允许访问的头信息,*表示全部
config.addAllowedHeader("*");
// 预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
config.setMaxAge(3600L);
// 允许提交请求的方法,*表示全部允许
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
// 设置监听器的优先级
bean.setOrder(0);
return bean;
}
}
2、sessionId问题,因前后端分离无法使用cookie,所以修改为登陆校验后返回sessionId给前端,前端拿到sessionId后放在请求头中,后端重写 DefaultWebSessionManager 中的 getSessionId 方法,从请求头中获取对应的sessionId。
2.1 登陆返回sessionId ( token )
@ResponseBody
@PostMapping(value = "/web/login")
@ApiOperation("登录")
public R login(@RequestBody SysUserEntity user)
throws AuthenticationException
{
SecurityUtils.getSubject().logout();// 此行代码用于修改会话标识未更新的BUG,作用清空登录之前产生的session信息
MenuData menuData = new MenuData();
try
{
Subject subject = ShiroUtils.getSubject();
MyUserAuthenticationToken token = new MyUserAuthenticationToken(user.getUsername(),
user.getPassword());
subject.login(token);
if (null != subject.getSession())
{
String sessionId = (String)subject.getSession().getId();
menuData.setToken(sessionId);
}
}
catch (LockedAccountException e)
{
return R.error(e.getMessage());
}
catch (DisabledAccountException e)
{
return R.error(e.getMessage());
}
catch (UnknownAccountException e)
{
return R.error(e.getMessage());
}
catch (IncorrectCredentialsException e)
{
SysUserEntity originUser = userService.queryByUsername(user.getUsername());
userService.updateUserLoginAttempts(originUser,
configService.getValue("sysLoginErrorNum"));
return R.error("账号/密码不正确");
}
catch (Exception e)
{
return R.error("账户验证失败");
}
return R.ok().put("data", menuData);
}
2.2 后台重写 DefaultWebSessionManager 中的 getSessionId方法
package net.sinorock.aj.modules.base.core.security.shiro;
import lombok.extern.slf4j.Slf4j;
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.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
/**
* @Description
* @Project aj-parent
* @PageName net.sinorock.aj.modules.base.core.security.shiro
* @ClassName MySessionManager
* @author MengyuWu
* @date 2019-8-2616:08
*/
@Configuration
@Slf4j
public class MySessionManager extends DefaultWebSessionManager {
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
public MySessionManager() {
super();
}
@Override
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
String id = WebUtils.toHttp(request).getHeader("Authorization");
//如果请求头中有 Authorization (前端请求头中设置的名字)则其值为sessionId
if (!StringUtils.isEmpty(id)) {
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 {
//否则按默认规则从cookie取sessionId
return super.getSessionId(request, response);
}
}
}
2.3 在shiro的配置类中注入自定义的session缓存管理器
/**
* @Description: 自定义的 shiro session 缓存管理器
* 用于跨域等情况下获取请求头中的sessionId
* @method: sessionManager
*
* @author: MengyuWu
* @date: 18:38 2019-8-26
* @throws
**/
@Bean
public SessionManager sessionManager()
{
// 将我们继承后重写的shiro session 注册
MySessionManager sessionManager = new MySessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
Collection<SessionListener> sessionListeners = new ArrayList<>();
sessionListeners.add(customSessionListener());
sessionManager.setSessionListeners(sessionListeners);
// 单位为毫秒,600000毫秒为1个小时
sessionManager.setSessionValidationInterval(3600000 * 12);
// 3600000 milliseconds = 1 hour
sessionManager.setGlobalSessionTimeout(3600000 * 12);
// 是否删除无效的,默认也是开启
sessionManager.setDeleteInvalidSessions(true);
// 是否开启 检测,默认开启
sessionManager.setSessionValidationSchedulerEnabled(true);
// 创建会话Cookie
Cookie cookie = new SimpleCookie(ShiroHttpSession.DEFAULT_SESSION_ID_NAME);
cookie.setName("WEBID");
cookie.setHttpOnly(true);
sessionManager.setSessionIdCookie(cookie);
// 单位为毫秒,600000毫秒为1个小时
sessionManager.setSessionValidationInterval(3600000 * 12);
// 3600000 milliseconds = 1 hour
sessionManager.setGlobalSessionTimeout(3600000 * 12);
// 是否删除无效的,默认也是开启
sessionManager.setDeleteInvalidSessions(true);
return sessionManager;
}
至此可解决前后端跨域后的sessionId的问题。前端调用后台接口后,发现没有鉴权的请求,直接302错误,并没有对应的返回值,所以下面针对302问题进行解决。
3、302问题解决(此问题解决参考该博文:Shiro和SpringBoot集成前后端分离登陆验证和权限验证接口302获取不到返回结果的问题_咋暖还寒时候的博客-CSDN博客 ,感谢博主的详细讲解)
3.1 自定义 FormAuthenticationFilter
package net.sinorock.aj.modules.base.core.security.shiro;
import com.alibaba.fastjson.JSONObject;
import net.sinorock.aj.common.constant.ErrorCodes;
import net.sinorock.aj.common.web.def.R;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
/**
* @Description:
* @program: aj-parent
* @Auther: CShi
* @Date: 2019-8-11 14:28
**/
public class UserFormAuthenticationFilter extends FormAuthenticationFilter
{
public UserFormAuthenticationFilter()
{
super();
}
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue)
{
if (((HttpServletRequest)request).getMethod().toUpperCase().equals("OPTIONS"))
{
return true;
}
return super.isAccessAllowed(request, response, mappedValue);
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception
{
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
return executeLogin(request, response);
} else {
return true;
}
} else {
//解决 WebUtils.toHttp 往返回response写数据跨域问题
HttpServletRequest httpRequest = (HttpServletRequest) request;
String origin = httpRequest.getHeader("Origin");
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Origin", origin);
//通过对 Credentials 参数的设置,就可以保持跨域 Ajax 时的 Cookie
//设置了Allow-Credentials,Allow-Origin就不能为*,需要指明具体的url域
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 返回固定的JSON串
WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
WebUtils.toHttp(response).getWriter().print(JSONObject.toJSONString(R.error(ErrorCodes.General.AUTH_EMPTY_ERROR.getCode(),ErrorCodes.General.AUTH_EMPTY_ERROR.getMsg())));
return false;
}
}
}
3.2 自定义PermissionsAuthorizationFilter
package net.sinorock.aj.modules.base.core.security.shiro;
import com.alibaba.fastjson.JSONObject;
import net.sinorock.aj.common.constant.ErrorCodes;
import net.sinorock.aj.common.web.def.R;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.apache.shiro.web.util.WebUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
/**
* @Description
* @Project aj-parent
* @PageName net.sinorock.aj.modules.base.core.security.shiro
* @ClassName CustomPermissionsAuthorizationFilter
* @author MengyuWu
* @date 2019-8-279:47
*/
public class CustomPermissionsAuthorizationFilter extends PermissionsAuthorizationFilter {
/**
* 根据请求接口路径进行验证
* @param request
* @param response
* @param mappedValue
* @return
* @throws IOException
*/
@Override
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {
// 获取接口请求路径
String servletPath = WebUtils.toHttp(request).getServletPath();
mappedValue = new String[]{servletPath};
return super.isAccessAllowed(request, response, mappedValue);
}
/**
* 解决权限不足302问题
* @param request
* @param response
* @return
* @throws IOException
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
Subject subject = getSubject(request, response);
if (subject.getPrincipal() != null) {
return true;
} else {
//解决 WebUtils.toHttp 往返回response写数据跨域问题
HttpServletRequest httpRequest = (HttpServletRequest) request;
String origin = httpRequest.getHeader("Origin");
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Origin", origin);
//通过对 Credentials 参数的设置,就可以保持跨域 Ajax 时的 Cookie
//设置了Allow-Credentials,Allow-Origin就不能为*,需要指明具体的url域
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
WebUtils.toHttp(response).getWriter().print(JSONObject.toJSONString(R.error(ErrorCodes.General.AUTH_EMPTY_ERROR.getCode(),ErrorCodes.General.AUTH_EMPTY_ERROR.getMsg())));
}
return false;
}
}
3.3 shiro配置类中添加自定义的PermissionsAuthorizationFilter、FormAuthenticationFilter
/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
* Filter Chain定义说明
* 1、一个URL可以配置多个Filter,使用逗号分隔
* 2、当设置多个过滤器时,全部验证通过,才视为通过
* 3、部分过滤器可指定参数,如perms,roles
*/
@Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager){
if(log.isDebugEnabled()){
log.debug("ShiroConfiguration.shirFilter()");
}
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 必须设置 SecurityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
//增加自定义过滤
Map<String, Filter> filters = new HashMap<>(5);
filters.put("authc", new UserFormAuthenticationFilter());
filters.put("perms", new CustomPermissionsAuthorizationFilter());
filters.put("logout", new MyUserLogoutFilter());
shiroFilterFactoryBean.setFilters(filters);
//拦截器.
Map<String,String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
//配置退出过滤器
/**
* anon(匿名) org.apache.shiro.web.filter.authc.AnonymousFilter
* authc(身份验证) org.apache.shiro.web.filter.authc.FormAuthenticationFilter
* authcBasic(http基本验证) org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
* logout(退出) org.apache.shiro.web.filter.authc.LogoutFilter
* noSessionCreation(不创建session) org.apache.shiro.web.filter.session.NoSessionCreationFilter
* perms(许可验证) org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
* port(端口验证) org.apache.shiro.web.filter.authz.PortFilter
* rest (rest方面) org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
* roles(权限验证) org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
* ssl (ssl方面) org.apache.shiro.web.filter.authz.SslFilter
* member (用户方面) org.apache.shiro.web.filter.authc.UserFilter
* user 表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe
*/
//<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/main/web/login", "anon");
filterChainDefinitionMap.put("/main/web/logout", "logout");
// 使用该过滤器过滤所有的链接
filterChainDefinitionMap.putAll(ShiroConfigConstant.filterMap);
//配置记住我或认证通过可以访问的地址
filterChainDefinitionMap.put("/**", "authc,perms");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
至此,302问题已解决,前端可通过后台接口返回 50011 (ErrorCodes.General.AUTH_EMPTY_ERROR)进行判断是否鉴权成功,不成功的话调转登陆页面。
此文为我在改造springboot+shiro整合适用前后端分离项目中的记录,如果有错误的地方,还请及时指出。
原文链接:springboot+shiro前后端分离过程中跨域问题、sessionId问题、302鉴权失败问题本文已参与「新人创作 - 掘金 (juejin.cn)