直接进入主题,项目是使用springboot,框架用的shiro作权限,mybatis作orm框架,项目须要作先后分离,这样就会致使一个问题,shiro是根据sessionID来识别是否是同一个request,但若是先后分离的话,就会出现跨域的问题,session极可能就会发生变化,这样就须要用一个标记来代表是同一个请求。初步的方案就是用token来代替session,但本质上说,如今的这种方式,仍是用的session的那一套,不过是对中间进行了处理,下面上代码:前端
咱们要先解决的是跨域的问题:java
springboot解决跨域很好解决,以下便可。nginx
package com.common.config.cors; 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; /** * @author :LX * 建立时间: 2019/5/30. 14:03 * 地点:广州 * 目的: 跨域访问控制 * 作先后分离的话,这个也是必配的 * 备注说明: */ @Configuration public class CorsConfig { private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); // 容许任何域名使用 corsConfiguration.addAllowedOrigin("*"); // 容许任何头 corsConfiguration.addAllowedHeader("*"); // 容许任何方法(post、get等) corsConfiguration.addAllowedMethod("*"); return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 对接口配置跨域设置 source.registerCorsConfiguration("/**", buildConfig()); return new CorsFilter(source); } }
而后自定义 realm ,简单点说,就是你实现查询用户角色和权限的类。这一步就省略了,不外乎查询数据库,查询当前用户的角色和权限。web
接着自定义token,简单的说,这里其实就是让前端请求的时候在请求头中带一个特定的标识,而后根据这个标识找到vlues,匹配上咱们的sessionId。redis
package com.common.config.shiro; 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.util.StringUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; /** * @author :LX * 建立时间: 2019/5/30. 18:08 * 地点:广州 * 目的: shiro 的 session 管理 * 自定义session规则,实现先后分离,在跨域等状况下使用token 方式进行登陆验证才须要,不然没必须使用本类。 * shiro默认使用 ServletContainerSessionManager 来作 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,调用 storeSessionId 方法保存sesionId 到 cookie中 * 为了支持无状态会话,咱们就须要继承 DefaultWebSessionManager * 自定义生成sessionId 则要实现 SessionIdGenerator * 备注说明: */ public class ShiroSession extends DefaultWebSessionManager { /** * 定义的请求头中使用的标记key,用来传递 token */ private static final String AUTH_TOKEN = "authToken"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public ShiroSession() { super(); //设置 shiro session 失效时间,默认为30分钟,这里如今设置为15分钟 //setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15); } /** * 获取sessionId,本来是根据sessionKey来获取一个sessionId * 重写的部分多了一个把获取到的token设置到request的部分。这是由于app调用登录接口的时候,是没有token的,登录成功后,产生了token,咱们把它放到request中,返回结 * 果给客户端的时候,把它从request中取出来,而且传递给客户端,客户端每次带着这个token过来,就至关因而浏览器的cookie的做用,也就能维护会话了 * @param request * @param response * @return */ @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { //获取请求头中的 AUTH_TOKEN 的值,若是请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是经过sessionId 来控制的 String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN); if (StringUtils.isEmpty(sessionId)){ //若是没有携带id参数则按照父类的方式在cookie进行获取sessionId return super.getSessionId(request, response); } else { //请求头中若是有 authToken, 则其值为sessionId request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); //sessionId request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; } } }
你们看上面的代码,其实就是咱们的请求头中获取 authToken 的值,而后塞入到sessionId中,代替了session.spring
若是你们还须要自定义这个token,或者说自定义生成的seesionId,就须要看下面的这个方法。数据库
根据个人研究,最终找到 JavaUuidSessionIdGenerator 这个类,而后能够找到apache
public Serializable generateId(Session session) { return UUID.randomUUID().toString(); }
上面的代码,其实就是生成了一串UUID,咱们能够实现SessionIdGenerator接口来完成自定义的sessionID生成json
public class UuidSessionIdGenerator implements SessionIdGenerator{ @Override public Serializable generateId(Session session) { Serializable uuid = new JavaUuidSessionIdGenerator().generateId(session); GGLogger.info("生成的sessionid是:"+uuid); return uuid; } }
###自定义生成sessionid sessionIdGenerator=ggauth.shiro.user.common.UuidSessionIdGenerator securityManager.sessionManager.sessionDAO.sessionIdGenerator=$sessionIdGenerator
上面自定义生成的代码是 参考的 https://blog.csdn.net/yaomingyang/article/details/78142763 的代码,未通过验证,但应该是没问题的。我这里是没有去修改生成UUID的逻辑。后端
其实配置了这2个东西以后,就能够弄shiro最终的配置了。
package com.common.config.shiro; import com.yunji.kwxt.common.Constant; import com.yunji.kwxt.common.filter.CORSAuthenticationFilter; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import java.util.LinkedHashMap; import java.util.Map; /** * @author :LX * 建立时间: 2019/5/27. 11:39 * 地点:广州 * 目的: shiro配置 * 备注说明: */ @Configuration public class ShiroConfig { private static Logger log = LoggerFactory.getLogger(ShiroConfig.class); /** * 对shiro的拦截器进行注入 * <p> * securityManager: * 全部Subject 实例都必须绑定到一个SecurityManager上,SecurityManager 是 Shiro的核心,初始化时协调各个模块运行。然而,一旦 SecurityManager协调完毕, * SecurityManager 会被单独留下,且咱们只须要去操做Subject便可,无需操做SecurityManager 。 可是咱们得知道,当咱们正与一个 Subject 进行交互时,实质上是 * SecurityManager在处理 Subject 安全操做 * * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(org.apache.shiro.mgt.SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean(); shiroFilter.setSecurityManager(securityManager); //设置遇到未登陆、未受权等状况时候,请求这些地址,返回相应的错误 shiroFilter.setLoginUrl("/user/shiroError?errorId=" + Constant.NEED_LOGIN); shiroFilter.setUnauthorizedUrl("/user/shiroError?errorId=" + Constant.NO_UNAUTHORIZED); //拦截器,配置访问权限 必须是LinkedHashMap,由于它必须保证有序。滤链定义,从上向下顺序执行,通常将 /**放在最为下边 Map<String, String> filterMap = new LinkedHashMap<String, String>(); // 配置不会被拦截的连接 顺序判断 filterMap.put("/user/login", "anon"); filterMap.put("/user/shiroError", "anon"); filterMap.put("/user/reg", "anon"); //剩余的请求shiro都拦截 filterMap.put("/**/*", "authc"); shiroFilter.setFilterChainDefinitionMap(filterMap); //自定义拦截器 Map<String, Filter> customFilterMap = new LinkedHashMap<>(); customFilterMap.put("corsAuthenticationFilter", new CORSAuthenticationFilter()); shiroFilter.setFilters(customFilterMap); return shiroFilter; } /** * securityManager 核心配置 * 安全控制层 * @return */ @Bean public org.apache.shiro.mgt.SecurityManager securityManager(){ DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); //设置自定义的realm defaultWebSecurityManager.setRealm(myRealm()); //自定义的shiro session 缓存管理器 defaultWebSecurityManager.setSessionManager(sessionManager()); //将缓存对象注入到SecurityManager中 defaultWebSecurityManager.setCacheManager(ehCacheManager()); return defaultWebSecurityManager; } /** * 自定义的realm * @return */ @Bean public MyRealm myRealm() { return new MyRealm(); } /** * 开启shiro 的AOP注解支持 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(org.apache.shiro.mgt.SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * shiro缓存管理器 * 1 添加相关的maven支持 * 2 注册这个bean,将缓存的配置文件导入 * 3 在securityManager 中注册缓存管理器,以后就不会每次都会去查询数据库了,相关的权限和角色会保存在缓存中,但须要注意一点,更新了权限等操做以后,须要及时的清理缓存 */ @Bean public EhCacheManager ehCacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:config/ehcache.xml"); return cacheManager; } /** * 自定义的 shiro session 缓存管理器,用于跨域等状况下使用 token 进行验证,不依赖于sessionId * @return */ @Bean public SessionManager sessionManager(){ //将咱们继承后重写的shiro session 注册 ShiroSession shiroSession = new ShiroSession(); //若是后续考虑多tomcat部署应用,可使用shiro-redis开源插件来作session 的控制,或者nginx 的负载均衡 shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO()); return shiroSession; } }
完成了上面的流程,基本就已经大功告成了,对了,还要加上下面的代码。
package com.common.filter; import com.alibaba.fastjson.JSONObject; import com.yunji.kwxt.common.Constant; import com.yunji.kwxt.common.enums.ResultEnum; import com.yunji.kwxt.common.model.ResultJson; import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; /** * @author :LX * 建立时间: 2019/5/31. 10:25 * 地点:广州 * 目的: 过滤OPTIONS请求 * 继承shiro 的form表单过滤器,对 OPTIONS 请求进行过滤。 * 先后端分离项目中,因为跨域,会致使复杂请求,即会发送preflighted request,这样会致使在GET/POST等请求以前会先发一个OPTIONS请求,但OPTIONS请求并不带shiro * 的'authToken'字段(shiro的SessionId),即OPTIONS请求不能经过shiro验证,会返回未认证的信息。 * * 备注说明: 须要在 shiroConfig 进行注册 */ public class CORSAuthenticationFilter extends FormAuthenticationFilter { /** * 直接过滤能够访问的请求类型 */ private static final String REQUET_TYPE = "OPTIONS"; public CORSAuthenticationFilter() { super(); } @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (((HttpServletRequest) request).getMethod().toUpperCase().equals(REQUET_TYPE)) { return true; } return super.isAccessAllowed(request, response, mappedValue); } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletResponse res = (HttpServletResponse)response; res.setHeader("Access-Control-Allow-Origin", "*"); res.setStatus(HttpServletResponse.SC_OK); res.setCharacterEncoding("UTF-8"); PrintWriter writer = res.getWriter(); ResultJson resultJson = new ResultJson(Constant.ERROR_CODE_NO_LOGIN, ResultEnum.ERROR.getStatus(), "请先登陆系统!", null); writer.write(JSONObject.toJSONString(resultJson)); writer.close(); return false; } }
为何要过滤,上面的注释说的很清楚了,建议你们仍是加上,这个类最终在shiro的拦截器那里配置了。
固然还有登陆那里要说一下,不少的新手否则就搞不懂了。
/** * 用户登陆 * @param username 用户名 * @param password 用户密码 * @return */ @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public ResultJson login(String username, String password, HttpServletRequest request){ //TODO 验证码验证 UsernamePasswordToken token = new UsernamePasswordToken(username, password); User user = userService.login(username, password); SecurityUtils.getSubject().login(token); //更新登陆信息 user.setIp(HttpTool.getIpAddr(request)); user.setOs(HttpTool.getOs(request)); user.setUpdateUserId(user.getId()); user.setUpdateTime(CommonTool.getTimestamp()); //设置session时间 //SecurityUtils.getSubject().getSession().setTimeout(1000*60*30); //token信息 Subject subject = SecurityUtils.getSubject(); Serializable tokenId = subject.getSession().getId(); return new ResultJson(null, ResultEnum.SUCCESS.getStatus(), "登陆认证成功", tokenId); }
咱们最终从shiro的session中取到了sessionId,回传给前端,前端后续的请求都要带这个token。
这样就实现了token方式的shiro整合springboot。
若是为了安全,还能够建议你们,获取到sessionId 以后,咱们进行一次加密,而后返回给前端,前端返回给咱们的时候,咱们能够在shirosession 类中对加密的sessionId解密,这样就更安全了。
最后还有一个问题须要说明一下,上述的代码中shiro使用了缓存,但个人缓存相关的配置却没有贴出来,由于我这里用的是java的缓存框架,建议使用redis的缓存框架,若是使用了缓存框架,细心的小伙伴就会发现,若是登陆后,在必定时间没有和后台进行交互,这个sessionId就会失效。
这是由于,当咱们登陆后若是走了缓存,session的存活时间就被缓存管理起来,咱们即便设置了shiro的缓存时间,设置应用的缓存时间都没法管理到第三方的缓存,shiro的sesssion和server的session不是同一个东西。他并非servlet来管理的,故而设置了也没有做用,须要去设置缓存中这个对象存活时间才有用,好比咱们弄了redis来管理sessionId,只有设置了在redis中session的存活时间才行,咱们直接设置
SecurityUtils.getSubject().getSession().setTimeout(1000*60*30);
#session过时时间,单位秒 server.servlet.session.timeout=30000
都没有任何用。好比我上面用ehcache来管理缓存,那只有在该缓存框架中设置这个参数才有用
我这里设置了120S,那若是120S没有任何交互,那这个缓存sessionId就会失效