关于java:Shiro-JWT权限验证

25次阅读

共计 11646 个字符,预计需要花费 30 分钟才能阅读完成。

1、什么是 Shiro

Shiro 是 Java 畛域十分出名的认证(Authentication)与受权(Authorization)框架,用以代替 JavaEE 中的 JAAS 性能。相较于其余认证与受权框架,Shiro 设计的非常简单,所以广受好评。任意 JavaWeb 我的项目都能够应用 Shiro 框架,而 Spring Security 必须要应用在 Spring 我的项目中。所以 Shiro 的适用性更加宽泛。像什 么 JFinal 和 Nutz 非 Spring 框架都能够应用 Shiro,而不能应用 Spring Security 框架。

  • 两大性能:认证 受权
  • 实用于 JavaWeb 我的项目
  • 不是 Spring 框架下的产品

1.1 什么是认证和受权

  • 认证:认证就是要核验用户的身份,比如说通过用户名和明码来测验用户的身份。说简略一些,认证就

是登陆。登陆之后 Shiro 要记录用户胜利登陆的凭证。

  • 受权:受权是比认证更加精密度的划分用户的行为。比如说在咱们开发的零碎中,对于数据库一般的开发人员可能只有读写权限,而数据库管理员能够有读写删等更高级的权限。这就是利用受权来限定不同身份用户 的行为。

1.2 shrio 怎么上述两个性能?

Shiro 能够利用 HttpSession 或者 Redis 存储用户的登陆凭证 ,以及角色或者身份信息。而后利用过滤器(Filter),对 每个 Http 申请过滤,查看申请对应的 HttpSession 或者 Redis 中的认证与受权信息。如果用户没有登陆,或者权限不够,那么 Shiro 会向客户端返回错误信息。也就是说,咱们写用户登陆模块的时候,用户登陆胜利之后,要调用 Shiro 保留登陆凭证。而后查问用户的角色和权限,让 Shiro 存储起来。未来不论哪个办法须要登陆拜访,或者领有特定的角色 跟权限能力拜访,咱们在办法前设置注解即可,非常简单。

2、什么是 JWT

JWT(Json Web Token), 是为了在网络应用环境间传递申明而执行的一种基于 JSON 的凋谢标 准。JWT 个别被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够减少一些额定的其它业务逻辑所必须的申明信息,该 token 也可间接被用于认证,也可被加密。

  • JWT 能够用在单点登录的零碎中

如果用户的 登陆凭证通过加密(Token)保留在客户端,客户端每次提交申请的时候,把 Token 上传给后端服务器节点。即使后端我的项目应用了负载平衡,每个后端节点接管到客户端上传 的 Token 之后,通过检测,是无效的 Token,于是就判定用户曾经胜利登陆,接下来就能够提供后端服务了

  • JWT 兼容更多的客户端

传统的 HttpSession 依附浏览器的 Cookie 寄存 SessionId,所以要求客户端必须是浏览器。当初的 JavaWeb 零碎,客户端能够是浏览器、APP、小程序,以及物联网设施。为了让这些设施都能拜访到 JavaWeb 我的项目,就必须要引入 JWT 技术 。JWT 的 Token 是纯字符串,至于客户端怎么保留,没有具体要求。 只有客户端发动申请的时候,附带上 Token 即可。所以像物联网设施,我 们能够用 SQLite 存储 Token 数据。

3、JWT 的实战

通过下面的介绍,咱们曾经理解了 JWT 的相干信息。说白了,JWT 就是对咱们的 token 进行加密,保留在客户端,每一个客户端的申请过去咱们就对这个 token 进行校验,来判断用户的信息

3.1 创立 JWT 工具类

用于对 Token 进行加密,解密,生成 …

  • 导入依赖
<dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.3</version>
        </dependency>
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.11</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.13</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
  • 为了保护的不便,咱们能够给 token 在配置文件中定义好过期工夫,秘钥,缓存工夫。前面通过属性注入即可

  • 创立 JWT 工具类
@Component
@Slf4j
public class JWTUtil {@Value("${emos.jwt.secret}")
    private String secret;
    @Value("${emos.jwt.expire}")
    private int expire;

    /**
     * 通过 UserId,创立一个 token
     * @param userId
     * @return
     */
    public String createToken(int userId){DateTime offset = DateUtil.offset(new Date(), DateField.DAY_OF_YEAR, expire);
        Algorithm algorithm = Algorithm.HMAC256(secret);// 应用算法加密 secret 密钥
        JWTCreator.Builder builder = JWT.create();
        String token = builder.withClaim("userId", userId)
                              .withExpiresAt(offset)
                              .sign(algorithm);
       return token;

    }

    /**
     * 通过 token 获取 userId
     * @param token
     * @return
     */
    public int getUserId(String token){DecodedJWT decodedJWT = JWT.decode(token);
        int userId = decodedJWT.getClaim("userId").asInt();
        return userId;
    }

    /**
     * 验证密钥
     * @param token
     */
    public void verifierToken(String token){Algorithm algorithm =Algorithm.HMAC256(secret);// 对密钥进行加密算法解决
        JWTVerifier verifier = JWT.require(algorithm).build();// 创立一个 jwt 的 verifier 对象
        verifier.verify(token);// 验证密钥和 token(加密后的密钥局部)是否统一 ---sign
    }



}

4、对接 JWT 和 Shiro 框架

当初咱们能够用过 JWTUtil 工具类生成 token 并对其进行一些操作,而 token 是不能间接交给 Shiro 的,依照 Shiro 框架,咱们还须要对 token 进行封装。

4.1 封装 token 对象

public class OAuth2Token implements AuthenticationToken {

    private String token;

    /**
     * 创立生成 token 对象的结构器
     *
     * @param token
     */
    public OAuth2Token(String token){this.token = token;}
    @Override
    public Object getPrincipal() {return token;}

    @Override
    public Object getCredentials() {return token;}
}

4.2 创立 Realm 类

依据下面实现 shiro 框架的步骤,咱们须要创立 OAuth2Realm 类,这个类须要继承 AuthorizingRealm

OAuth2Realm 类是 AuthorizingRealm 的实现类,咱们要在这个实现类中定义认证和受权的办法。因为认证与受权模块设计到用户模块和权限模块,当初咱们还没有真正的开发业务模块,所 以咱们这里先临时定义空的认证去受权办法,把 Shiro 和 JWT 整合起来,在后续章节咱们再实现 认证与受权。

@Component
public class OAuth2Realm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {return token instanceof OAuth2Token;}
    /**
     * 受权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
      //TODO 查问用户的权限列表
      //TODO 把权限列表增加到 info 对象中
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
        throws AuthenticationException {
        //TODO 从令牌中获取 userId,而后检测该账户是否被解冻。SimpleAuthenticationInfo info = new SimpleAuthenticationInfo();
        //TODO 往 info 对象中增加用户信息、Token 字符串
        return info;
    }
}

4.3 如何设计 token(令牌)的过期工夫

咱们在定义 JwtUtil 工具类的时候,生成的 Token 都有过期工夫。那么问题来了,假如 Token 过 期工夫为 15 天,用户在第 14 天的时候,还能够免登录失常拜访零碎。然而到了第 15 天,用户的 Token 过期,于是用户须要从新登录零碎。

HttpSession 的过期工夫比拟优雅,默认为 15 分钟。如果用户间断应用零碎,只有间隔时间不超 过 15 分钟,零碎就不会销毁 HttpSession 对象。

JWT 的令牌过期工夫能不能做成 HttpSession 那样超时工夫,只有用户距离操作工夫不超过 15 天,零碎就不须要用户从新登录零碎。实现这种 成果的计划有两种:双 Token 和 Token 缓存,这里重点讲一下 Token 缓存计划。

简略的来说,就是当咱们第一次登录的时候,此时咱们无效的登录工夫是 15 天,如果我在第 15 天的时候登录,那么此时还不应该须要从新登录,只有说在第 15 天过后,也就是咱们 15 天内没有操作,且再过 15 天之后过期,这就是为什么咱们缓存的工夫是过期工夫的 1 倍。

Token 缓存计划是把 Token 缓存到 Redis,而后设置 Redis 外面缓存的 Token 过期工夫为失常 Token 的 1 倍, 而后依据状况刷新 Token 的过期工夫。

Token 生效,缓存也不存在的状况:当第 15 天,用户的 Token 生效当前,咱们让 Shiro 程序到 Redis 查看是否存在缓存的 Token,如果这个 Token 不存在于 Redis 外面,就阐明用户的操作距离了 15 天,须要从新登录。

Token 生效,然而缓存还存在的状况 如果 Redis 中存在缓存的 Token,阐明以后 Token 生效后,间隔时间还没有超过 15 天,不应该让用户从新登录。所以要生成新的 Token 返回给客户端,并且把这个 Token 缓存到 Redis 外面,这种操作称为刷新 Token 过期工夫。

咱们定义 OAuth2Filter 类拦挡所有的 HTTP 申请:

一方面它会把申请中的 Token 字符串提取出 来,封装成对象交给 Shiro 框架;

另一方面,它会查看 Token 的有效性。如果 Token 过期,那么 会生成新的 Token,别离存储在 ThreadLocalToken 和 Redis 中。

之所以要把 新令牌 保留到 ThreadLocalToken 外面,是因为要向 AOP 切面类 传递这个 新令牌。尽管 OAuth2Filter 中有 doFilterInternal() 办法,咱们能够失去响应并且写入 新令牌。然而 这个做十分麻烦,首先咱们要通过 IO 流读取响应中的数据,而后还要把数据解析成 JSON 对象,最初再放入这个新令牌。如果咱们定义了 AOP 切面类,拦挡所有 Web 办法返回的 R 对象,而后 在 R 对象 外面增加 新令牌,这多简略啊。然而 OAuth2Filter 和 AOP 切面类之间没有调用关 系,所以咱们很难把 新令牌 传给 AOP 切面类。这里我想到了 ThreadLocal,只有是同一个线程,往 ThreadLocal 外面写入数据和读取数据是 完全相同的。在 Web 我的项目中,从 OAuth2Filter 到 AOP 切面类,都是由同一个线程来执行的,中途不会更换线程。所以咱们能够释怀的把新令牌保留都在 ThreadLocal 外面,AOP 切面类 能够成 功的取出新令牌,而后往 R 对象 外面增加新令牌即可。ThreadLocalToken 是我自定义的类,外面蕴含了 ThreadLocal 类型的变量,能够用来保留线程 平安的数据,而且防止了应用线程锁

4.4 设计 ThreadLocalToken 类

@Component
public class ThreadLocalToken {
    private  ThreadLocal<String> threadLocal;

    public void setToken(String token){threadLocal.set(token);
    }

    public String getToken(){return   threadLocal.get();
    }

    public void clear(){threadLocal.remove();
    }

}

4.5 创立 OAuth2Filter 类

@Component
@Scope("prototype")// 在 spring 的 IOC 容器中应用多例模式创立实例
public class OAuth2Filter extends AuthenticatingFilter {

    @Autowired
    private RedisTemplate redisTemplate;// 将 token 保留到 redis

    @Autowired
    private ThreadLocalToken threadLocalToken;// 保留 token 的封装类

    @Value("${emos.jwt.cache-expire}")
    private int expireDate;// 令牌过期工夫

    @Autowired
    private JWTUtil jwtUtil; // 用于令牌加密的工具类

    /**
     * 生成 token
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest,
                                              ServletResponse servletResponse) throws Exception {HttpServletRequest request = (HttpServletRequest) servletRequest;

        // 从申请中获取 token 并进行判断
        String tokenStr = getTokenByRequest(request);
        if(StrUtil.isBlank(tokenStr)){return  null;}

        return new OAuth2Token(tokenStr);
    }

    /**
     * 是否容许过滤
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
                                      Object mappedValue) {
//        判断是否是 Option 申请
        HttpServletRequest req = (HttpServletRequest) request;
        if(req.getMethod().equals(HttpMethod.OPTIONS.name())){return  true;// 放过 OPTIONS 申请}
        return false;
    }

    /**
     * 解决所有应该有 shiro 解决的申请
     * 也就是不被放过的申请
     * @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.setHeader("Content-Type","text/html;charset=UTF-8");
        // 容许跨域申请
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Origin", response.getHeader("Origin"));
        // 清空以后 ThreadLocalToken 中的 token,因为咱们前面可能要新生成 token
        threadLocalToken.clear();
        // 从申请中获取令牌
        String tokenStr = getTokenByRequest(request);
        // 如果令牌为空,在响应中返回前端相干信息
        if(StrUtil.isBlank(tokenStr)){response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            response.getWriter().print("no useful token");
            return false;
        }

        // 如果令牌存在,刷新令牌
        try{
            // 校验令牌
            jwtUtil.verifierToken(tokenStr);
        }catch (TokenExpiredException e) { // 发现令牌过期

            // 去 redis 中找
            if (redisTemplate.hasKey("token")) {
                //redis 存在,从新为客户生成一个新的 token
                redisTemplate.delete("token");
                int userId = jwtUtil.getUserId(tokenStr);
                String token = jwtUtil.createToken(userId);
                redisTemplate.opsForValue().set(token, userId + "", expireDate, TimeUnit.DAYS);
                threadLocalToken.setToken(token);

            }
            //redis 令牌不存在, 须要从新登录
            else {response.setStatus(HttpStatus.SC_UNAUTHORIZED);
                response.getWriter().print("令牌曾经过期");
                return false;
            }

        }catch (JWTDecodeException e){response.setStatus(HttpStatus.SC_UNAUTHORIZED);
            response.getWriter().print("有效的令牌");
            return false;
        }
        boolean bool = executeLogin(request, response);
        return bool;

    }

    /**
     * 解决登录失败的状况
     * @param token
     * @param e
     * @param request
     * @param response
     * @return
     */
    @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;

        }

    private String getTokenByRequest(HttpServletRequest httpServletRequest){String token = httpServletRequest.getHeader("token");

        if(StrUtil.isBlank(token)){
            // 如果申请头中不存在就去申请体中获取
            token = httpServletRequest.getParameter("token");
        }

        return token;
    }
}

4.6 创立 ShiroConfig 类

@Configuration
public class ShiroConfig {@Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(oAuth2Realm);
        securityManager.setRememberMeManager(null);
        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         OAuth2Filter oAuth2Filter) {ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        HashMap<String, Filter> filters = new HashMap<>();
        filters.put("oauth2", oAuth2Filter);
        shiroFilter.setFilters(filters);

        // 因为 LinkedHashMap 能够保障存获得有序性
        LinkedHashMap<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("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;

    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){return  new LifecycleBeanPostProcessor();
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

}

4.7 利用 AOP,把更新的令牌返回给客户端

@Aspect
@Component
public class tokenAop {
    @Autowired
    private ThreadLocalToken localToken;

    @Pointcut("execution(public * com.example.emos.wx.controller.*.*(..)))")
    public void aspect(){}
    @Around("aspect()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws  Throwable{R r  = (R) proceedingJoinPoint.proceed();
        String token = localToken.getToken();
        if(token != null){r.put("token",token);
            localToken.clear();}
        return r;
    }
}

这样就实现了咱们 Jwt+Shiro 和咱们我的项目的集成!!

正文完
 0