令牌刷新机制
客户端发送请求,过滤器拦截,如果客户端令牌过期,redis没有过期,重新生成新的token,保存在ThreadLocalToken类(多线程问题,防止书籍覆盖覆盖),然后执行controller,设置aop,从ThreadLocalToken中取出新的token,添加到过滤器,返回给客户端。 filter不能直接调用aop所以设置了中间类ThreadLocalToken类 (同一个线程)为什么不把生成令牌的方式写道filtet中,因为filter要操作io流,麻烦
1、创建OAuth2Token 客户端传进来的token不能直接被shiro使用,所以创建OAuth2Token类,对客户端传进来token进行封装
package com.example.shirojwt.config;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @Author: Ja7
* @Date: 2022-01-02 11:07
*/
/*
1、
* token不能shiro认证对象
* 客户端提交的token不能直接返回给shiro框架
* 需要先封装成AuthenticationToken类型对象
* 才能被shiro使用
*
* 流程 AuthenticationToken -> AuthorizingRealm -> AuthenticatingFilter -> ShiroConfig(springboot就可以使用shiro和jwt技术)
* */
// 扩展方法
public class OAuth2Token implements AuthenticationToken {
// 方法覆盖
private String token;
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
// 构造器,给token传参
public OAuth2Token(String token) {
this.token = token;
}
}
2、创建realm类
package com.example.shirojwt.config;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 2、
*
* @Author: Ja7
* @Date: 2022-01-02 11:21
* 继承 AuthorizingRealm
*/
@Component
public class OAuth2Realm extends AuthorizingRealm {
/*
* 覆盖
* 判断传进来的token是不是OAuth2Token类型
* */
// 处理令牌字符串
// instanceof 判断是不是OAuth2Token类
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof OAuth2Token;
}
@Resource
private JwtUtil jwtUtil;
/*
* 授权
* */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 创建授权对象(要求我们返回一个授权对象)
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 查询用户的权限列表
// 把权限添加到info对象中
return info;
}
/*
* 认证
* */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 从令牌中获取userId,然后检测该账户是否被冻结
// 创建认证对象
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(); // 认证对象
// 往info对象中添加用户信息、token字符串
return null;
}
}
3、创建媒介类
package com.example.shirojwt.config;
import org.springframework.stereotype.Component;
/**
* @Author: Ja7
* @Date: 2022-01-02 12:34
*/
/*
* 3、媒介类
* */
@Component
public class ThreadLocalToken {
private ThreadLocal<String> local = new ThreadLocal<>();
public void setToken(String token) {
local.set(token);
}
public String getToken() {
return local.get();
}
public void clear() {
local.remove();
}
}
4、创建filter
package com.example.shirojwt.config;
import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.exceptions.TokenExpiredException;
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.annotation.Resource;
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;
/*
* 3、
* */
@Component
@Scope("prototype") // 变成多例对象 不加的话 ThreadLocalToken有问题
public class OAuth2Filter extends AuthenticatingFilter {
@Autowired
private ThreadLocalToken threadLocalToken;
@Value("${emos.jwt.cache-expire}")
private int cacheExpire;
@Resource
private JwtUtil jwtUtil;
@Resource
private RedisTemplate redisTemplate;
/*
1、方法覆盖从请求中获取令牌字符串封装成令牌对象,最后会交给shiro
OAuth2Token在这里使用
* 拦截请求之后,用于把令牌字符串封装成令牌对象
* */
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
// 强制类型转换
HttpServletRequest req = (HttpServletRequest) request;
// -1步骤 从请求头获取令牌字符串
String token = getRequestToken(req);
if (StrUtil.isBlank(token)) {
return null;
}
// 令牌不等于空,封装成OAuth2Token对象
return new OAuth2Token(token);
}
/*
2、 判断哪些方法是应该被shiro处理,那些不应该被处理(options请求不应该被处理/预处理)
* 判断那种方法是可被shiro框架处理
* */
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
// 强制类型转换
HttpServletRequest req = (HttpServletRequest) request;
// 判断是不是options请求,
if (req.getMethod().equals(RequestMethod.OPTIONS.name())) { // 判断是不是Options请求,如果是,就放行,不是就拦截
// 直接放行
return true;
}
// 处理options请求外,都该被处理
return false;
}
/*
3、
* 处理所有应该被shiro处理的请求
* */
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
// 强制类型转换
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 设置响应头
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
// 允许跨域请求(设置跨域参数)
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
// 清空ThreadLocalToken
threadLocalToken.clear();
// 拿到请求头或者请求体中的token
String token = getRequestToken(req);
// 判断token是不是空
if (StrUtil.isBlank(token)) { // 没有token
//设置响应码
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效令牌");
// 验证了,这次是无效的,就不需要在执行后面的realm类
return false;
}
try {
// 验证令牌
jwtUtil.verifierToken(token);
} catch (TokenExpiredException e) {// 令牌过期
// 判断redis中是否还存在token
if (redisTemplate.hasKey(token)) { // redis中还保存着令牌
// 删除老令牌
redisTemplate.delete(token);
// 使用jwtUtil 生成新令牌
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 (Exception e) { // 有可能是伪造的
// 内容有问题
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
resp.getWriter().print("无效令牌");
return false;
}
// 令牌没问题了 间接让shiro调用realm类调用 Realm类
boolean bool = executeLogin(request, response);
// 如果是false 就是认证或者授权失败
return bool;
}
/*
4 、
* shiro认定: 判断用户没有登录,或者登录失败执行此方法 往客户端返回错误消息
* 覆盖此方法可以知道是认证失败,或者是授权失败,如果直接从142 行进行判断,就不能得出是哪个失败
* */
@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
// 强制类型转换
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// 设置响应头
resp.setContentType("text/html");
resp.setCharacterEncoding("UTF-8");
// 允许跨域请求(设置跨域参数)
resp.setHeader("Access-Control-Allow-Credentials", "true");
resp.setHeader("Access-Control-Allow-Origin", req.getHeader("Origin"));
resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
try {
resp.getWriter().print(e.getMessage()); // e.getMessage() 返回错误消息
} catch (Exception exception) {
}
// 认证失败
return false;
}
/*
5、
* 作用:与doFilter
* 功能:掌管请求权和拦截方法 与传统doFilter一样
* */
@Override
public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
super.doFilterInternal(request, response, chain);
}
/*
-1
* 请求中获取令牌
* */
private String getRequestToken(HttpServletRequest request) {
// 从请求投中获取
String token = request.getHeader("token");
if (StrUtil.isBlank(token)) {
// 如果请求头中没有,就判断请求体中有没有
token = request.getParameter("token");
}
return token;
}
}
5、装配
package com.example.shirojwt.config;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/*
* 5
* */
@Configuration
public class ShiroConfig {
/*
* 封装OAuth2Realm类, OAuth2Realm添加了@Component注解
* */
@Bean("securityManager")
public SecurityManager securityManager (OAuth2Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
securityManager.setRememberMeManager(null); // 查资料
return securityManager;
}
/*
* 封装OAuth2Filter类
* */
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, OAuth2Filter filter) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> map = new HashMap<>();
map.put("oauth2", filter); // 把自定义的过滤器放到ShiroFilterFactoryBean中
shiroFilter.setFilters(map);
// 存放那些需要拦截,哪些不需要拦截 anon不拦截
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/webjars/**", "anon");
filterMap.put("/druid/**", "anon");
filterMap.put("/app/**", "anon");
filterMap.put("/sys/login", "anon");
filterMap.put("/swagger/**", "anon");
filterMap.put("/v2/api-docs", "anon");
filterMap.put("/swagger-ui.html", "anon");
filterMap.put("/swagger-resources/**", "anon");
filterMap.put("/captcha.jpg", "anon");
filterMap.put("/user/register", "anon");
filterMap.put("/user/login", "anon");
// filterMap.put("/test/**", "anon");
filterMap.put("/meeting/recieveNotify", "anon");
filterMap.put("/**", "oauth2"); // 会被44 行执行过滤
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
@Bean // 默认用对象名首字母小写 作为bean名
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/*
aop
* web方法执行前,验证权限
* */
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
// 往aop方法中保存setSecurityManager
advisor.setSecurityManager(securityManager);
return advisor;
}
}
6、创建aop将新的令牌返回给客户端
package com.example.shirojwt.aop;
import com.example.shirojwt.config.ThreadLocalToken;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/*
* 6
* 往客户端返回中添加新生成的令牌
* */
@Aspect
@Component
public class TokenAspect {
@Resource
private ThreadLocalToken threadLocalToken;
// 切点 : 拦截那些方法的调用
@Pointcut("execution(public * com.example.shirojwt.controller.*.*(..)))")
public void aspect() {
}
// 定义事件:环绕事件
@Around("aspect()")
public Object around(ProceedingJoinPoint point) throws Throwable {
R r = (R) point.proceed(); // 方法执行结果
String token = threadLocalToken.getToken();
// 如果ThreadLocal中存在token,说明是新的Token
if (token != null) {
r.put("token", token); // 往响应中放置token
// 清理掉 threadLocalToken中的token 保证threadLocalToken只会有一个token
threadLocalToken.clear();
}
return r;
}
}