之所以需要创建存储令牌的媒介类,是因为后面的filter界面要使用。
一、创建ThreadLocalToken类
创建ThreadLocalToken类的目的:
在com.example.emos.wx.config.shiro
中创建ThreadLocalToken
类。
写入如下代码:
package com.example.emos.wx.config.shiro;
import org.springframework.stereotype.Component;
@Component
public class ThreadLocalToken {
private ThreadLocal local=new ThreadLocal();
//因为要在ThreadLocal中保存令牌,所以需要setToken。
public void setToken(String token){
local.set(token);
}
public String getToken(){
return (String) local.get();
}
public void clear(){
local.remove();//把绑定的数据删除了
}
}
下图为创建目录的层级关系:
二、创建OAuth2Filter类
创建过滤器的目的:
因为OAuth2Filter
类要读写ThreadLocal
中的数据,所以OAuth2Filter
类必须要设置成多例的,否则ThreadLocal
将无法使用。
在配置文件中,添加JWT需要的密匙,过期时间和缓存过期时间。
emos:
jwt:
#密钥
secret: abc123456
#令牌过期时间(天)
expire: 5
#令牌缓存时间(天数)
cache-expire: 10
在com.example.emos.wx.config.shiro
中创建OAuth2Filter
类。
package com.example.emos.wx.config.shiro;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
在写好@Scope("prototype")
后,就表明以后Spring使用OAuth2Filter
类默认是多例。
@Value("${emos.jwt.cache-expire}")
考察的一个知识点,从xml文件中获取属性文件的属性值。
因为要在Redis中操作,所以要声明private RedisTemplate redisTemplate;
申明好这个对象后,就可以对redis中的数据进行读写操作了。
filter类用来区分哪些请求应该被shiro处理,哪些请求不该被shiro处理。
如果请求被shiro处理的话,那么createToken方法就被执行了,
createToken从请求中获取令牌字符串,然后封装成令牌对象OAuth2Token,交给shiro框架去处理。
getRequestToken是一个自定义方法,用来获取令牌字符串,然后传递给字符串Token对象。
@Component
@Scope("prototype")
public class OAuth2Filter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RedisTemplate redisTemplate;
@Override
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response) throws Exception {
//获取请求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
filter过滤这一块细讲一下:
isAccessAllowed
是判断哪些请求可以被shiro处理,哪些不可以被shiro处理。
由于isAccessAllowed
方法中request
是ServletRequest
,所以需要进行转换HttpServletRequest
,
然后判断这次request
请求是不是options请求。如果不是,就需要被shiro处理。
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
HttpServletRequest req = (HttpServletRequest) request;
// Ajax提交application/json数据的时候,会先发出Options请求
// 这里要放行Options请求,不需要Shiro处理
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
return true;
}
// 除了Options请求之外,所有请求都要被Shiro处理
return false;
}
那么,shiro是怎么处理的呢?
onAccessDenied 方法
设置响应的字符集,和响应的请求头。setHeader
方法用来设置跨域请求。
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setHeader("Content-Type", "text/html;charset=UTF-8");
//允许跨域请求
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
//clear方法用来清理threadLocal类中的方法,
threadLocalToken.clear();
//获取请求token,如果token不存在,直接返回401
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
然后验证令牌是否过期。
如果验证出现问题,就会抛出异常。
通过捕获异常,就知道是令牌有问题,还是令牌过期了。
JWTDecodeException
是内容异常。
通过redisTemplate
的hasKey
查询Redis
是否存在令牌。
如果存在令牌,就删除老令牌,重新生成一个令牌,给客户端。
executeLogin
方法,让shiro
执行realm
类。
try {
jwtUtil.verifierToken(token); //检查令牌是否过期
} catch (TokenExpiredException e) {
//客户端令牌过期,查询Redis中是否存在令牌,如果存在令牌就重新生成一个令牌给客户端
if (redisTemplate.hasKey(token)) {
redisTemplate.delete(token);//删除老令牌
int userId = jwtUtil.getUserId(token);
token = jwtUtil.createToken(userId); //生成新的令牌
//把新的令牌保存到Redis中
redisTemplate.opsForValue().set(token, userId + "", cacheExpire, TimeUnit.DAYS);
//把新令牌绑定到线程
threadLocalToken.setToken(token);
} else {
//如果Redis不存在令牌,让用户重新登录
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("令牌已经过期");
return false;
}
} catch (JWTDecodeException e) {
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效的令牌");
return false;
}
boolean bool = executeLogin(request, response);
return bool;
}
登录失败后输出的信息。
@Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException e, ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.setContentType("application/json;charset=utf-8");
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
try {
resp.getWriter().print(e.getMessage());//捕获认证失败的消息
} catch (IOException exception) {
}
return false;
}
获取请求头里面的token
private String getRequestToken(HttpServletRequest httpRequest) {
//从header中获取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,则从参数中获取token
if (StringUtils.isBlank(token)) {
token = httpRequest.getParameter("token");
}
return token;
}
doFilterInternal
方法从父类doFilterInternal中继承,掌管拦截请求和响应的。这里不覆写。
@Override
public void doFilterInternal(ServletRequest request,
ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
}
到此这篇关于SpringBoot创建存储令牌的媒介类和过滤器的文章就介绍到这了,更多相关SpringBoot内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!