近期在写前后端分离的项目,由于前后端分离导致原来使用的shiro配置无法满足现有系统要求。同时在前后端分离项目中存在的一些问题。例如,一些用户信息需要存储在后端方便进行安全性判断,但这些存储在后端的session前端却获取不到(特别奇怪),以及浏览器访问后端接口出现的跨域问题。
由于前后端分离导致前端和后端分别占用不同的端口,所以浏览器在访问不同接口的时候就会存在跨域问题。
我是在springboot后端项目中添加CorsConfig配置类,用于解决跨域问题,当然前端也可以解决这个跨域问题。
import org.springframework.boot.web.servlet.FilterRegistrationBean;
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;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@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;
}
}
这样就能解决前后端跨域问题。
起因是我在做登录的时候,用户信息被我用session存储在了后端,但是我在调用其他接口的时候,我却无法在后端获取到我保存在后端的用户信息,无法判断前端是否登录过。
这个问题一开始困扰了我很久,搜了半天发现了一个不错的解释:这是因为服务器判断前端的请求是同一个 session 的依据是通过网页默认的一个sessionid的cookie判断的,如果存在跨域,cookie值传不过来,也就当下一个请求过来时服务端无法识别为同一个会话,会被当做一个新的会话处理,故找不到session原保存的值。
所以我就去测试两个不同请求的sessionId是否是一样的,发现果然不一样,这也是为什么我在后端无法获取到我保存在后端的session。
其实解决办法有很多种,例如说不用session存储数据而是利用Redis,这当然是最轻松的方式。但是本着我的session是利用shiro自带的Util存储的,所以我的解决方式是将sessionId回传给前端,然后前端保存后每次访问后端接口的时候携带保存的sessionId,这样就能我就能获取到原来的session了。
1、在登录接口中将sessionId回传给前端
public ResponseResult doLogin(String username, String password) {
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
Subject subject = SecurityUtils.getSubject();
try{
subject.login(token);
UserInfo userInfo = baseMapper.selectById(username);
//利用shiro的Util获取sessionId
String sessionId = (String) subject.getSession().getId();
//存储用户信息
ShiroUtils.setSessionAttribute(SessionConst.USER_INFO_SESSION,userInfo);
System.out.println((String)ShiroUtils.getSession().getId());
userInfo.setSalt("");
userInfo.setPassword("");
Map<String,Object> data = new HashMap<>();
//保存key-value类型数据AUTHORIZATION,值为sessionId
data.put("AUTHORIZATION", sessionId);
data.put("userInfo",userInfo);
//回传给前端
return ResponseResult.success(data);
}catch(UnknownAccountException e){
return ResponseResult.error(ResponseResultEnum.SESSION_ERROR);
}catch (IncorrectCredentialsException e){
return ResponseResult.error(ResponseResultEnum.PASSWORD_ERROR);
}
}
前端拿到sesionId后进行保存,并每次调用接口的时候携带Id即可
//举例:登录method
login() {
this.$axios.get("/login", {
params: {
username: this.form1Data.username,
password: this.form1Data.password
}
}).then((res) => {
if (res.data.code === 200) {
console.log(res)
var userInfo = res.data.data.userInfo
var auth = res.data.data.AUTHORIZATION
ElementUI.Message.success("登录成功");
this.$store.commit("SET_USERINFO", userInfo);
//将信息保存在auth中
this.$store.commit("SET_AUTH", auth)
let url_name = this.$route.params.redirect
console.log(url_name)
if (url_name !== undefined && url_name !== null && url_name != '') {
this.$router.replace({
name: url_name
})
} else
this.$router.push('/')
} else { // 有问题
ElementUI.Message.error(res.data.message);
}
}
)
}
//在main.js文件中进行前置拦截
axios.interceptors.request.use(config => {
let auth = store.getters.get_auth
//从auth中获取后端传给前端的sessionId,以后调用任何接口就可以携带sessionId了
if (auth != null) {
config.headers['AUTHORIZATION'] = auth;
}
console.log(config.headers)
return config;
}, error => {
return Promise.reject(error)
});
2、在配置类中写一个自定义的sessionManager
并重写getSessionId
方法,便于对前端传递过来的sessionId进行判断,检测是否是原来的sessionId。
@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);
}
}
}
3、在shiro配置类中注入自定义的session缓存管理器
@Bean("sessionManager")
public SessionManager sessionManager(){
MySessionManager sessionManager = new MySessionManager();
sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
sessionManager.setGlobalSessionTimeout(1000 * 60 * 30);
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
关键: 因为getSessionId()
方法是通过DefaultWebSecurityManager类
进行实现的,所以我们需要将SessionManager
注入到安全管理中
/**
* 安全管理
*
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//注入
securityManager.setSessionManager(sessionManager());
securityManager.setRealm(getShiroRealm());
return securityManager;
}
至此可解决前后端跨域后的sessionId的问题。