Shiro+JWT实现认证和授权

流程:

1、将token(JWT生成和验证)封装成认证对象(使用ThreadLocal保证线程安全)
在这里插入图片描述
2、定义认证与授权的实现方法(Realm类)
3、拦截HTTP请求,验证Token(Filter)
- 验证token合法性,判断是否过期,若客户端token过期,redis中的未过期则重新生成token反之重新登录。
4、把设置应用到Shiro框架(创建ShiroConfig回传四个对象)
在这里插入图片描述
5、 回传token:使用AOP拦截Web对象返回方法,从ThreadLocalToken中获取token写入返回对象,然后返回。
在这里插入图片描述

在这里插入图片描述
代码实现

JWT加密和验证token

/**
 * JWT对userId进行加密生成token
 * 生成token、从token中获得userId、验证token的合法性
 */
@Component
public class JwtUtil {
    @Value("${emos.jwt.secret}")
    private String secret;
    @Value("${emos.jwt.expire}")
    private int expire;
    @Value("${emos.jwt.cache-expire}")
    private String cacheExpire;

    //使用userId创建token
    public String creatToken(int userId){
        //计算偏移5天(expire)后的数据
        Date date=DateUtil.offset(new Date(), DateField.DAY_OF_YEAR,expire);
        //创建算法对象
        Algorithm algorithm=Algorithm.HMAC256(secret);
        JWTCreator.Builder builder= JWT.create();
        //加密
        String token=builder.withClaim("userId",userId).withExpiresAt(date).sign(algorithm);
        return token;
    }
    //从token中获取userId
    public int getUserId(String token){
        //创建一个解码对象
        DecodedJWT jwt=JWT.decode(token);
        int userId=jwt.getClaim(token).asInt();
        return userId;
    }
    //验证token是否合法
    public void verifierToken(String token){
        //创建算法对象
        Algorithm algorithm=Algorithm.HMAC256(secret);
        //构造验证对象
        JWTVerifier verifier=JWT.require(algorithm).build();
        //验证token,是不是是否过期,是不是使用secret密钥进行加密的
        verifier.verify(token);//验证失败抛出RuntimException,所以不需要返回,直接捕获异常即可
    }
}

一、封装token

@Component
public class ThreadLocalToken {
    private ThreadLocal<String> tokenLocal=new ThreadLocal<>();
    public void setTokenLocal(String token){
        tokenLocal.set(token);
    }
    public String getTokenLocal(){
        return tokenLocal.get();
    }
    public void clear(){
        tokenLocal.remove();
    }
}

二、创建Realm类继承AuthorizingRealm ,实现授权和认证

//shiro授权,验证token
@Component
public class AuthRealm extends AuthorizingRealm {
    @Autowired
    private JwtUtil jwtUtil;

    /**
     * 判断传入的token对象是否符合要求
     * AuthenticationToken是接口类型,传入AuthenticationToken的子类即可,判断是否是自定义的AuthToken类对象
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof AuthToken;
    }

    /**
     * 授权(验证权限时调用)
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo();
        //TODO 查询用户的权限列表
        //TODO 把权限列表添加到info对象中
        return (AuthorizationInfo) info;
    }

    /**
     * 认证(登录时调用)
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //TODO 从令牌中获取userId,然后检测该账户是否被冻结
        SimpleAuthenticationInfo info=new SimpleAuthenticationInfo();
        //TODO 往info对象中添加用户信息、Token字符串
        return info;
    }
}

三、创建Filter继承AuthenticatingFilter 对HTTP请求进行过滤,并对token进行验证

/**
 * 对请求进行过滤,对Options请求放行
 * 封装token对象
 */
@Component
@Scope("prototype")
public class AuthFilter extends AuthenticatingFilter {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Value("${emos.jwt.cache-expire}")
    private int cacheExpire;
    @Autowired
    private JwtUtil jwtUtil; //验证token
    @Autowired
    private RedisTemplate redisTemplate; //操作Redis

    /**
     * 拦截请求后,用于把令牌字符串封装成令牌对象AuthToken
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        //获取请求token
        String token=getRequestToken((HttpServletRequest) servletRequest);
        if (StrUtil.isBlank(token)){ //判断Token是否为null和空字符串
            return null;
        }
        return new AuthToken(token);
    }

    /**
     * 拦截请求,判断是否需要被Shiro处理,放行Options请求
     * 返回为true则不执行onAccessDenied,反之执行
     * @param request
     * @param response
     * @param mappedValue
     * @return true 放行 false 不放行
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req= (HttpServletRequest) request;
        //Ajaxt提交application/json数据的时候,会先发出Options请求(判断连接是否可用,不携带数据)
        //这里要放行Options请求,不需要Shiro处理
        if(req.getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }
        //除了Options请求外,都不予放行,等待被Shiro处理
        return false;
    }

    /**
     * 从请求头中获取token,验证token是否有效,是否需要刷新
     * false 请求结束
     * true 进入到业务controller
     * 核心: 调用executeLogin, 并不是真正的login, 本质还是调用subject.login(token)到Realm去做认证, 返回true认证通过, 访问controller
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request= (HttpServletRequest) servletRequest;
        //设置响应体参数
        HttpServletResponse response= (HttpServletResponse) servletResponse;
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        //允许跨域请求
        response.setHeader("Access-Control-Allow-Credentials","true");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
        //使用前先清空
        threadLocalToken.clear();

        String token=getRequestToken(request);
        if (StrUtil.isBlank(token)){ //token为空返回客户端一个错误消息
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            response.getWriter().print("无效令牌");
            return false;
        }
        //验证token
        try{
            jwtUtil.verifierToken(token);//验证失败抛出异常
        }catch (TokenExpiredException e){//过期异常,刷新令牌
            //如果客户端token过期,判断redis中的缓存异常是否过期
            //缓存异常过期则需要重新登录,未过期就重新生成token
            if(redisTemplate.hasKey(token)){ //缓存token未过期
                redisTemplate.delete(token);
                //从老token中获取userId重新加密生成新token,并添加到redis和ThreadLocal中
                int userId=jwtUtil.getUserId(token);
                token=jwtUtil.creatToken(userId);
                redisTemplate.opsForValue().set(token,userId+"",cacheExpire, TimeUnit.DAYS);
                threadLocalToken.setTokenLocal(token);
            }
            else{ //两者都过期,需要重新登录
                response.setStatus(HttpStatus.SC_UNAUTHORIZED);
                response.getWriter().print("令牌已过期");
                return false;
            }
        }catch (JWTDecodeException e){//内容异常
            response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            response.getWriter().print("无效令牌");
            return false;
        }
        //令牌正常,执行Realm类,进行Shiro的认证和授权
        //调用executeLogin, 并不是真正的login, 本质还是调用subject.login(token)到Realm去做认证, 返回true认证通过, 访问controller
        return executeLogin(servletRequest,servletResponse);//认证和授权失败都返回false
    }

    /**
     * executeLogin  认证失败调用方法
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        //认证失败返回状态码等响应信息
        HttpServletResponse resp= (HttpServletResponse) request;
        response.setContentType("text/html");
        response.setCharacterEncoding("UTF-8");
        //允许跨域请求
        resp.setHeader("Access-Control-Allow-Credentials","true");
        resp.setHeader("Access-Control-Allow-Origin", resp.getHeader("Origin"));
        resp.setStatus(HttpStatus.SC_UNAUTHORIZED);
        try {
            resp.getWriter().print(e.getMessage());//返回错误信息
        } catch (Exception exception) {

        }

        return false;
    }

    @Override
    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 从请求头中获取token
     * @param request
     * @return
     */
    private String getRequestToken(HttpServletRequest request){
        String token=request.getHeader("token");
        if (StrUtil.isBlank(token)){ //如果请求头中获取不到token就尝试从请求体中获得
            token=request.getParameter("token");
        }
        return token;
    }
}

四、创建ShiroConfig,将Realm和Filter写入Shiro框架

@Configuration
public class ShiroConfig {
    /**
     * 封装Realm对象
     * @param realm
     * @return
     */
    @Bean("securityManager")
    public SecurityManager securityManager(AuthRealm realm){
        DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    /**
     * 封装Filter对象
     * 设置Filter拦截路径
     * @param securityManager
     * @param filter
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,AuthFilter filter){
        ShiroFilterFactoryBean shiroFilter=new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        //将Filter对象封装到Map中,然后map传给facteryBean
        Map<String, Filter> map=new HashMap<>();
        map.put("authc",filter); //authc--拦截
        shiroFilter.setFilters(map);

        //过滤规则
        Map<String,String> mapFilter=new LinkedHashMap<>();
        //TODO 拦截规则待更新
        //anno表示允许匿名访问
        mapFilter.put("/webjars/**","anno");
        mapFilter.put("/druid/**","anno");
        mapFilter.put("/app/**","anno");
        mapFilter.put("/sys/login","anno");
        mapFilter.put("/swagger-ui.html","anno");
        mapFilter.put("/druid/**","anno");
        mapFilter.put("/v2/api-docs","anno");
        mapFilter.put("/user/register","anno");
        mapFilter.put("/user/login","anno");
        mapFilter.put("/test/**","anno");
        mapFilter.put("/**","authc");//authc--进行身份认证后才能访问
        shiroFilter.setFilterChainDefinitionMap(mapFilter);
        return shiroFilter;
    }

    /**
     * 管理Shiro对象生命周期
     * @return
     */
    @Bean()
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }
    /**
     * 开启Shiro注解模式,可以在Controller中的方法上添加注解
     * AOP切面类,Web方法执行前,验证权限
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor=new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

五、创建TokenAop拦截Web方法返回对象,传递给客户端新的token

/**
 * 拦截web方法的返回值,对token进行处理
 */
@Component
@Aspect
public class TokenAop {
    @Autowired
    private ThreadLocalToken threadLocalToken;

    @Pointcut("execution(public * com.example.emoswx.controller.*.*(..))")
    public void aspect(){

    }

    @Around("aspect()") //环绕通知
    //通过ProceedingJoinPoint获取当前执行的方法
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
       ReturnMap returnMap = (ReturnMap) joinPoint.proceed();//执行目标方法,因为controller中的方法返回值被封装成R对象,所以可以使用R对象接收
       String token=threadLocalToken.getTokenLocal();
       if (token!=null){  //判断是否生成新的token,生成则返回给客户端
           returnMap.put("token",token);
           //每次使用完ThreadLocal都调用它的remove()方法清除数据,防止内存泄露
           threadLocalToken.clear();
       }
       return returnMap;
    }

}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值