前言
這次在處理一個小項目時用到了前后端分離,服務端使用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權限配置
1、ShiroConfig。這里主要是shiro核心配置。比如SecurityManager、SessionManager、CacheManager。
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();
}
}
2、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);
}
