springboot整合shiro
因前后多个项目中使用的到springboot和shiro,多方查阅书籍和博客,在此做一个小小整理,以备后需。好记性不如烂笔头。
一、shiro介绍
略
二、springboot介绍
略
三、整合
1、添加依赖
在pom文件中新增shiro相关的依赖。缓存这里暂且采用ehcache。如果是用redis需要做小部分修改。
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <!-- shiro ehcache --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.3.2</version> </dependency> <!-- shiro标签支持 --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency> <!-- ehchache --> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency>
2、shiro配置
除了以上,在shiroFilter中还需要配置at.pollux.thymeleaf.shiro.dialect.ShiroDialect,为了在thymeleaf里使用shiro的标签的bean
@Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); }
3、ehcache的配置
1、首先得有一个ehcache的xml文件,ehcache.xml(内容略)放到/src/main/resource下,也就是类路径下。
2、需要在applicantion.yml文件中声明
spring: cache: ehcache: config: classpath:config/ehcache.xml
3、启动类上需要加上@EnableCaching注解
项目启动时spring会自动构造net.sf.ehcache.CacheManager对象,在类属性中可以通过@Autowired注解得到该对象。
然后将cacheManager注入到ehCacheManager中。
@Autowired private CacheManager cacheManager; @Bean public EhCacheManager ehCacheManager() { EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManager(cacheManager); return ehCacheManager; }
四、前后端分离导致的问题
1、前后端分离,后端只负责返json数据。
后端处理:
需要自定义一个配置类MyWebMvcConfig,实现
org.springframework.web.servlet.config.annotation.WebMvcConfigurer接口。
MyWebMvcConfig里面可以添加很多mvc相关的配置。
@Configuration public class MyWebMvcConfig implements WebMvcConfigurer { /** * 以前写SpringMVC的时候,如果需要访问一个页面,必须要写Controller类,然后再写一个 * 方法跳转到页面,感觉好麻烦,其实重写WebMvcConfigurerAdapter中的addViewControllers方法即可 * 达到效果了 */ @Override public void addViewControllers(ViewControllerRegistry registry) { WebMvcConfigurer.super.addViewControllers(registry); } /** * 添加类型转换器和格式化器 * * @param registry */ @Override public void addFormatters(FormatterRegistry registry) { } /** * 跨域支持(主要看这个) * * @param registry */ @Override public void addCorsMappings(CorsRegistry registry) { List<String> allowedOrigins = new ArrayList<>(); allowedOrigins.add("http://localhost:8983");//前端访问地址 String[] objects = allowedOrigins.toArray(new String[allowedOrigins.size()]); registry.addMapping("/**") .allowCredentials(true) //允许Cookie跨域,在做登录校验的时候有用 .allowedMethods("GET", "POST", "DELETE", "PUT")//允许提交请求的方法,*表示全部允许 .maxAge(3600 * 24)//预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了 .allowedOrigins(objects)//允许向该服务器提交请求的URI,*表示全部允许 .allowedHeaders("*"); //允许的头信息,*标识全部允许 } /** * 添加静态资源--过滤 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { } /** * 配置消息转换器 * * @param converters */ @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { } /** * 配置拦截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // TestInterceptor extends HandlerInterceptorAdapter // registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**"); } }
2、或者另外一种方式:
自定义一个过滤器,添加相关跨域处理
@Component public class CorsFilter implements Filter { final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(CorsFilter.class); public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; HttpServletRequest reqs = (HttpServletRequest) req; response.setHeader("Access-Control-Allow-Origin", reqs.getHeader("Origin")); response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "x-requested-with,APPID,token"); response.setHeader("Access-Control-Expose-Headers", "APPID");//允许前端跨域获取的头 chain.doFilter(req, res); } public void init(FilterConfig filterConfig) { } public void destroy() { } }
2、前端
function loginOn(){ var data_={ username:"admin", password:"admin" } $.ajax({ type: "POST", url: "http://192.168.2.240:8091/login", data:data_, xhrFields: { withCredentials: true // 携带跨域cookie }, /*processData: false,*/ //测试时,这个如果带上,返回头Response里面会拿不到Cookie,Cookie里面存放了sessionid success: function(data) { console.log(data); } }); }
3、好的!现在登录正常了。
但是在shiro的配置里面,我们有关于登录页的配置
@Bean ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); shiroFilterFactoryBean.setLoginUrl("/login"); ..... }
shiro发现用户没有登录时,会重定向到/login页面。但是,因为前后端是分离的。所以得给前端返一个未登录
的json消息。
跨域重定向不好处理,貌似jsonp可以解决。这个重定向,返回的header里面有一个
Location... 后面跟的是重定向的地址,浏览器是不会去处理的。
所以。需要重写一些东西。我只重写了量个过滤器:
FormAuthenticationFilter和LogoutFilter
并将ShiroFilterFactoryBean里面注册的filter替换成自己的。
MyFormAuthenticationFilter
@Configuration public class MyFormAuthenticationFilter extends FormAuthenticationFilter { private static final Logger log = LoggerFactory.getLogger(MyFormAuthenticationFilter.class); @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { if (isLoginRequest(request, response)) { if (isLoginSubmission(request, response)) { if (log.isTraceEnabled()) { log.trace("Login submission detected. Attempting to execute login."); } return executeLogin(request, response); } else { if (log.isTraceEnabled()) { log.trace("Login page view."); } // allow them to see the login page ;) return true; } } else { if (log.isTraceEnabled()) { log.trace("Attempting to access a path which requires authentication. Forwarding to the " + "Authentication url [" + getLoginUrl() + "]"); } // 认证未通过json返回 HttpServletResponse res = (HttpServletResponse) response; HttpServletRequest req = (HttpServletRequest) request; // 登出json返回 res.setCharacterEncoding("UTF-8"); res.setHeader("Content-type", "application/json;charset=UTF-8"); res.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin")); res.setHeader("Access-Control-Allow-Credentials", "true"); res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); ResponseMsg httpResponse = ResponseMsg.create(Code.NotLogin); res.getWriter().write(JSONObject.toJSONString(httpResponse)); return false; } } }
之前是想在方法里抛出一个自定义的异常类BizException extends RuntimeException。但是发现自定义的
MyExceptionHandler implements HandlerExceptionResolver没法拦截处理到我的异常。所以直接在方法
里,对response做了处理。
同理MyLogoutFilter
public class MyLogoutFilter extends LogoutFilter { @Override protected void issueRedirect(ServletRequest request, ServletResponse response, String redirectUrl) throws Exception { HttpServletResponse res = (HttpServletResponse) response; HttpServletRequest req = (HttpServletRequest) request; // 登出json返回 res.setCharacterEncoding("UTF-8"); res.setHeader("Content-type", "application/json;charset=UTF-8"); res.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin")); res.setHeader("Access-Control-Allow-Credentials", "true"); res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE"); ResponseMsg httpResponse = ResponseMsg.create(Code.OK); res.getWriter().write(JSONObject.toJSONString(httpResponse)); } }
最后替换:
@Bean ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();//获取filters filters.put("authc", new MyFormAuthenticationFilter());//将自定义 的MyFormAuthenticationFilter注入shiroFilter中 filters.put("logout", new MyLogoutFilter());//将自定义 的MyLogoutFilter注入shiroFilter中 shiroFilterFactoryBean.setSecurityManager(securityManager); ..... }
4、完成。