前言
这次在处理一个小项目时用到了前后端分离,服务端使用springboot2.x。权限验证使用了Shiro。前后端分离首先需要解决的是跨域问题,POST接口跨域时会预发送一个OPTIONS请求,浏览器收到响应后会继续执行POST请求。 前后端分离后为了保持会话状态使用session持久化插件shiro-redis,持久化session可以持久化到关系型数据库,也可以持久化到非关系型数据库(主要是重写SessionDao)。Shiro已提供了SessionDao接口和抽象类。如果项目中用到Swagger的话,还需要把swagger相关url放行。
搭建依赖
<dependency>
<!--session持久化插件-->
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>
3.2
.
3
</version>
</dependency>
<dependency>
<!--spring shiro依赖-->
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>
1.4
.
1
</version>
</dependency>
Shiro权限配置
public
class
ShiroConfig {
@Value
(
"${spring.redis.shiro.host}"
)
private
String host;
@Value
(
"${spring.redis.shiro.port}"
)
private
int
port;
@Value
(
"${spring.redis.shiro.timeout}"
)
private
int
timeout;
@Value
(
"${spring.redis.shiro.password}"
)
private
String password;
/**
* 权限规则配置
**/
@Bean
public
ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean =
new
ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put(
"authc"
,
new
MyFormAuthorizationFilter());
Map<String, String> filterChainDefinitionMap =
new
LinkedHashMap<>();
//swagger资源不拦截
filterChainDefinitionMap.put(
"/swagger-ui.html"
,
"anon"
);
filterChainDefinitionMap.put(
"/swagger-resources/**/**"
,
"anon"
);
filterChainDefinitionMap.put(
"/v2/api-docs"
,
"anon"
);
filterChainDefinitionMap.put(
"/webjars/springfox-swagger-ui/**"
,
"anon"
);
filterChainDefinitionMap.put(
"/configuration/security"
,
"anon"
);
filterChainDefinitionMap.put(
"/configuration/ui"
,
"anon"
);
filterChainDefinitionMap.put(
"/login/ajaxLogin"
,
"anon"
);
filterChainDefinitionMap.put(
"/login/unauth"
,
"anon"
);
filterChainDefinitionMap.put(
"/login/logout"
,
"anon"
);
filterChainDefinitionMap.put(
"/login/register"
,
"anon"
);
filterChainDefinitionMap.put(
"/**"
,
"authc"
);
shiroFilterFactoryBean.setLoginUrl(
"/login/unauth"
);
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return
shiroFilterFactoryBean;
}
/**
* shiro安全管理器(权限验证核心配置)
**/
@Bean
public
SecurityManager securityManager() {
DefaultWebSecurityManager securityManager =
new
DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
securityManager.setSessionManager(sessionManager());
securityManager.setCacheManager(cacheManager());
return
securityManager;
}
/**
* 会话管理
**/
@Bean
public
SessionManager sessionManager() {
MySessionManager sessionManager =
new
MySessionManager();
sessionManager.setSessionIdUrlRewritingEnabled(
false
);
//取消登陆跳转URL后面的jsessionid参数
sessionManager.setSessionDAO(sessionDAO());
sessionManager.setGlobalSessionTimeout(-
1
);
//不过期
return
sessionManager;
}
/**
* 使用的是shiro-redis开源插件 缓存依赖
**/
@Bean
public
RedisManager redisManager() {
RedisManager redisManager =
new
RedisManager();
redisManager.setHost(host+
":"
+port);
redisManager.setTimeout(timeout);
redisManager.setPassword(password);
return
redisManager;
}
/**
* 使用的是shiro-redis开源插件 session持久化
**/
public
RedisSessionDAO sessionDAO() {
RedisSessionDAO redisSessionDAO =
new
RedisSessionDAO();
redisSessionDAO.setRedisManager(redisManager());
return
redisSessionDAO;
}
/**
* 缓存管理
**/
@Bean
public
CacheManager cacheManager() {
RedisCacheManager redisCacheManager =
new
RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return
redisCacheManager;
}
/**
* 权限管理
**/
@Bean
public
MyShiroRealm myShiroRealm() {
return
new
MyShiroRealm();
}
}
public
class
MyShiroRealm
extends
AuthorizingRealm {
private
Logger logger= LoggerFactory.getLogger(MyShiroRealm.
class
);
@Resource
UserDao userDao;
@Override
protected
AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
logger.info(
"===================权限验证=================="
);
return
null
;
}
@Override
protected
AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
throws
AuthenticationException {
UsernamePasswordToken token=(UsernamePasswordToken) authenticationToken;
User currentUser=userDao.findUser(token.getUsername());
if
(
null
== currentUser){
throw
new
AuthenticationException(
"账户不存在"
);
}
if
(!currentUser.getPassword().equals(
new
String(token.getPassword()))){
throw
new
IncorrectCredentialsException(
"账户密码不正确"
);
}
if
(currentUser.getIsdel()==
1
){
throw
new
LockedAccountException(
"账户已冻结"
);
}
Subject subject = SecurityUtils.getSubject();
BIUser biUser=
new
BIUser();
biUser.setUserId(currentUser.getUserId());
biUser.setOrgId(currentUser.getOrgid());
biUser.setUserName(currentUser.getUsername());
biUser.setPassword(currentUser.getPassword());
biUser.setSessionId(subject.getSession().getId().toString());
biUser.setIsdel(currentUser.getIsdel());
biUser.setCreateTime(currentUser.getCreatetime());
logger.info(
"======已授权"
+biUser.toString()+
"===="
);
return
new
SimpleAuthenticationInfo(biUser,biUser.getPassword(),biUser.getUserName());
}
}
3、MySessionManager。shiro权限验证是根据客户端Cookie中的JSESSIONID值来确定身份是否合格。前后端分离后这个地方需要处理。客户端调用服务端登陆接口,验证通过后返回给客户端一个token值(这里我放的是sessionid)。客户端保存token值,然后调用其他接口时把token值放在header中。对前端来说也就是放在ajax的headers参数中。
public
class
MySessionManager
extends
DefaultWebSessionManager {
private
static
final
String AUTHORIZATION =
"Authorization"
;
private
static
final
String REFERENCED_SESSION_ID_SOURCE =
"Stateless request"
;
public
MySessionManager() {
}
@Override
protected
Serializable getSessionId(ServletRequest request, ServletResponse response) {
//从前端ajax headers中获取这个参数用来判断授权
String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
if
(StringUtils.hasLength(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中取值
return
super
.getSessionId(request, response);
}
}
}
4、MyFormAuthorizationFilter。对于跨域的POST请求,浏览器发起POST请求前都会发送一个OPTIONS请求已确定服务器是否可用,OPTIONS请求通过后继续执行POST请求,而shiro自带的权限验证是无法处理OPTIONS请求的,所以这里需要重写isAccessAllowed方法。
public
class
MyFormAuthorizationFilter
extends
FormAuthenticationFilter {
protected
boolean
isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) {
HttpServletRequest httpServletRequest = WebUtils.toHttp(servletRequest);
if
(
"OPTIONS"
.equals(httpServletRequest.getMethod())) {
return
true
;
}
return
super
.isAccessAllowed(servletRequest, servletResponse, o);
}
}
5、处理跨域
@Override
public
void
addCorsMappings(CorsRegistry registry) {
registry.addMapping(
"/**"
)
.allowedOrigins(
"*"
)
.allowedMethods(
"PUT"
,
"DELETE"
,
"GET"
,
"POST"
)
.allowedHeaders(
"*"
)
.exposedHeaders(
"access-control-allow-headers"
,
"access-control-allow-methods"
,
"access-control-allow"
+
"-origin"
,
"access-control-max-age"
,
"X-Frame-Options"
,
"Authorization"
)
.allowCredentials(
false
).maxAge(
3600
);
}