前后端权限控制——JWT与Shiro、vue路由导航守卫

JWT

jwt与传统session方式对比

在这里插入图片描述

基于session的方式会在服务器端产生一个session,然后通过jsessionid对比来找到用户对应的session,当session增多对服务器是一个很大的开销,而基于jwt的方式,每次客户端带来一个token直接通过解析token来鉴权
token可以存储在localstorage、sessionstorage、cookie,localstorage存于本地,如果不手动清楚不会被清掉;sessionstorage存于会话,当浏览器窗口关闭则清掉

创建jwt

导入依赖

<dependency>
	<groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

配置文件

##jwt配置
# 代表这个JWT的接收对象,存入audience
audience.clientId=098f6bcd4621d373cade4e832627b4f6
# 密钥, 经过Base64加密, 可自行替换
audience.base64Secret=MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
# JWT的签发主体,存入issuer
audience.name=restapiuser
# 过期时间,时间戳
audience.expiresSecond=172800

audience实体

@Data
@ConfigurationProperties(prefix = "audience")//获得属性文件中前缀为audience的属性来存入对应的属性中
@Component
public class Audience {

    private String clientId;
    private String base64Secret;
    private String name;
    private int expiresSecond;

}

创建方法

   /**
     * 构建jwt
     * @param userId
     * @param audience
     * @return
     */
    public static String createJWT(String userId, Audience audience) {
        String token = null;
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            //生成签名密钥
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            //userId是重要信息,进行加密下
            String encryId = Base64Util.encode(userId);

            //添加构成JWT的参数
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // 可以将基本不重要的对象信息放到claims
                    .claim("userId", userId)
                    .setIssuer(audience.getClientId())              // 代表这个JWT的签发主体;
                    .setIssuedAt(new Date())        // 是一个时间戳,代表这个JWT的签发时间;
                    .setAudience(audience.getName())          // 代表这个JWT的接收对象;
                    .signWith(signatureAlgorithm, signingKey);
            //添加Token过期时间
            int TTLMillis = audience.getExpiresSecond();
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp)  // 是一个时间戳,代表这个JWT的过期时间;
                        .setNotBefore(now); // 是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的
            }

            //生成JWT
            token = builder.compact();
        } catch (Exception e) {
            log.error("签名失败", e);
            //抛出创建失败的异常
        }
        return token;
    }

jwt的校验

拦截器进行统一校验

拦截器在过滤器之后,可以发挥和过滤器相同的功能,但是过滤器是针对url进行过滤,而拦截器是针对请求的资源进行拦截,所以拦截器可以具体到处理器方法

public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private Audience audience;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String token = request.getHeader("token");
        if(token!=null){
            if(JwtTokenUtil.parseJwt(token,audience.getBase64Secret())){
                return true;
            }else{
                //响应错误信息,ObjectMapper转换json格式
                ObjectMapper mapper = new ObjectMapper();
                String value = mapper.writeValueAsString(""/*错误信息*/);
                response.getWriter().write(value);
                response.getWriter().close();
                return false;
            }
        }else{
            if(request.getRequestURI().endsWith("login")){
                return true;
            }else{
                ObjectMapper mapper = new ObjectMapper();
                String value = mapper.writeValueAsString(""/*错误信息*/);
                response.getWriter().write(value);
                response.getWriter().close();
                return false;
            }
        }
    }
}

配置拦截器

@Configuration
public class DemoWebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**").allowedOrigins("*");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtInterceptor());
    }
}

解析jwt

    /**
     * 解析jwt
     * @param jsonWebToken
     * @param base64Security
     * @return
     */
    public static boolean parseJwt(String jsonWebToken,String base64Security){
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                    .parseClaimsJws(jsonWebToken).getBody();
            return true;
        } catch (ExpiredJwtException  eje) {
            log.error("===== Token过期 =====", eje);
            return false;
        } catch (Exception e){
            log.error("===== token解析异常 =====", e);
            return false;
        }
    }

Shiro

概述

Shiro是一个Java安全框架,它用于认证(authentication),授权(authorization),加密(cryptography),会话管理,适用于小型移动应用到大型企业级应用的各种应用

入门实例

配置文件

# 等号前为用户名,等号后逗号前为用户密码,逗号后都是角色
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# 等号前为角色,等号后为匹配的权限
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

测试

    @Test
    public void test1(){

        //创建安全管理器
        Factory<SecurityManager> factory = new IniSecurityManagerFactory();
        SecurityManager manager = factory.getInstance();
        SecurityUtils.setSecurityManager(manager);

        //创建用户
        Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken("root","secret");

        //进行认证
        try{
            currentUser.login(token);
            System.out.println(currentUser.getPrincipals());
        } catch (UnknownAccountException uae){
            System.out.println("账号不存在");
        } catch (IncorrectCredentialsException ice){
            System.out.println("密码不正确");
        }

        //判断当前用户是否拥有角色
        if(currentUser.hasRole("admin")){
            System.out.println(currentUser.getPrincipals()+"有角色admin");
        }else{
            System.out.println(currentUser.getPrincipals()+"没有角色admin");
        }

        //判断当前用户是否拥有权限
        if(currentUser.isPermitted("lightsaber:perm")){
            System.out.println(currentUser.getPrincipals()+"有权限perm");
        }else{
            System.out.println(currentUser.getPrincipals()+"没有权限perm");
        }
    }

核心概念

  1. Subject:Subject本质上是当前执行用户的安全特定的"视图",Subject不一定表示的是一个人,它表示任何当前与软件交互的对象,所有Subject都绑定到SecurityManager,与Subject交互的所有委托都会交给SecurityManager
  2. SecurityManager:SecurityManager是Shiro体系结构的核心,充当一种“保护伞”对象,协调内部安全组件,它管理着所有的Subject,类似SpringMVC中的前端控制器
  3. Realm:领域充当了Shiro和应用程序安全数据之间的“桥梁”或“连接器”。当需要与安全相关的数据(如用户帐户)进行实际交互以执行身份验证(登录)和授权(访问控制)时,SercurityManager会从域中寻找,它类似DataSource
  4. Shiro应用流程:应用代码通过Subject来进行认证和授权,Subject再将其委托给SercurityManager,事先我们再SercurityManager中注入Realm(Shiro不提供维护用户/权限,而是由开发人员自己注入),然后SercurityManager得到合法的用户及权限进行判断

核心组件

  1. Subject:主体,任何可以与应用交互的对象
  2. SercurityManager:相当于SpringMVC中的前端控制器,Shiro的核心,管理着所有的Subject,控制认证、授权、会话和缓存管理
  3. Authenticator:认证器,负责主体认证,可以自行扩展认证规范
  4. Authorizer:授权器,用来决定用户是否有权限进行相应的操作,控制资源是否能被访问
  5. Realm:数据域,可以有一个或多个,由用户自行实现,Shiro不知道用户和权限数据在哪里以及以什么方式存储,所以Realm一般由自己实现,如缓存实现、jdbc实现等
  6. SessionManager:会话管理器,用户管理Session的生命周期
  7. SessionDao:会话数据访问对象,可以将session保存到数据库
  8. CacheManager:缓存控制器,用来管理如权限等数据的缓存,这些数据一般很少变,所以放到缓存中提高性能
  9. Cryprography:密码模块,用于对密码的加密解密

SpringMVC整合Shiro

认证

  1. 添加依赖
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.7.1</version>
        </dependency>
  1. 自定义Realm
public class DbRealm extends AuthorizingRealm {

    @Autowired
    private UserDao userDao;

    /**
     * 授权
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    /**
     * 认证
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String account = (String) token.getPrincipal();
        User user = userDao.getByAccount(account);
        //判断账号是否存在
        if(user==null){
            throw new UnknownAccountException();
        }
        //进行认证得到认证信息
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getAccount(),user.getPassword(),"realm");
        return info;
    }
}

该realm作为SecurityManager和数据库之间的桥梁,将token中的认证信息与数据库中的信息进行认证,然后得到认证信息

  1. 编写配置文件shiro.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

    <!--  配置安全管理器  -->
    <bean id="dbrealm" class="com.rbac.shiro.DbRealm"></bean>
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="dbrealm"></property>
    </bean>
    <!--  配置shiro过滤器-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="unauthorizedUrl" value="/unauthorized.jsp"/><!-- 没有权限时跳转到指定的页面  -->
        <!--   过滤器链的配置,等号前表示指定的url,后面表示经过的过滤器   -->
        <property name="filterChainDefinitions">
            <value>
                /index.jsp = authc
                /unauthorized.jsp = anon
                /login.jsp = anon
                /user/login = anon
                /logout = logout
                /** = user
            </value>
        </property>
    </bean>
</beans>

shiro的配置文件可以直接配置在spring的配置文件中,但单独配置更方便维护,所以要在applicationContext.xml中将该配置文件引用<import resource="classpath:shiro.xml"/>
通过字段的形式,将Realm和SecurityManager绑定
loginUrl表示登录页面的url,当没有认证时自动跳转到指定的登录页面
filterChainDefinitions指定uri和过滤器的映射,这里的过滤器是shiro默认的过滤器

shiro默认过滤器

配置对应的过滤器功能
anonAnonymousFilter指定的url可以匿名访问(未登录不会被拦截)
authcFormAuthenticationFilter需要登录才能访问,如果没有登录则跳转到登录页面
logoutauthc.LogoutFilter退出过滤器,主要属性redirectUrl为退出时重定向的地址,session将会失效
userUserFilter用户过滤器,当前请求存在经过身份验证的用户才可访问,登录操作不做检查
authcBasicBasicHttpAuthenticationFilter经过httpbaic验证的访问才能通过
rolesRolesAuthorizationFilter验证用户是否有指定角色
permsPermissionsAuthorizationFilter验证用户是否有指定权限
portPortFilter端口过滤,表示访问可以通过的端口

加密/解密

  1. 配置凭证匹配器
    <!--  配置数据域  -->
    <bean id="dbrealm" class="com.rbac.shiro.DbRealm">
        <property name="credentialsMatcher" ref="credentialMatcher"/>
    </bean>
    <!--  配置密码匹配器  -->
    <bean id="credentialMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
        <property name="hashAlgorithmName" value="MD5"/>
        <property name="hashIterations" value="1024"/>
    </bean>
  1. 在数据域认证的时候加上盐值(这里使用用户账号作为盐值)
        //使用账号获取盐值
        ByteSource salt = ByteSource.Util.bytes(account);
        //进行认证得到认证信息
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getAccount(),user.getPassword(),salt,"realm");
        return info;

授权

  1. 配置授权的realm
    @Autowired
    private PermissionDao permissionDao;

    /**
     * 授权,查到当前用户的权限并授予当前用户
     * @param principals
     * @return 授权信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //查到当前用户所有权限
        List<Permission> permissionList = permissionDao.getByAccount((String) principals.getPrimaryPrincipal());
        //将权限授予授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        permissionList.forEach(p->{
            info.addStringPermission(p.getIdentification());
        });
        return info;
    }

realm的授权这一步的目的是为当前登录的用户赋予权限,即查到当前用户的权限并将这些权限放到授权信息中,shiro会拿这些权限来与当前访问需要的权限对比,如果当前访问需要的权限在这些权限中,则允许访问

  1. 配置url与权限的映射map
/***
 * @author shaofan
 * @Description url与过滤器链映射
 * @Date 2021-8-3
 * @Time 17:56
 */
public class PermsMapFactoryBean implements FactoryBean<Map<String,String>> {

    @Autowired
    private PermissionDao permissionDao;

    @Override
    public Map<String, String> getObject() throws Exception {
        List<Permission> permissionList = permissionDao.getByCondition(new Permission());
        Map<String,String> map = new LinkedHashMap<>();//过滤器映射的配置顺序需要指定,使用hashmap则是随机顺序
        map.put("/index.jsp","authc");
        map.put("/login.jsp","anon");
        map.put("/user/login","anon");
        map.put("/logout","logout");
        map.put("/403.jsp","anon");
        permissionList.forEach(p->{
            map.put("/"+p.getLink(),"perms["+p.getIdentification()+"]");
        });
        map.put("/**","user");//对于这个通配符需要最后一个配置,他会直接过滤掉所有的访问,后面配置的过滤器都不会起作用
        return map;
    }

    @Override
    public Class<?> getObjectType() {
        return Map.class;
    }
}
    <!--  配置shiro过滤器-->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="/login.jsp"/>
        <property name="unauthorizedUrl" value="/403.jsp"/><!-- 没有权限时跳转到指定的页面  -->
        <!--   过滤器链的配置,等号前表示指定的url,后面表示经过的过滤器   -->
        <property name="filterChainDefinitionMap" ref="permsMap"/>
    </bean>
    <bean id="permsMap" class="com.rbac.shiro.PermsMapFactoryBean"/>

用户权限是动态数据,所以url和权限过滤器的映射不能写死在xml文件中,需要动态的添加映射,所以使用fileterChainDefinitionMap而非filterChainDefinit,自定义一个Bean,继承自FactoryBean来获得映射map

缓存(整合shiro ehcache)

  1. 导入依赖
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.7.1</version>
        </dependency>
  1. ehcache配置文件ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
         updateCheck="false">
    <!--
       diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
       user.home – 用户主目录
       user.dir  – 用户当前工作目录
       java.io.tmpdir – 默认临时文件路径
     -->
    <diskStore path="java.io.tmpdir/Tmp_EhCache"/>
    <!--
       defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
     -->
    <!--
      name:缓存名称。
      maxElementsInMemory:缓存最大数目
      maxElementsOnDisk:硬盘最大缓存个数。
      eternal:对象是否永久有效,一但设置了,timeout将不起作用。
      overflowToDisk:是否保存到磁盘,当系统当机时
      timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
      timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
      diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
      diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
      diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
      memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
      clearOnFlush:内存数量最大时是否清除。
      memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
      FIFO,first in first out,这个是大家最熟的,先进先出。
      LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
      LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
   -->
    <defaultCache
            eternal="false"
            maxElementsInMemory="10000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="259200"
            memoryStoreEvictionPolicy="LRU"/>

</ehcache>
  1. 配置缓存管理器
    <!--  配置安全管理器  -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="dbrealm"></property>
        <property name="cacheManager" ref="cacheManager"/>
    </bean>
    <!-- 配置缓存管理器 -->
    <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
        <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
    </bean>

配置缓存之后,只有第一次查找权限从数据库中查找,然后读取到内存中,之后每次直接从内存中取得

过滤器解析

理论基础

过滤器是servlet规范中提出的,所有过滤器都是最终实现Filter接口,所有请求想要通过过滤器要找的都是doFilter方法,shiro的默认过滤器自然也是这样,以这一理论点为基础,就可以清晰的分析处shiro的过滤器执行原理

执行原理(以FormAuthenticationFilter为例)

![在这里插入图片描述](https://img-blog.csdnimg.cn/e5775bdb4cd3466d8045fe641a9f2bc4.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ4NDY4Mzgw,size_16,color_FFFFFF,t_70

  1. OncePerRequestFilter实现了doFilter方法,作用是确保请求只经过一次过滤器,并在doFilter方法中调用了doFilterInternal方法,doFilterInternal方法由子类实现,
  2. AdviceFilter子类实现了doFilterInternal方法,在方法中调用了preHandle方法,postHandle方法,使用preHandle方法来判断是否继续执行,postHandle方法由另一子类重写,如果设置了HSTS(http严格安全传输协议)将其写入http头,否则不执行任何操作
  3. PathMatchingFilter子类实现了preHanle方法,使用isFilterChainContinued方法来判断是否继续,isFilterChainContinued使用onPreHandle方法来判断是否继续
  4. AccessContrllerFilter子类重写了onPreHandle方法
    return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
    当isAccessAllowed返回true时,onAccessDenied不执行;返回值即isAccessAllowed方法的返回值,当isAccessAllowed返回false时,返回值为onAccessDenied的返回值
  5. AuthenticationFilter子类实现了isAccessAllowed方法,该方法判断是否通过认证
    (return subject.isAuthenticated() && subject.getPrincipal() != null;)
    AuthenticationFilter子类也实习那了isAccessAllowed方法,该方法在判断是否通过认证的基础上,对于登录请求和拥有权限限定符的请求也会返回true
    return super.isAccessAllowed(request, response, mappedValue) || (!isLoginRequest(request, response) && isPermissive(mappedValue));
  6. FormAuthenticationFilter子类实现了onAccessDenied方法,该方法对登录请求放行并对非登录请求记录请求并做出跳转到登录地址的响应

SpringBoot整合shiro

Springboot配置shiro

  1. 导入依赖
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.7.1</version>
        </dependency>
  1. 配置bean,与springmvc中的shiro.xml配置一样,这里也可以使用shiro.xml,但是官方推荐java的方式
@Configuration
public class ShiroConfig {

    @Bean
    public JwtRealm jwtRealm(){
        return new JwtRealm();
    }

    @Bean
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(jwtRealm());
        return manager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        shiroFilterFactoryBean.setFilterChainDefinitionMap(permsMapFactoryBean().getObject());
        return shiroFilterFactoryBean;
    }

    @Bean
    public PermsMapFactoryBean permsMapFactoryBean(){
        return new PermsMapFactoryBean();
    }
}

使用jwt替代shiro默认的认证

  1. 自定义过滤器实现jwt认证
public class JwtFilter extends AuthenticationFilter {
    @Autowired
    private Audience audience;

    /**
     * 判断是否通过认证
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader("token");
        if(token==null) return false;
        if(JwtTokenUtil.parseJwt(token,audience.getBase64Secret())){
            return true;
        }
        return false;
    }

    /**
     * 认证失败之后调用
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//        if(登录请求)
//            return true;
        System.out.println("认证失败");
        return false;
    }
}
  1. 将自定义的过滤器添加到过滤器链中
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt",jwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(permsMapFactoryBean().getObject());
        return shiroFilterFactoryBean;
    }
    @Bean
    public JwtFilter jwtFilter(){
        return new JwtFilter();
    }

使用jwt替代掉默认的user过滤器

    @Override
    public Map<String, String> getObject() {
        Map<String,String> map = new LinkedHashMap<>();//过滤器映射的配置顺序需要指定,使用hashmap则是随机顺序
        map.put("/user/login","anon");
        map.put("/logout","logout");
        map.put("/**","jwt");
        return map;
    }

自定义shiro默认的授权

  1. 自定义授权过滤器
public class PermsFilter extends PermissionsAuthorizationFilter {
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException{
        //自定义没有权限时候的逻辑,默认是重定向到指定的页面,但在前后端分离开发时,这里逻辑应返回一个json数据
        return false;
    }
}

这里自定义的授权过滤器还是沿用了shiro原有的isAccessAllowed方法,这种情况下使用了jwt代替了原有的授权是不成功的,因为在isAccessAllowed方法对是否认证进行了判断,而没有执行shiro的认证的话判断是不成功的,所以对于登录请求还是要使用shiro认证来进行,即在UserController的login方法调用shiro的认证逻辑

  1. 配置授权过滤器
    @Bean
    public PermsFilter permsFilter(){
        return permsFilter();
    }
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager());
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt",jwtFilter());
        filterMap.put("perms",permsFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(permsMapFactoryBean().getObject());
        return shiroFilterFactoryBean;
    }

注意

按照上述使用shiro原有的认证和授权的isAccessAllowed方法的话,还是用到了cookie和session,这样就不得不面临一个跨域问题,当发送跨域请求的时候如果没有带上cookie的话shiro会认为是不同用户的请求,这样就不能通过授权逻辑中的认证,解决办法:
1. 前端解决跨域问题,通过如apache、ngix这样的web服务器来发送请求
2. 重写isAccessAllowed方法

前端权限控制

问题引入

进行权限管理时,可以通过数据库直接绑定链接、按钮的形式使前端对无权限的功能不展示,但是这样不能阻止地址栏的直接访问

没有登录,访问任何功能直接跳转到登录

判断是否登录,根据判断是否存在token来确定
localStorage:一直存在于浏览器,必须手动清除
sessionStorage:浏览器关闭,数据自动清楚
cookie:根据指定的时间保持数据

导航守卫

如果要判断是否登录,在每一个跳转时都编写一次太麻烦,使用导航守卫就能很好的解决这一问题,全局前置守卫类似于servlet的Filter,能够在导航前进行过滤操作

全局前置守卫

const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {

})

参数

  1. to:即将要进入的路由对象,要跳转到哪里去
  2. from:当前正要离开的路由,从哪里跳转
  3. next:类似于过滤器中的doFilter方法,表示执行跳转

next():进行跳转
next(false):中断当前的跳转
next("/路径"):跳转到一个不同的地址
next(error):如果传入的参数是一个error,导航被终止并将错误交给router.onError回调

没有权限访问的页面,进行提示

对后台查到的权限链接动态增加路由,并使用一个通配符表示的路由添加在最后,这样当访问的时候访问的路由不存在前面可以访问的路由中,就会跳转到最后一个通配符表示的路由

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值