前言
上一篇博客采用了搭建Demo的方式说明了如何使用Shiro共享session实现分布式架构。而本篇博客将介绍Shiro与JWT结合,实现前后端分离。本Demo仍然力求简洁清晰,因此在工程代码中有与上一篇博客代码重合部分将被省略,如有不清楚的地方请先看第一篇关于Shiro基础博客然后再回来继续阅读。
业务设计
JWT : JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。与Shiro结合主要关注的是无状态服务,要想更好的理解什么是无状态服务就必须先理解什么是有状态服务,有状态服务服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
其缺点是:
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
而无状态服务,在服务端不保存任何客户端请求者信息,也就是在服务端去掉了session,客户端的每次请求必须具备自描述信息,也就是携带Token通过它的信息识别客户端身份。
其优点是:
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务;
- 服务端的集群和状态对客户端透明;
- 服务端可以任意的迁移和伸缩;
- 减小服务端存储压力
更新工程
在原工程基础上增加JWT支持,只需要将JWT集成进工程然后增加JWT访问过滤器并修改Shiro配置类即可,通过这样简单三步即可完成。
(1)集成JWT
pom.xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
(2)增加访问过滤器
JwtFilter
package microapps.com.cn.shirodemo.shiro;
import microapps.com.cn.shirodemo.db.UserService;
import microapps.com.cn.shirodemo.domain.User;
import microapps.com.cn.shirodemo.common.JwtUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
public class JwtFilter extends BasicHttpAuthenticationFilter {
@Autowired
private UserService UserService;
/**
* 前置处理(处理跨域请求)
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
/**
* 后置处理(处理跨域请求)
*/
@Override
protected void postHandle(ServletRequest request, ServletResponse response) {
// 添加跨域支持
this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response));
}
/**
* 对所有请求进行过滤(除登陆)
* 1.登陆时不走过滤器;
* 2.访问权限时调用过滤器,然后再走realm权限验证方法;
* @param request
* @param response
* @param mappedValue
* @return
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
System.out.println("----------->isAccessAllowed");
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// s1. 判断header中是否存在jwtToken
if (this.isHeaderToken(request)){
request.setAttribute("errorMsg","无效请求,没有携带token");
return false;
}
String jwtToken = httpServletRequest.getHeader(JwtUtils.AUTH_HEADER);
// s2. 判断token是否过期
if(JwtUtils.isTokenExpired(jwtToken)) {
request.setAttribute("errorMsg","无效请求,携带过期token");
return false;
}
// s3. 判断Token是否有效
if(!JwtUtils.verify(jwtToken,JwtUtils.SECRET)){
request.setAttribute("errorMsg","无效请求,携带无效token");
return false;
}
try {
// 检测Header里的 JWT token内容是否正确,尝试使用 token进行登录
return executeLogin(request, response);
} catch (Exception e) {
System.out.println("jwt 登陆错误");
}
return true;
}
/**
* 执行登陆
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
UsernamePasswordToken token = createToken(request,response);
try {
Subject subject = getSubject(request, response);
subject.login(token); // 交给 Shiro 去进行登录验证
return onLoginSuccess(token, subject, request, response);
} catch (AuthenticationException e) {
return onLoginFailure(token, e, request, response);
}
}
/**
* isAccessAllowed()方法返回false,会进入该方法,表示拒绝访问
*/
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse);
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.setStatus(HttpStatus.OK.value());
Object errorMsg = servletRequest.getAttribute("errorMsg");
PrintWriter writer = httpResponse.getWriter();
writer.print("{\"code\": 401, \"msg\": \""+errorMsg.toString()+"\"}");
fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse);
return false;
}
/**
* 检测Header中是否包含 JWT token 字段
*/
private boolean isHeaderToken(ServletRequest request) {
return ((HttpServletRequest) request).getHeader(JwtUtils.AUTH_HEADER) == null;
}
/**
* 从 Header 里提取 JWT token
*/
@Override
protected UsernamePasswordToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authorization = httpServletRequest.getHeader(JwtUtils.AUTH_HEADER);
String username = JwtUtils.getUsername(authorization);
User user = UserService.getUserByName(username); // 获取用户密码
UsernamePasswordToken token = new UsernamePasswordToken(username,user.getPassword());
return token;
}
/**
* 添加跨域支持
*/
protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD");
httpServletResponse.setHeader("Access-Control-Allow-Headers",
httpServletRequest.getHeader("Access-Control-Request-Headers"));
}
}
(3)修改Shiro配置类
ShiroConfig
package microapps.com.cn.shirodemo.config;
import microapps.com.cn.shirodemo.shiro.JwtFilter;
import microapps.com.cn.shirodemo.shiro.LoginRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Description: Shiro配置类
* @Author: liuhe
* @Date: 2020/10/10
*/
@Configuration
public class ShiroConfig {
......
/**
* Filter工厂,设置对应的过滤条件和跳转条件
* @param securityManager
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login"); //登录
shiroFilterFactoryBean.setSuccessUrl("/index"); //首页
// 添加 jwt 专用过滤器,拦截除 /login 和 /logout 外的请求
Map<String, Filter> filterMap = new LinkedHashMap<>();
filterMap.put("jwtFilter", jwtFilter());
shiroFilterFactoryBean.setFilters(filterMap);
LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不需要权限过滤的路径
filterChainDefinitionMap.put("/login","anon");
filterChainDefinitionMap.put("/refreshToken","anon");
filterChainDefinitionMap.put("/logout","logout");
// 配置需要权限过滤的路径
filterChainDefinitionMap.put("/**", "jwtFilter,authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
......
/**
* 不向 Spring容器中注册 JwtFilter Bean,
* 防止 Spring 将 JwtFilter 注册为全局过滤器
*/
@Bean
public FilterRegistrationBean<Filter> registration(JwtFilter filter) {
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<Filter>(filter);
registration.setEnabled(false);
return registration;
}
@Bean
public JwtFilter jwtFilter() {
return new JwtFilter();
}
/**
* 禁用session, 不保存用户登录状态。保证每次请求都重新认证
*/
@Bean
protected SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
}