前言
這次在處理一個小專案時用到了前後端分離,服務端使用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); }