Apache Shiro+JWT

一、概述

什么是Apache Shiro

Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。

Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但事实并非如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员确保其应用程序安全的工作。

以下是Apache Shiro可以做的一些事情:

验证用户以验证其身份
为用户执行访问控制,例如:
确定是否为用户分配了某个安全角色
确定是否允许用户执行某些操作
在任何环境中使用Session API,即使没有Web容器或EJB容器也是如此。
在身份验证,访问控制或会话生命周期内对事件做出反应。
聚合用户安全数据的1个或多个数据源,并将其全部显示为单个复合用户“视图”。
启用单点登录(SSO)功能
无需登录即可为用户关联启用“记住我”服务
......
以及更多 - 全部集成到一个易于使用的内聚API中。
Shiro尝试为所有应用程序环境实现这些目标 - 从最简单的命令行应用程序到最大的企业应用程序,而不会强制依赖其他第三方框架,容器或应用程序服务器。当然,该项目旨在尽可能地融入这些环境,但它可以在任何环境中开箱即用。

Apache Shiro功能

Apache Shiro+JWT_第1张图片
功能模块.png

四大核心模块

  • 身份验证:有时称为“登录”,这是证明用户是他们所说的人的行为。

  • 授权:访问控制的过程,即确定“谁”可以访问“什么”。

  • 会话管理:即使在非Web或EJB应用程序中,也可以管理特定于用户的会话。

  • 密码学:使用加密算法保持数据安全,同时仍然易于使用。

还有其他功能可以在不同的应用程序环境中支持和强化这些问题,尤其是:

Web支持:Shiro的Web支持API可帮助轻松保护Web应用程序。
缓存: 缓存是Apache Shiro API中的第一层公民,可确保安全操作保持快速高效。
并发: Apache Shiro支持具有并发功能的多线程应用程序。
测试: 存在测试支持以帮助您编写单元和集成测试,并确保您的代码按预期受到保护。
“运行方式”: 允许用户假定其他用户的身份(如果允许)的功能,有时在管理方案中很有用。
“记住我”: 记住用户在会话中的身份,因此他们只需要在必要时登录。

二、架构分析

1.Shiro的架构有3个主要概念:和SubjectSecurityManagerRealms
Apache Shiro+JWT_第2张图片
架构图.png
  • Subject
    官网的描述为当前的用户、第三方服务等,其实就是与集成了shiro的系统交互的访客,抽象为Subject。
    Subject实例必须绑定一个SecurityManager
  • SecurityManager
    SecurityManager是Shiro架构的核心,充当一种“伞形”对象,协调其内部安全组件,共同形成对象图。 我们只需要对其进行相应的配置即可
    当我们与Subject交互时,实际上工作的是幕后的SecurityManager,它可以完成任何Subject安全操作的繁重任务。
  • Realms
    Realms充当Shiro与应用程序安全数据之间的“桥梁”或“连接器”。当实际与安全相关数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个领域中查找许多这些内容。
    从这个意义上讲,Realm本质上是一个特定于安全性的DAO:它封装了数据源的连接细节,并根据需要使相关数据可用于Shiro。配置Shiro时,必须至少指定一个Realm用于身份验证和/或授权。所述SecurityManager可与多个境界被配置,但至少有一个是必需的。
    Shiro提供了开箱即用的Realms,可以连接到许多安全数据源(也称为目录),如LDAP,关系数据库(JDBC),文本配置源(如INI和属性文件等)。如果默认域不符合您的需要,您可以插入自己的Realm实现来表示自定义数据源。
    与其他内部组件一样,Shiro SecurityManager管理如何使用Realms获取要表示为Subject实例的安全性和身份数据。
2.详细架构
Apache Shiro+JWT_第3张图片
image.png
  • Subjectorg.apache.shiro.subject.Subject
    当前与软件交互的实体(用户,第三方服务,cron作业等)的特定于安全性的“视图”。

  • SecurityManager(org.apache.shiro.mgt.SecurityManager)
    如上所述,这SecurityManager是Shiro建筑的核心。它主要是一个“伞形”对象,协调其托管组件,以确保它们一起平稳运行。它还管理Shiro对每个应用程序用户的视图,因此它知道如何对每个用户执行安全操作。

  • 认证器(org.apache.shiro.authc.Authenticator)
    Authenticator是,负责执行和反应以验证(注册)用户企图的组件。当用户尝试登录时,该逻辑由执行Authenticator。该Authenticator知道如何与一个或多个协调Realms存储有关用户/帐户信息。从这些数据中获取的数据Realms用于验证用户的身份,以保证用户确实是他们所说的人。

  • AuthenticationStrategy(org.apache.shiro.authc.pam.AuthenticationStrategy)
    如果Realm配置了多个,AuthenticationStrategy则将协调领域以确定身份验证尝试成功或失败的条件(例如,如果一个领域成功但其他领域失败尝试是否成功?必须所有领域成功吗?只有第一个?)。

  • Authorizer(org.apache.shiro.authz.Authorizer)
    Authorizer是部件负责确定用户在该应用程序的访问控制。这种机制最终会说明是否允许用户做某事。与此类似Authenticator,它Authorizer也知道如何协调多个后端数据源以访问角色和权限信息。在Authorizer使用该信息来确定到底是否允许用户执行特定的操作。

  • SessionManager(org.apache.shiro.session.mgt.SessionManager)
    SessionManager知道如何创建和管理用户Session生命周期,提供在所有环境中的用户强大的会话体验。这是安全框架领域的一项独特功能 - 即使没有可用的Web / Servlet或EJB容器,Shiro也能够在任何环境中本地管理用户Sessions。默认情况下,Shiro将使用现有的会话机制(例如Servlet容器),但如果没有,例如在独立应用程序或非Web环境中,它将使用其内置的企业会话管理提供相同的编程经验。的SessionDAO存在允许任何数据源被用来坚持的会议。

  • SessionDAO(org.apache.shiro.session.mgt.eis.SessionDAO)
    SessionDAO执行Session代表的持久性(CRUD)操作SessionManager。这允许将任何数据存储插入会话管理基础结构。

  • CacheManager(org.apache.shiro.cache.CacheManager)
    CacheManager创建和管理Cache其他四郎组件使用实例的生命周期。由于Shiro可以访问许多后端数据源以进行身份​​验证,授权和会话管理,因此缓存一直是框架中的一流架构功能,可在使用这些数据源时提高性能。任何现代开源和/或企业缓存产品都可以插入Shiro,以提供快速有效的用户体验。

  • Cryptography(org.apache.shiro.crypto.*)
    密码学是企业安全框架的自然补充。Shiro的crypto软件包包含易于使用和理解的密码密码,哈希(aka摘要)和不同编解码器实现的表示。该软件包中的所有类都经过精心设计,易于使用且易于理解。使用Java本机加密支持的任何人都知道它可能是一个具有挑战性的驯服动物。Shiro的加密API简化了复杂的Java机制,使密码学易于用于普通的凡人。

  • Realm(org.apache.shiro.realm.Realm)
    如上所述,Realms充当Shiro与应用程序安全数据之间的“桥接”或“连接器”。当实际与安全相关数据(如用户帐户)进行交互以执行身份验证(登录)和授权(访问控制)时,Shiro会从为应用程序配置的一个或多个领域中查找许多这些内容。您可以根据Realms需要配置任意数量(通常每个数据源一个),Shiro将根据需要进行身份验证和授权协调。

总结:Subject相当于shiro的门面(前台),负责对外交互,实际的验证授权等需要由SecurityManager决定,而SecurityManager做出决定需要经过数据验证,那么数据由Realm来提供,到此就把shiro的框架流程串起来了。

二、shiro整合spring

由于我们在开发中,基本上都是结合spring使用shiro,所以这里略过了官网的入门案例

Shiro一直支持Spring Web应用程序。在Web应用程序中,所有可通过Shiro访问的Web请求都必须通过主Shiro过滤器。此过滤器本身非常强大,允许基于任何URL路径表达式执行临时自定义过滤器链。
以下是如何在基于Spring的Web应用程序中配置Shiro:

1、采用xml方式配置(不推荐使用)

在web.xml中配置shiro过滤器




    shiroFilter
    org.springframework.web.filter.DelegatingFilterProxy
    
        targetFilterLifecycle
        true
    


...



    shiroFilter
    /*

在applicationContext.xml中配置



    
    
    
  
    
  
     -->
    
    
    
    

    
        
            # some example chain definitions:
            /admin/** = authc, roles[admin]
            /docs/** = authc, perms[document:read]
            /** = authc
            # more URL-to-FilterChain definitions here
        
    








 ... 
...



    
    
    
    







    ...

开启shiro注解
在请求接口中,也许我们会通过shiro的注解进行安全认证,(例如@RequiresRoles, @RequiresPermissions等等)
方法很简单,我们只需要在applicationContext.xml中添加如下配置,但注意,此时必须保证lifecycleBeanPostProcessor进行了配置




    
    

2.采用注解的方式进行配置(推荐使用)

shiro的配置

2.1 首先自定义Realm
/**
 * 认证
 *
 */
@Component
public class MyRealm extends AuthorizingRealm {
  
    /**
     * 授权(验证权限时调用)
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        User user = (User)principals.getPrimaryPrincipal();
        Long userId = user.getUserId();

        //用户权限列表
        Set permsSet = shiroService.getUserPermissions(userId);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(permsSet);
        return info;
    }

    /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
     UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token;
        String username = usernamePasswordToken.getUsername();
       char[] password = usernamePasswordToken.getPassword();
      //根据用户名和密码去验证用户
        ...
      //验证通过后
        User user = getUser();

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password,username);
        return info;
    }
}

2.2 shiro的配置类
/**
 * Shiro配置
 *
 */
@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public SecurityManager securityManager(MyRealm myRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);
        securityManager.setSessionManager(sessionManager);

        return securityManager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

      //放行一些不用权限验证的路径
        Map filterMap = new LinkedHashMap<>();
        filterMap.put("/webjars/**", "anon");
        filterMap.put("/druid/**", "anon");
        filterMap.put("/api/**", "anon");
        filterMap.put("/sys/login", "anon");
        filterMap.put("/**/*.css", "anon");
        filterMap.put("/**/*.js", "anon");
        filterMap.put("/**/*.html", "anon");
        filterMap.put("/img/**", "anon");
        filterMap.put("/fonts/**", "anon");
        filterMap.put("/plugins/**", "anon");
        filterMap.put("/swagger/**", "anon");
        filterMap.put("/favicon.ico", "anon");
        filterMap.put("/captcha.jpg", "anon");
        filterMap.put("/", "anon");
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }
  
    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }


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

}

配置过滤器,将上面写的过滤器加入到容器中

package cn.environmental.config;

import cn.expand.filter.CorsFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;

import cn.environmental.common.xss.XssFilter;

import javax.servlet.DispatcherType;

/**
 * Filter配置
 *
 */
@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean shiroFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        //注意shiroFilter和shiroConfig中匹配
        registration.setFilter(new DelegatingFilterProxy("shiroFilter"));
        //该值缺省为false,表示生命周期由SpringApplicationContext管理,设置为true则表示由ServletContainer管理
        registration.addInitParameter("targetFilterLifecycle", "true");
        registration.setEnabled(true);
        registration.setOrder(Integer.MAX_VALUE - 1);
        registration.addUrlPatterns("/*");
        return registration;
    }

  
}

三、Shiro整合JWT

JWT的介绍不在这里进行说明了,可以自己查阅相关资料
引入JWT后的过程如下:

3.1 在用户登录的成功的时候为其生成token,并返回,用户访问其他接口时需要携带token!

①引入jjwt依赖


    io.jsonwebtoken
    jjwt
    0.9.0

②生成token

public class JwtTest {

    /****
     * 创建Jwt令牌
     */
    @Test
    public void testCreateJwt(){
        JwtBuilder builder= Jwts.builder()
                .setId("888")             //设置唯一编号
                .setSubject("小白")       //设置主题  可以是JSON数据
                //.addClaims(xxx)  自定义载荷信息
                .setIssuedAt(new Date())  //设置签发日期
                //.setExpiration(date)//用于设置过期时间 ,参数为Date类型数据
                .signWith(SignatureAlgorithm.HS256,"秘钥");//设置签名 使用HS256算法,并设置SecretKey(字符串)
        //构建 并返回一个字符串
        System.out.println( builder.compact() );
//eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjIyODd9.RBLpZ79USMplQyfJCZFD2muHV_KLks7M1ZsjTu6Aez4

    }
}

③解析token

/***
 * 解析Jwt令牌数据
 */
@Test
public void testParseJwt(){
    String compactJwt="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLlsI_nmb0iLCJpYXQiOjE1NjIwNjI5MjUsImV4cCI6MTU2MjA2MjkyNX0._vs4METaPkCza52LuN0-2NGGWIIO7v51xt40DHY1U1Q";
    Claims claims = Jwts.parser().
            setSigningKey("秘钥").
            parseClaimsJws(compactJwt).
            getBody();
    System.out.println(claims);
}
3.2 引入token后需要在上面的shiroFilter中加入一个自定义的过滤器来专门验证token

/**
 * token过滤器
 */
public class OAuth2Filter extends AuthenticatingFilter {

    @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);
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if(((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name())){
            return true;
        }

        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //获取请求token,如果token不存在,直接返回401
        String token = getRequestToken((HttpServletRequest) request);
        if(StringUtils.isBlank(token)){
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
            httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());

            String json = new Gson().toJson(R.error(HttpStatus.SC_UNAUTHORIZED, "invalid token"));

            httpResponse.getWriter().print(json);

            return false;
        }

        return executeLogin(request, response);
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setContentType("application/json;charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Origin", HttpContextUtils.getOrigin());
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            R r = R.error(HttpStatus.SC_UNAUTHORIZED, throwable.getMessage());

            String json = new Gson().toJson(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {

        }

        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;
    }


}
public class OAuth2Token implements AuthenticationToken {
    private String token;

    public OAuth2Token(String token){
        this.token = token;
    }

    @Override
    public String getPrincipal() {
        return token;
    }

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

创建完验证token的过滤器后需要在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 shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);

        //token过滤验证
        Map filters = new HashMap<>();
        filters.put("oauth2", new OAuth2Filter());
        shiroFilter.setFilters(filters);

        Map 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("/aaa.txt", "anon");
        filterMap.put("/**", "oauth2");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

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

}

token的过滤器的优先级比较高,这时候需要验证权限的接口就会先判断token是否有效了,需要将上面的Myrealm的认证方法改一下

  /**
     * 认证(登录时调用)
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getPrincipal();

        //验证token的代码(略)
          ...
        //token失效提醒用户(略)
        //return
       
        //查询用户信息
        User user = queryUser(user.getUserId());

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, accessToken, getName());
        return info;
    }

你可能感兴趣的:(Apache Shiro+JWT)