Shiro+JWT 前后端分离方案
理论的东西就不说了,网上一大堆教程。
因为我本身web应用做得比较多,所以本篇文章主要是结合SpringBoot来讲解。当然shiro也是支持standlon模式使用的(可以参考我的另一篇文章 自定义Realm)
使用SpringBoot,最优的依赖方案是shiro-spring-boot-starter
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.4.0</version>
</dependency>
SpringBoot集成Shiro的思路路径是 创建配置文件-->创建过滤器-->创建Realm
。(其实也不完全正确,这几个步骤得结合起来考虑)
首先,我们来创建配置文件
ShiroConfig
SecurityManager
是Shiro的核心组件,所以得首先创建它。
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm);
注意:shiroRealm是我们通过Spring Autowired自动注入的,这里的shiroRealm也就是我们自定义的realm,后面会讲到–ShiroRealm。
@Autowired
private ShiroRealm shiroRealm;
因为我们是前后端分离项目,所以shiro的Session功能我们是用不到的,得把它关闭。下面代码的作用就是关闭subject session。
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO(); DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator(); defaultSessionStorageEvaluator.setSessionStorageEnabled(false); subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator); securityManager.setSubjectDAO(subjectDAO);
官方文档中有这么一段话介绍 securityManager.subjectDAO.sessionStorageEvaluator.sessionStorageEnabled = false
This will prevent Shiro from using a Subject’s session to store that Subject’s state across requests/invocations/messages for all Subjects. 这将阻止Shiro使用Subject的会话存储Subject的请求/调用/消息中的状态。 Just be sure that you authenticate on every request so Shiro will know who the Subject is for any given request/invocation/message. 一定要保证您对每个请求进行身份验证,以便于Shiro知道任何给定请求/调用/消息的subject是谁。
所以,前后端分离项目的关键是**每个请求都得交给shiro去验证身份,也就是说每个请求都会调用subject的login方法
**
继而每次请求都会调用自定义realm中的doGetAuthenticationInfo
方法。
创建SecurityManager
后,我们来创建过滤器Filter.
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.html", "anon");
filterChainDefinitionMap.put("/**/*.svg", "anon");
filterChainDefinitionMap.put("/**/*.pdf", "anon");
filterChainDefinitionMap.put("/**/*.jpg", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.ico", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
- anon–表示不进行拦截,是anonymous的缩写
JwtFilter
是我们自定义的Filter,稍候我们会创建。
Shiro可以通过@RequiresPermissions(value = {"sys:admin:get"})
、@RequiresRoles
细粒度的控制资源访问权限,在Spring应用中,要想使用注解的方式控制权限。还得配置以下几个java bean。
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 下面的代码是添加注解支持
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/*defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");*/
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
ShiroConfig完整配置文件如下:
@Slf4j
@Configuration
public class ShiroConfig {
@Autowired
private ShiroRealm shiroRealm;
/**
*
* @return
*/
@Bean("securityManager")
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(shiroRealm);
/*
* 关闭shiro自带的session,详情见文档
* http://shiro.apache.org/session-management.html#SessionManagement-
* StatelessApplications%28Sessionless%29
*/
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/doc.html", "anon");
filterChainDefinitionMap.put("/**/*.js", "anon");
filterChainDefinitionMap.put("/**/*.css", "anon");
filterChainDefinitionMap.put("/**/*.html", "anon");
filterChainDefinitionMap.put("/**/*.svg", "anon");
filterChainDefinitionMap.put("/**/*.pdf", "anon");
filterChainDefinitionMap.put("/**/*.jpg", "anon");
filterChainDefinitionMap.put("/**/*.png", "anon");
filterChainDefinitionMap.put("/**/*.ico", "anon");
// 添加自己的过滤器并且取名为jwt
Map<String, Filter> filterMap = new HashMap<String, Filter>(1);
filterMap.put("jwt", new JwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边
filterChainDefinitionMap.put("/**", "jwt");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/**
* 下面的代码是添加注解支持
* @return
*/
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
/*defaultAdvisorAutoProxyCreator.setUsePrefix(true);
defaultAdvisorAutoProxyCreator.setAdvisorBeanNamePrefix("_no_advisor");*/
return defaultAdvisorAutoProxyCreator;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
JwtFilter
在配置ShiroConfig时,我们提到需要配置自定义过滤器。过滤器的作用就是拦截指定的请求,比如我们在上面设置的是filterChainDefinitionMap.put("/**", "jwt");
拦截所有请求(这里的所有指的是流入到当前过滤器的所有请求,如果是图片、字体等请求,则不会进入,因为在此之前已经被拦截了)
我们来看具体的实现吧:JwtFilter
继承了BasicHttpAuthenticationFilter
类,我们只要重写几个关键的方法即可。
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效,请重新登录", e);
}
}
/**
*
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("X-Access-Token");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
// 普通应用中获取subject方式 subject = SecurityUtils.getSubject();
// 然后登录 subject.login(token)
//login方法最后会回调到ShiroRealm.doGetAuthenticationInfo
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
- isAccessAllowed:判断是否允许访问,内部调用executeLogin执行登录,executeLogin没有抛出异常,表示允许登录(相当于每次请求都会进行登录逻辑判断)
- executeLogin: 从请求头中拿出token,调用subject的login方法就行登录验证(这里封装了JwtToken,[稍候讲解](### JwtToken))
- preHandle: 跨越支持
JwtToken
JwtToken的实现很简单,但要注意两点:
- 必须实现AuthenticationToken接口
- 返回的Principal和Credentials是一样的,都是成员变量token
下面讲解自定义Realm时会提到这两点。
import org.apache