SpringBoot整合Shiro+JWT实现认证及权限校验

序言

本文讲解如何使用SpringBoot整合Shiro框架来实现认证及权限校验,但如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot整合Shiro框架的时候会联合JWT一起搭配使用。

Shiro

Shiroapache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份
认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。

Shiro架构图

Shiro 核心组件

用户、角色、权限之间的关系

  • 用户拥有不同角色
  • 角色拥有不同权限

1、UsernamePasswordTokenShiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token
2、SecurityManagerShiro 的核心部分,负责安全认证和授权。
3、SujectShiro 的一个抽象概念,包含了用户信息。
4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。
5、AuthenticationInfo,用户的角色信息集合,认证时使用。
6、AuthorzationInfo,角色的权限信息集合,授权时使用。
7、DefaultWebSecurityManager,安全管理器,开发者自定义的Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。
8、ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。

JWT

JWT(JSON WEB TOKEN)JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。

JWT的构成
JWT由三部分构成:Header(头部)、Payload(载荷)和Signature(签名)。

1.Header(头) 作用:记录令牌类型、签名算法等 例如:{
    
    “alg":"HS256","type","JWT}

2.Payload(有效载荷)作用:携带一些用户信息 例如{
    
    "userId":"1","username":"mayikt"}

3.Signature(签名)作用:防止Token被篡改、确保安全性 例如 计算出来的签名,一个字符串

项目环境

  • Shiro:1.4.1
  • SpringBoot:2.5.6
  • JDK:1.8

搭建项目

pom依赖

<dependencies>
<!--    lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
<!--        aop-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
<!--        web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
<!--        shiro-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.1</version>
        </dependency>
<!--        shiro-chcache-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>1.4.1</version>
        </dependency>
<!--        jwt-->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.2.0</version>
        </dependency>
<!--        fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.15</version>
        </dependency>
    </dependencies>

JWTUtil

public class JWTUtils {
    
    

    /**
     * 过期时间
     */
    private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000;

    /**
     * 校验
     * @param token
     * @param username
     * @param password
     * @return
     */
    public static boolean verify(String token, String username, String password) {
    
    
        try {
    
    
            Algorithm algorithm = Algorithm.HMAC256(password);
            JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception e) {
    
    
            return false;
        }
    }

    /**
     * 颁发令牌
     * @param username
     * @param password
     * @return
     */
    public static String sign(String username, String password) {
    
    
        try {
    
    
            //设置过期时间:获取当前时间+过期时间(毫秒)
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //设置签名的加密算法:HMAC256
            Algorithm algorithm = Algorithm.HMAC256(password);
            // 附带username信息
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
    
    
            return null;
        }
    }

    /**
     * 获取用户名
     * @param token
     * @return
     */
    public static String getUsername(String token) {
    
    
        if (token == null || "".equals(token)) {
    
    
            return null;
        }
        try {
    
    
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
    
    
            return null;
        }
    }


}

JWTToken

JWTToken是定义的一个Token类,继承了AuthenticationToken类,实现getPrincipalgetCredentials方法,(这两个方法本来是用于获取token中的信息,和识别token的,但JWTUtils已经为我们提供了这样的方法,所以这两个方法对于JWTToken没有意义)。用于将客户端传来的Token进行封装,便于Realm识别Token类型,进行认证和授权。

public class JWTToken implements AuthenticationToken {
    
    

    /**
     * 密钥
     */
    private String token;

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

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

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

}

JWTFilter过滤器

因为 JWT 的整合,我们需要⾃定义⾃⼰的过滤器 JWTFilterJWTFilter 继承了 BasicHttpAuthenticationFilter,并部分原⽅法进⾏了重写。

public class JWTFilter extends BasicHttpAuthenticationFilter {
    
    

    /**
     * Header中的Token标志
     */
    private static String LOGIN_SIGN = "Authorization";

    /**
     * 是否允许访问
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    
    
        if (isLoginAttempt(request, response)) {
    
    
            try {
    
    
                executeLogin(request, response);
            } catch (Exception e) {
    
    
                if (e instanceof AuthorizationException) {
    
    
                    throw new AuthorizationException("访问资源权限不足!");
                } else {
    
    
                    //token 异常 认证失败
                    throw new AuthenticationException("token 异常 认证失败");
                }
            }
        }
        return true;
    }

    /**
     * 是登录尝试
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        //判断是否是登录请求
        String authorization = req.getHeader(LOGIN_SIGN);
        return authorization != null;
    }

    /**
     * 执行登录
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    
    
        HttpServletRequest req = (HttpServletRequest) request;
        String header = req.getHeader(LOGIN_SIGN);
        JWTToken token = new JWTToken(header);
        //提交给realm进⾏登⼊,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);

        return true;
    }
}

自定义ShiroRealm

自定义的Realm对象,该对象继承于AuthorizingRealm,实现了Shiro具体认证和授权的方法。

  • doGetAuthenticationInfo方法用于->认证:校验帐号和密码
  • doGetAuthorizationInfo方法用于->授权:授予角色和权限

另外需要注意:
必须要重写supports方法,因为是自己定义的Tokenshiro无法识别,需要修改Realm中的supports方法,使 shiro 支持自定义Token

public class ShiroRealm extends AuthorizingRealm {
    
    

    @Autowired
    private RoleService roleService;
    @Autowired
    private MenuService menuService;
    @Autowired
    private UserService userService;

    /**
     * 因为是自己定义的Token,shiro无法识别,需要修改Realm中的supports方法,使 shiro 支持自定义token。
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
    
    
        return token instanceof JWTToken;
    }

    /**
     * 认证:校验帐号和密码
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    
    
        String token = (String) authenticationToken.getCredentials();
        //从token中获取用户名
        String username = JWTUtils.getUsername(token);
        //获取数据库中存取的用户,密码是加密后的
        User user = userService.selectByUserName(username);
        if (user != null) {
    
    
            // 密码验证
            if (!JWTUtils.verify(token, username, user.getPassword())) {
    
    
                // 密码不正确
                throw new IncorrectCredentialsException();
            }
            return new SimpleAuthenticationInfo(token, token, getName());
        } else {
    
    
            throw new UnknownAccountException();
        }
    }

    /**
     * 授权:授予角色和权限
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    
    
        //获取用户名
        String userName = JWTUtils.getUsername(principals.toString());
        //根据用户名查询用户
        User user = userService.selectByUserName(userName);
        //实例化一个授权信息
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        if (user != null) {
    
    
            //赋予角色
            List<Role> roles = roleService.selectRoleByUserId(user.getId());
            for (Role role : roles) {
    
    
                //将角色添加到授权信息中
                info.addRole(role.getRoleKey());
            }
            //赋予资源
            List<Menu> permissions = menuService.selectPermsByUserId(user.getId());
            for (Menu permission : permissions) {
    
    
                //将权限添加授权信息中
                info.addStringPermission(permission.getPerms());
            }
        }
        return info;
    }

}

ShiroConfig

ShiroConfig用于进行Shiro的相关配置,主要包括ShiroFilterFactoryBeanDefaultWebSecurityManagerRealm的配置。

@Configuration
public class ShiroConfig {
    
    

    /**
     * 生命周期处理器
     * @return
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    
    
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 加密方式
     * @return
     */
    @Bean(name = "hashedCredentialsMatcher")
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
    
    
        // 散列凭证匹配器
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        // 设置哈希算法名称,这里使用MD5算法
        credentialsMatcher.setHashAlgorithmName("MD5");
        // 设置哈希迭代,这里迭代2次,相当于 md5(md5(""))
        credentialsMatcher.setHashIterations(2);
        // 设置存储的凭据16进制编码,需要和生成密码时的一样,默认是 Base64
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**
     * 自定义Realm
     * @param cacheManager
     * @return
     */
    @Bean(name = "shiroRealm")
    @DependsOn("lifecycleBeanPostProcessor")
    public ShiroRealm shiroRealm(EhCacheManager cacheManager) {
    
    
        ShiroRealm realm = new ShiroRealm();
        realm.setCacheManager(cacheManager);
        return realm;
    }

    /**
     * 缓存管理器
     * @return
     */
    @Bean(name = "ehCacheManager")
    @DependsOn("lifecycleBeanPostProcessor")
    public EhCacheManager ehCacheManager() {
    
    
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return ehCacheManager;
    }

    /**
     * 安全管理器
     * @param shiroRealm
     * @return
     */
    @Bean(name = "securityManager")
    public DefaultWebSecurityManager securityManager(ShiroRealm shiroRealm) {
    
    
        // 实例化会话管理器
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置缓存管理器
        securityManager.setCacheManager(ehCacheManager());

        /**
         * 关闭shiro自带的session
         * 详情见文档: http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(evaluator);

        securityManager.setSubjectDAO(subjectDAO);

        // 设置自定义Realm
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }

    /**
     * 过滤工厂
     * @param securityManager
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filters = new LinkedHashMap<>();
        filters.put("jwt", new JWTFilter());

        factoryBean.setFilters(filters);

        Map<String, String> filterChainDefinitionManager = new LinkedHashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterChainDefinitionManager.put("/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterChainDefinitionManager);
        return factoryBean;
    }

    /**
     * 自动代理配置
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        proxyCreator.setProxyTargetClass(true);
        return proxyCreator;
    }

    /**
     * 开启注解支持
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor aASA = new AuthorizationAttributeSourceAdvisor();
        aASA.setSecurityManager(securityManager);
        return aASA;
    }
}

这里开启注解支持需要添加DefaultAdvisorAutoProxyCreator(可选)AuthorizationAttributeSourceAdvisorDefaultAdvisorAutoProxyCreator也可以选择不加,这里加是因为防止重复代理和可能引起代理出错的问题

认证和授权规则

认证过滤器

  • anon:无需认证。
  • authc:必须认证。
  • authcBasic:需要通过 HTTPBasic 认证。
    授权过滤器
  • perms:必须拥有某个权限才能访问。
  • role:必须拥有某个角色才能访问。
  • port:请求的端口必须是指定值才可以。
  • rest:请求必须基于 RESTfulPOSTPUTGETDELETE
  • ssl:必须是安全的 URL 请求,协议 HTTPS

自定义异常处理

使用@RestControllerAdvice捕获Controller层抛出的异常。

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    

    @ExceptionHandler(value = Exception.class)
    @ResponseStatus(HttpStatus.OK)
    public Object errorHandler(Exception e, HttpServletRequest httpServletRequest) {
    
    
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("success", false);
        if (e instanceof NoHandlerFoundException) {
    
    
            jsonObject.put("code", 404);
            jsonObject.put("msg", "找不到请求资源");
        } else if (e instanceof MissingServletRequestParameterException) {
    
    
            jsonObject.put("code", -200);
            jsonObject.put("msg", "缺少参数");
        } else if (e instanceof UnauthenticatedException) {
    
    
            jsonObject.put("code", 401);
            jsonObject.put("msg", "用户未登录,请登录");
        } else if (e instanceof AuthorizationException) {
    
    
            jsonObject.put("code", 402);
            jsonObject.put("msg", "权限不足");
        } else if (e instanceof AuthenticationException) {
    
    
            jsonObject.put("code", 403);
            jsonObject.put("msg", "帐号密码错误,请重新登录");
        } else if (e instanceof MaxUploadSizeExceededException) {
    
    
            jsonObject.put("code", 240);
            jsonObject.put("msg", "文件上传超出大小限制");
        } else if (e instanceof SQLException) {
    
    
            jsonObject.put("code", 250);
            jsonObject.put("msg", "数据库操作失败");
        } else if (e instanceof SocketTimeoutException) {
    
    
            jsonObject.put("code", 260);
            jsonObject.put("msg", "服务连接超时");
        } else if (e instanceof SocketException) {
    
    
            jsonObject.put("code", 240);
            jsonObject.put("msg", "服务连接失败");
        } else if (e instanceof IOException) {
    
    
            jsonObject.put("code", 500);
            jsonObject.put("msg", "系统错误");
            e.printStackTrace();
        } else {
    
    
            jsonObject.put("code", 500);
            jsonObject.put("msg", "系统错误");
            e.printStackTrace();
        }
        return jsonObject;
    }

}

定义User

用户实体类

@Data
public class User {
    
    

    // ID
    private Integer id;
    // 用户名
    private String userName;
    // 密码
    private String password;
    // 盐值
    private String salt;

}

定义Role

角色实体类

@Data
public class Role {
    
    
    
    // ID
    private Integer id;
    // 角色字符串
    private String roleKey;

}

Menu

菜单实体类

@Data
public class Menu {
    
    
    
    // ID
    private Integer id;
    // 权限字符串
    private String perms;

}

UserService

接口

public interface UserService {
    
    

    User selectByUserName(String username);

}

实现类

@Service
public class UserServiceImpl implements UserService {
    
    

    @Override
    public User selectByUserName(String username) {
    
    
        User user = new User();
        user.setUserName(username);
        user.setPassword("dc483e80a7a0bd9ef71d8cf973673924");

        return user;
    }
}

这里为了方便演示把密码写死了

RoleService

接口

public interface RoleService {
    
    

    List<Role> selectRoleByUserId(Integer id);

}

实现类

@Service
public class RoleServiceImpl implements RoleService {
    
    
    @Override
    public List<Role> selectRoleByUserId(Integer id) {
    
    
        List<Role> roles = new ArrayList<>();

        Role admin = new Role();
        admin.setRoleKey("admin");

        roles.add(admin);
        return roles;
    }
}

这里为了方便演示把角色写死了

MenuService

接口

public interface MenuService {
    
    

    List<Menu> selectPermsByUserId(Integer id);

}

实现类

@Service
public class MenuServiceImpl implements MenuService {
    
    
    @Override
    public List<Menu> selectPermsByUserId(Integer id) {
    
    
        Menu saveUser = new Menu();
        saveUser.setPerms("sys:user:save");

        List<Menu> menus = new ArrayList<>();
        menus.add(saveUser);
        
        return menus;
    }
}

这里为了方便演示把权限写死了

统一结果集工具

public class R extends HashMap<String, Object> {
    
    

    public static final int SUCCESS_CODE = 200;

    private R() {
    
    

    }

    public static R build(int code, String msg) {
    
    
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public static R success() {
    
    
        R r = new R();
        r.put("code", 200);
        r.put("msg", "success");
        return r;
    }

    public static R success(String key, Object value) {
    
    
        R r = R.success();
        r.put(key, value);
        return r;
    }

    public static R failure() {
    
    
        R r = new R();
        r.put("code", 500);
        r.put("msg", "操作失败");
        return r;
    }

    public static R failure(int code, String msg) {
    
    
        R r = new R();
        r.put("code", code);
        r.put("msg", msg);
        return r;
    }

    public R add(String key, Object value) {
    
    
        super.put(key, value);
        return this;
    }

    public R delete(String key) {
    
    
        if (key != null && (!"code".equals(key) || !"msg".equals(key))) {
    
    
            this.remove(key);
        }
        return this;
    }

    public int getCode() {
    
    
        return (int) this.get("code");
    }

    public void setCode(int code) {
    
    
        this.put("code", code);
    }

    public String getMsg() {
    
    
        return (String) this.get("msg");
    }

    public void setMsg(String msg) {
    
    
        this.put("msg", msg);
    }
}

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 path="java.io.tmpdir/Tmp_EhCache"/>
    <defaultCache
            eternal="false"
            maxElementsInMemory="1000"
            overflowToDisk="false"
            diskPersistent="false"
            timeToIdleSeconds="0"
            timeToLiveSeconds="600"
            memoryStoreEvictionPolicy="LRU"/>
    <cache name="user"
           eternal="false"
           maxElementsInMemory="10000"
           overflowToDisk="false"
           diskPersistent="false"
           timeToIdleSeconds="0"
           timeToLiveSeconds="0"
           memoryStoreEvictionPolicy="LFU"/>
</ehcache>

配置详解

<!--     
       name:缓存名称。     
       maxElementsInMemory:缓存最大个数。     
       eternal:对象是否永久有效,一但设置了,timeout将不起作用。     
       timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。     
       timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。     
       overflowToDisk:当内存中对象数量达到maxElementsInMemory时,Ehcache将会对象写到磁盘中。     
       diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。     
       maxElementsOnDisk:硬盘最大缓存个数。     
       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.     
       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。     
       memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。     
       clearOnFlush:内存数量最大时是否清除。     
    -->     

定义登录接口

@RestController
public class LoginController {
    
    

    @Autowired
    private UserService service;

    @RequestMapping("/login")
    public R login(String userName, String password) {
    
    

        if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
    
    
            return R.failure(590, "帐号或者密码不能为空");
        }
        User user = service.selectByUserName(userName);
        if (user == null) {
    
    
            return R.failure(590, "帐号不存在");
        }
        if (!encrypt(userName, password).equals(user.getPassword())) {
    
    
            return R.failure(590, "密码错误");
        }

        //token生成采用加密后的密码,这个和realm中的校验必须一致
        return R.success("token", JWTUtils.sign(userName, user.getPassword()));
    }

}

定义用户控制器

@RestController
@RequestMapping("user")
public class UserController {
    
    

    @RequestMapping("save")
    @RequiresPermissions("sys:user:save")
    public R save() {
    
    
        return R.success();
    }

    @RequestMapping("delete")
    @RequiresPermissions("sys:user:delete")
    public R delete() {
    
    
        return R.success();
    }

}

整个层级效果

登录测试

错误演示

正确演示

鉴权演示

save接口和delete接口分别需要sys:user:save权限和sys:user:delete权限才能访问。

这里为了方便演示,查询权限的业务类写死了,任意用户都只有save权限

由于查询权限业务写死,任意用户都有save权限,而save接口刚好需要save权限才能访问,所以我们可以正常访问。

delete接口就需要delete权限,而我们的业务写死了,只有save权限,这个时候访问delete接口就没有权限访问。

Shiro注解权限控制

  1. RequiresAuthentication:使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证(一般指需要登录)。
  2. RequiresGuest:使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。
  3. RequiresPermissions:当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
  4. RequiresRoles:当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常(下面列出解决办法)。
  5. RequiresUser:当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。

数据表脚本

如果想模拟真实业务通过数据库查询用户信息以及角色和权限,以下为对应实体类的数据表脚本。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for menu
-- ----------------------------
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `perms` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of menu
-- ----------------------------
INSERT INTO `menu` VALUES (1, 'sys:user:save');
INSERT INTO `menu` VALUES (2, 'sys:user:delete');

-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role`  (
  `id` int NOT NULL,
  `role_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色字符串',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'root');
INSERT INTO `role` VALUES (2, 'admin');

-- ----------------------------
-- Table structure for role_menu
-- ----------------------------
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `rid` bigint NULL DEFAULT NULL COMMENT '角色ID',
  `mid` bigint NULL DEFAULT NULL COMMENT '权限ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色和菜单关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of role_menu
-- ----------------------------
INSERT INTO `role_menu` VALUES (1, 1, 1);

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
  `salt` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '盐值',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'admin', 'dc483e80a7a0bd9ef71d8cf973673924', NULL);

-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `uid` bigint NULL DEFAULT NULL COMMENT '用户ID',
  `rid` bigint NULL DEFAULT NULL COMMENT '角色ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户和角色关联表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);

SET FOREIGN_KEY_CHECKS = 1;

教程到此结束~

本文教程案例下载:https://download.csdn.net/download/qq_31762741/85384639

猜你喜欢

转载自blog.csdn.net/qq_31762741/article/details/124784627