shiro 结合 jwt

令牌刷新机制

客户端发送请求,过滤器拦截,如果客户端令牌过期,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;
    }
}
​

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值