37000 字 + 代码,艿艿肝的 Shiro 从入门到实战,直接收藏吃灰!

国央企创新负责人如何通过平台提升技术需求挖掘与资源配置的精准性?.docx 国央企创新负责人如何通过平台提升技术需求挖掘与资源配置的精准性? 立即下载

点击上方“芋道源码”,选择“设为星标”

管她前浪,还是后浪?

能浪的浪,才是好浪!

每天 8:55 更新文章,每天掉亿点点头发...

源码精品专栏

 

大家好,我是艿艿,一个让你秃头的小胖子。。。

最近状态有点小好,抠脚一算, https://github.com/YunaiV/SpringBoot-Labs 仓库的 Spring Boot、Spring Cloud、Dubbo 的示例代码,竟然要破 60000 行了 = = 默默撸了 2 年了要~

“

摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Shiro/ 「芋道源码」欢迎转载,保留摘要,谢谢!

  • 1. 概述

  • 2. 快速入门

  • 3. Shiro 注解

  • 4. 项目实战

  • 666. 彩蛋


“

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labs 的 lab-33 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

“

艿艿:本文是《芋道 Spring Boot 安全框架 Spring Security 入门》 的姊妹篇,所以开头就“重复”再来一遍,嘿嘿。

基本上,在所有的开发的系统中,都必须做认证(authentication)和授权(authorization),以保证系统的安全性。???? 考虑到很多胖友对认证和授权有点分不清楚,艿艿这里引用一个网上有趣的例子:

“

FROM 《认证 (authentication) 和授权 (authorization) 的区别》

  • authentication [ɔ,θɛntɪ'keʃən] 认证

  • authorization [,ɔθərɪ'zeʃən] 授权

以打飞机举例子:

  • 【认证】你要登机,你需要出示你的 passport 和 ticket,passport 是为了证明你张三确实是你张三,这就是 authentication。

  • 【授权】而机票是为了证明你张三确实买了票可以上飞机,这就是 authorization。

以论坛举例子:

  • 【认证】你要登陆论坛,输入用户名张三,密码 1234,密码正确,证明你张三确实是张三,这就是 authentication。

  • 【授权】再一 check 用户张三是个版主,所以有权限加精删别人帖,这就是 authorization 。

所以简单来说:认证解决“你是谁”的问题,授权解决“你能做什么”的问题。另外,在推荐阅读下《认证、授权、鉴权和权限控制》 文章,更加详细明确。

在 Java 生态中,目前有 Spring Security 和 Apache Shiro 两个安全框架,可以完成认证和授权的功能。本文,我们再来学习下 Apache Shiro 。其官方对自己介绍如下:

“

FROM 《Apache Shiro 官网》

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management.
Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,它可以提供身份验证、授权、加密和会话管理的功能。

With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
通过 Shiro 易于理解的 API ,你可以快速、轻松地保护任何应用程序 —— 从最小的移动端应用程序到大型的的 Web 和企业级应用程序。

更多关于 Shiro 的介绍,胖友可以自行阅读《Apache Shiro 1.2.x 参考手册》 ,虽然目前 Shiro 截止到目前已经发布到 1.4.2 版本,但是该手册依然很有参考价值。

2. 快速入门

“

示例代码对应仓库:lab-33-shiro-demo 。

在本小节中,我们来对 Shiro 进行快速的入门,实现一个最小化的使用示例。

2.1 引入依赖

在 pom.xml 文件中,引入相关依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>lab-33-shiro-demo</artifactId>

    <dependencies>
        <!-- 实现对 Spring MVC 的自动化配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 实现对 Shiro 的自动化配置 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.4.2</version>
        </dependency>
    </dependencies>

</project>
  • 具体每个依赖的作用,胖友自己认真看下艿艿添加的所有注释噢。

不过实际上,shiro-spring-boot-starter 依赖对 Shiro 的自动化配置基本没啥用,需要自己来主动实现对 Shiro 的配置。

2.2 ShiroConfig

在 cn.iocoder.springboot.lab01.shirodemo.config 包下,创建 ShiroConfig 抽象类,实现 Shiro 的自定义配置。代码如下:

// ShiroConfig.java

@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() { /**省略代码**/ }

    @Bean
    public DefaultWebSecurityManager securityManager() { /**省略代码**/ }

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() { /**省略代码**/ }

}
  • 一共有三个 Bean 的配置,我们逐个来看看。

2.2.1 Realm

我们先来看看 Realm 的定义。

“

《Apache Shiro 1.2.x 参考手册 —— Realms》

Realm 是可以访问程序特定的安全数据如用户、角色、权限等的一个组件。Realm 会将这些程序特定的安全数据转换成一种 Shiro 可以理解的形式,Shiro 就可以依次提供容易理解的 Subject 程序API而不管有多少数据源或者程序中你的数据如何组织。

Realm 通常和数据源是一对一的对应关系,如关系数据库,LDAP 目录,文件系统,或其他类似资源。因此,Realm 接口的实现使用数据源特定的API 来展示授权数据(角色,权限等),如JDBC,文件IO,Hibernate 或JPA,或其他数据访问API。

Realm 实质上就是一个特定安全的 DAO

因为这些数据源大多通常存储身份验证数据(如密码的凭证)以及授权数据(如角色或权限),每个 Shiro Realm 能够执行身份验证和授权操作。

  • ???? 好长一段描述,抓重点,最后一句的“身份验证”(认证)和“授权”,这个就是 Realm 的职责。

Realm 整体的类图如下:

  • Realm 接口,主要定义了**“认证”**方法。代码如下:

    // Realm.java
    
    AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
    
  • AuthorizingRealm 抽象类,主要额外定义了授权方法。代码如下:

    // AuthorizingRealm.java
    
    protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
    
    • AuthorizingRealm 同时实现了 Authorizer 接口,提供判断经过认证过的 Subject 是否具有指定的角色、权限的方法。

  • 从图中我们可以看出,Shiro 提供了多种 AuthorizingRealm 的实现类,提供从不同的数据源获取数据。不过一般在项目中,我们会自定义实现 AuthorizingRealm ,从自己定义的表结构中读取用户、角色、权限等数据。虽然说,Shiro 提供了 JdbcRealm 可以访问数据库,但是它的表结构是固定的,所说我们才要自定义定义实现 AuthorizingRealm 。

本示例中,在 #realm() 方法,我们创建了 SimpleAccountRealm Bean 对象。代码如下:

// ShiroConfig.java

@Bean
public Realm realm() {
    // 创建 SimpleAccountRealm 对象
    SimpleAccountRealm realm = new SimpleAccountRealm();
    // 添加两个用户。参数分别是 username、password、roles 。
    realm.addAccount("admin", "admin", "ADMIN");
    realm.addAccount("normal", "normal", "NORMAL");
    return realm;
}
  • SimpleAccountRealm 是使用内存作为数据源,我们可以手动往里面添加用户、角色、权限等数据。???? 毕竟作为一个示例,艿艿不想引入数据库,增加复杂性。不过我们在「3. 项目实战」中,我们会看到我们使用自定义的 AuthorizingRealm 实现类。

  • 在该方法里,我们添加了「admin/admin」和「normal/normal」两个用户,分别对应 ADMIN 和 NORMAL 角色。

2.2.2 SecurityManager

我们再来看看 SecurityManager 的定义。

“

《Apache Shiro 1.2.x 参考手册 —— Session Management》

SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。

本示例中,在 #securityManager() 方法,我们创建了 DefaultWebSecurityManager Bean 对象。代码如下:

// ShiroConfig.java

@Bean
public DefaultWebSecurityManager securityManager() {
    // 创建 DefaultWebSecurityManager 对象
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 设置其使用的 Realm
    securityManager.setRealm(this.realm());
    return securityManager;
}
  • 不用特别去纠结 SecurityManager ,创建好 DefaultWebSecurityManager Bean 就完事了~等后续我们入门完 Shiro 之后,胖友可以在慢慢细细去研究。

2.2.3 ShiroFilter

通过 AbstractShiroFilter 过滤器,实现对请求的拦截,从而实现 Shiro 的功能。AbstractShiroFilter 整体的类图如下:

本示例中,在 #shiroFilterFactoryBean() 方法,我们创建了 ShiroFilterFactoryBean Bean 对象。代码如下:

// ShiroConfig.java

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
    // <1> 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
    ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();

    // <2> 设置 SecurityManager
    filterFactoryBean.setSecurityManager(this.securityManager());

    // <3> 设置 URL 们
    filterFactoryBean.setLoginUrl("/login"); // 登陆 URL
    filterFactoryBean.setSuccessUrl("/login_success"); // 登陆成功 URL
    filterFactoryBean.setUnauthorizedUrl("/unauthorized"); // 无权限 URL

    // <4> 设置 URL 的权限配置
    filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap());

    return filterFactoryBean;
}
  • <1> 处,创建 ShiroFilterFactoryBean 对象,用于创建 SpringShiroFilter 过滤器。

  • <2> 处,设置其 SecurityManager 属性。

  • <3> 处,设置各种 URL 。

    • #setLoginUrl(String loginUrl) 方法,设置登陆 URL 。在 Shiro 中,约定 GET loginUrl 为登陆页面,POST loginUrl 为登陆请求。

    • #setSuccessUrl(String successUrl) 方法,设置登陆成功 URL 。在登陆成功时,会重定向到该 URL 上。

    • #etUnauthorizedUrl(String unauthorizedUrl) 方法,设置无权限的 URL 。在请求校验权限不通过时,会重定向到该 URL 上。

    • 上述的 URL 对应的接口,都需要我们自己来实现。具体可见「2.3 SecurityController」小节。

  • <4> 处,调用 #setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) 方法,设置 URL 的权限配置。

在看 #filterChainDefinitionMap() 方法的具体 URL 的权限配置之前,我们先来了解下 Shiro 内置的过滤器们。在 Shiro DefaultFilter 枚举类中,枚举了这些过滤器,以及其配置名。整理表格如下:

Filter NameClass
anonorg.apache.shiro.web.filter.authc.AnonymousFilter
authcorg.apache.shiro.web.filter.authc.FormAuthenticationFilter
authcBasicorg.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
logoutorg.apache.shiro.web.filter.authc.LogoutFilter
noSessionCreationorg.apache.shiro.web.filter.session.NoSessionCreationFilter
permsorg.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
portorg.apache.shiro.web.filter.authz.PortFilter
restorg.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
rolesorg.apache.shiro.web.filter.authz.RolesAuthorizationFilter
sslorg.apache.shiro.web.filter.authz.SslFilter
userorg.apache.shiro.web.filter.authc.UserFilter

比较常用的过来器有:

  • anon :AnonymousFilter :允许匿名访问,即无需登陆。

  • authc :FormAuthenticationFilter :需要经过认证的用户,才可以访问。如果是匿名用户,则根据 URL 不同,会有不同的处理:

    • 如果拦截的 URL 是 GET loginUrl 登陆页面,则进行该请求,跳转到登陆页面。

    • 如果拦截的 URL 是 POST loginUrl 登陆请求,则基于请求表单的 username、password 进行认证。认证通过后,默认重定向到 GET loginSuccessUrl 地址。

    • 如果拦截的 URL 是其它 URL 时,则记录该 URL 到 Session 中。在用户登陆成功后,重定向到该 URL 上。

  • logout :LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向到 GET loginUrl 登陆页面。

  • roles :RolesAuthorizationFilter :拥有指定角色的用户可访问。

  • perms :PermissionsAuthorizationFilter :拥有指定权限的用户可以访问。

下面,让我们回过头来看看 #filterChainDefinitionMap() 方法的具体 URL 的权限配置。代码如下:

// ShiroConfig.java

private Map<String, String> filterChainDefinitionMap() {
    Map<String, String> filterMap = new LinkedHashMap<>(); // 注意要使用有序的 LinkedHashMap ,顺序匹配
    filterMap.put("/test/echo", "anon"); // 允许匿名访问
    filterMap.put("/test/admin", "roles[ADMIN]"); // 需要 ADMIN 角色
    filterMap.put("/test/normal", "roles[NORMAL]"); // 需要 NORMAL 角色
    filterMap.put("/logout", "logout"); // 退出
    filterMap.put("/**", "authc"); // 默认剩余的 URL ,需要经过认证
    return filterMap;
}
  • /test/echo :我们设置为 anon ,允许匿名访问。

  • /test/admin 和 /test/normal :我们设置为 roles[...] ,需要指定角色的用户可以访问。其中 ... 处为需要添加的角色名。

  • /logout :我们设置为 logout ,实现退出操作。

  • /** :剩余的 URL ,我们设置为 authc ,需要登陆的用户才可以访问。同时,对于 loginUrl 需要执行登陆相关的拦截。

另外,这里在补充一点,请求在 ShiroFilter 拦截之后,会根据该请求的情况,匹配到配置的内置的 Shiro Filter 们,逐个进行处理。也就是说,ShiroFilter 实际内部有一个由 内置的 Shiro Filter 组成的过滤器链。

至此,我们已经完成了 Shiro 的自定义配置。虽然篇幅有点长,但是可以等我们跑完整个「2. 快速入门」的示例之后,胖友再自己回过头来看看,会发现还是比较清晰明了的。

2.3 SecurityController

在 cn.iocoder.springboot.lab01.shirodemo.controller 包路径下,创建 SecurityController 类,提供登陆、登陆成功等接口。代码如下:

// SecurityController.java

@Controller
@RequestMapping("/")
public class SecurityController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @GetMapping("/login")
    public String loginPage() { /**省略代码**/ }

    @ResponseBody
    @PostMapping("/login")
    public String login(HttpServletRequest request) { /**省略代码**/ }

    @ResponseBody
    @GetMapping("/login_success")
    public String loginSuccess() { /**省略代码**/ }

    @ResponseBody
    @GetMapping("/unauthorized")
    public String unauthorized() { /**省略代码**/ }

}
  • 一共有 4 个接口,我们逐个来看看。

2.3.1 登陆页面

GET /login 地址,跳转登陆页面。代码如下:

// SecurityController.java

@GetMapping("/login")
public String loginPage() {
    return "login.html";
}
  • 返回 resources/static/login.html 静态页面。代码如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>登陆页面</title>
    </head>
    <body>
        <form action="/login" method="post">
            用户名:<input type="text" name="username"/> <br />
            密码:<input type="password" name="password"/> <br />
            <input type="submit" value="登录"/>
        </form>
    </body>
    </html>
    
    • 一个简单的登陆的表单,POST 提交登陆请求到 /login 地址上。

2.3.2 登陆请求

对于登陆请求,会被我们配置的 Shiro FormAuthenticationFilter 过滤器进行拦截,进行用户的身份认证。整个过程如下:

  • FormAuthenticationFilter 解析请求的 username、password 参数,创建 UsernamePasswordToken 对象。

  • 然后,调用 SecurityManager 的 #login(Subject subject, AuthenticationToken authenticationToken) 方法,执行登陆操作,进行“身份验证”(认证)。

  • 在这内部中,调用 Realm 的 #getAuthenticationInfo(AuthenticationToken token) 方法,进行认证。此时,根据认证的是否成功,会有不同的处理:

    • 如果认证通过,则 FormAuthenticationFilter 会将请求重定向到 GET loginSuccess 地址上。

    • 【重要】如果认证失败,则会将认证失败的原因设置到请求的 attributes 中,后续该请求会继续请求到 POST login 地址上。这样,在 POST loginUrl 地址上,我们可以从 attributes 中获取到失败的原因,提示给用户。

所以,POST loginUrl 的目的,实际是为了处理认真失败的情况。也因此,POST login 地址,实现代码如下:

// SecurityController.java

@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request) {
    // <1> 判断是否已经登陆
    Subject subject = SecurityUtils.getSubject();
    if (subject.getPrincipal() != null) {
        return "你已经登陆账号:" + subject.getPrincipal();
    }

    // <2> 获得登陆失败的原因
    String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
    // 翻译成人类看的懂的提示
    String msg = "";
    if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
        msg = "账号不存在";
    } else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
        msg = "密码不正确";
    } else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
        msg = "账号被锁定";
    } else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
        msg = "账号已过期";
    } else {
        msg = "未知";
        logger.error("[login][未知登陆错误:{}]", shiroLoginFailure);
    }
    return "登陆失败,原因:" + msg;
}
  • <1> 处,对于已经登陆成功的用户,如果我们再次请求 POST loginUrl 地址,依然会直接跳转到该地址上。此处,我们是提供用户已经的登陆。???? 可能有部分胖友会希望重新进行一次登陆的逻辑,那么就需要重写 FormAuthenticationFilter 过滤器。

  • <2> 处,从请求的 attributes 中,获取 FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME 对应的值,即登陆失败的原因。从代码中,我们可以看出,失败原因为异常的全类名,我们需要进行翻译成人类可读的提示。

2.3.3 登陆成功

GET login_success 地址,登陆成功响应。代码如下:

// SecurityController.java

@ResponseBody
@GetMapping("/login_success")
public String loginSuccess() {
    return "登陆成功";
}
  • 如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,用户、角色、权限等等信息。

  • 如果非 AJAX 请求的情况下,重定向到登陆成功的页面。例如说,管理后台的 HOME 页面。

2.3.4 未授权

GET unauthorized 地址,未授权响应。代码如下:

// SecurityController.java

@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized() {
    return "你没有权限";
}
  • 如果是 AJAX 请求的情况下,我们可以返回 JSON 字符串。例如说,你没有权限。

  • 如果非 AJAX 请求的情况下,重定向到登陆成功的页面。例如说,未授权的页面。

2.4 TestController

在 cn.iocoder.springboot.lab01.shirodemo.controller 包路径下,创建 TestController 类,提供测试 API 接口。代码如下:

// TestController.java

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/demo")
    public String demo() {
        return "示例返回";
    }

    @GetMapping("/home")
    public String home() {
        return "我是首页";
    }

    @GetMapping("/admin")
    public String admin() {
        return "我是管理员";
    }

    @GetMapping("/normal")
    public String normal() {
        return "我是普通用户";
    }

}
  • 对于 /test/demo 接口,直接访问,无需登陆。

  • 对于 /test/home 接口,无法直接访问,需要进行登陆。

  • 对于 /test/admin 接口,需要登陆「admin/admin」用户,因为需要 ADMIN 角色。

  • 对于 /test/normal 接口,需要登陆「user/user」用户,因为需要 USER 角色。

胖友可以按照如上的说明,进行各种测试。例如说,登陆「user/user」用户后,去访问 /test/admin 接口,会返回无权限的提示~

2.5 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

// Application.java

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
    
}

至此,我们已经完成了 Shiro 的入门。美滋滋,胖友可以自己多多测试一下。

3. Shiro 注解

在 Shiro 中,提供了如下五个注解,可以直接添加在 SpringMVC 的 URL 对应的方法上,实现权限配置。下面,我们来分别看看。

3.1 @RequiresGuest

@RequiresGuest 注解,和 anon 等价。

3.2 @RequiresAuthentication

@RequiresAuthentication 注解,和 authc 等价。

3.3 @RequiresUser

@RequiresUser 注解,和 user 等价,要求必须登陆。

3.4 @RequiresRoles

@RequiresRoles 注解,和 roles 等价。代码如下:

// RequiresRoles.java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRoles {

    /**
     * A single String role name or multiple comma-delimited role names required in order for the method
     * invocation to be allowed.
     */
    String[] value();
    
    /**
     * The logical operation for the permission check in case multiple roles are specified. AND is the default
     * @since 1.1.0
     * 当有多个角色时,AND 表示要拥有全部角色,OR 表示拥有任一角色即可
     */
    Logical logical() default Logical.AND; 
}

使用示例如下:

// 属于 NORMAL 角色
@RequiresRoles("NORMAL")

// 要同时拥有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})

// 拥有 ADMIN 或 NORMAL 任一角色即可
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)

如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。不了解的胖友,可以看看《芋道 Spring Boot SpringMVC 入门》的「5. 全局异常处理」小节。

3.5 @RequiresPermissions

@RequiresPermissions 注解,和 perms 等价。代码如下:

// RequiresPermissions.java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermissions {

    /**
     * The permission string which will be passed to {@link org.apache.shiro.subject.Subject#isPermitted(String)}
     * to determine if the user is allowed to invoke the code protected by this annotation.
     */
    String[] value();
    
    /**
     * The logical operation for the permission checks in case multiple roles are specified. AND is the default
     * @since 1.1.0
     * 当有多个权限时,AND 表示要拥有全部权限,OR 表示拥有任一权限即可
     */
    Logical logical() default Logical.AND; 

}

使用示例如下:

// 拥有 user:add 权限
@RequiresPermissions("user:add")

// 要同时拥有 user:add 和 user:update 权限
@RequiresPermissions({"user:add", "user:update"})

// 拥有 user:add 和 user:update 任一权限即可
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)

如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。不了解的胖友,可以看看《芋道 Spring Boot SpringMVC 入门》的「5. 全局异常处理」小节。

3.6 使用示例

在 lab-33-shiro-demo 示例的基础上,我们进行修改,增加 Shiro 注解的使用。

在 cn.iocoder.springboot.lab01.shirodemo.controller 包路径下,创建 DemoController 类,提供示例 API 接口。代码如下:

// DemoController.java

@RestController
@RequestMapping("/demo")
public class DemoController {

    @RequiresGuest
    @GetMapping("/echo")
    public String demo() {
        return "示例返回";
    }

    @GetMapping("/home")
    public String home() {
        return "我是首页";
    }

    @RequiresRoles("ADMIN")
    @GetMapping("/admin")
    public String admin() {
        return "我是管理员";
    }

    @RequiresRoles("NORMAL")
    @GetMapping("/normal")
    public String normal() {
        return "我是普通用户";
    }

}
  • 每个 URL 的权限验证,和「3.2.2 TestController」是一一对应的。

胖友可以按照如上的说明,进行各种测试。例如说,登陆「user/user」用户后,去访问 /demo/admin 接口,会返回无权限的提示~

4. 项目实战

在开源项目翻了一圈,找到一个相对合适项目 renren-fast 。主要以下几点原因:

  • 基于 Shiro 实现。

  • 基于 RBAC 权限模型,并且支持动态的权限配置。

  • 基于 OAuth2 授权认证。

  • 前后端分离。同时前端采用 Vue ,相对来说后端会 Vue 的比 React 的多。

考虑到方便自己添加注释,艿艿 Fork 出一个仓库, 地址是 https://github.com/YunaiV/renren-fast 。

下面,来跟着艿艿一起走读下 renren-fast 的权限相关功能。

4.1 表结构

基于 RBAC 权限模型,一共有 5 个表。

“

对 RBAC 权限模型不了解的胖友,可以看看《到底什么是RBAC权限模型?!》

???? 嘻嘻,艿艿的大学毕业设计,做的就是统一认证中心,2011 年的时候,前后端分离。前端采用 ExtJS 框架,后端自己参考 Spring Security 造的权限框架的轮子,提供 SDK 接入统一认证中心,使用 HTTP 通信。

实体表说明
SysUserEntitysys_user用户信息
SysRoleEntitysys_role用户信息
SysUserRoleEntitysys_user_role用户和角色关联
SysMenuEntitysys_menu菜单权限
SysRoleMenuEntitysys_role_menu角色和菜单关联

5 个表的关系比较简单:

  • 一个 SysUse ,可以拥有多个 SysRole ,通过 SysUserRole 存储关联。

  • 一个 SysRole ,可以拥有多个 SysMenu ,通过 SysRoleMenu 存储关联。

4.1.1 SysUserEntity

SysUserEntity ,用户实体类。代码如下:

// SysUserEntity.java

@Data
@TableName("sys_user")
public class SysUserEntity implements Serializable {
    
    private static final long serialVersionUID = 1L;

    /** 用户ID */
    @TableId
    private Long userId;

    @NotBlank(message = "用户名不能为空", groups = {AddGroup.class, UpdateGroup.class})
    private String username;

    @NotBlank(message = "密码不能为空", groups = AddGroup.class)
    private String password;

    /** 盐  */
    private String salt;

    @NotBlank(message = "邮箱不能为空", groups = {AddGroup.class, UpdateGroup.class})
    @Email(message = "邮箱格式不正确", groups = {AddGroup.class, UpdateGroup.class})
    private String email;

    /** 手机号 */
    private String mobile;

    /** 状态  0:禁用   1:正常 */
    private Integer status;

    /** 创建者ID */
    private Long createUserId;

    /** 创建时间 */
    private Date createTime;

    /** 角色ID列表 */
    @TableField(exist = false)
    private List<Long> roleIdList;

}
  • 添加 @TableField(exist = false) 注解的字段,非存储字段。后续的实体,补充重复赘述。

  • 每个字段比较简单,胖友自己根据注释理解下即可。

  • renren-fast 的 DAO 采用 MyBatis-Plus 访问数据库。感兴趣的胖友,可以看看《芋道 Spring Boot MyBatis 入门》的「4. MyBatis-Plus」小节。

对应表的创建 SQL 如下:

-- 系统用户
CREATE TABLE `sys_user` (
  `user_id` bigint NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用户名',
  `password` varchar(100) COMMENT '密码',
  `salt` varchar(20) COMMENT '盐',
  `email` varchar(100) COMMENT '邮箱',
  `mobile` varchar(100) COMMENT '手机号',
  `status` tinyint COMMENT '状态  0:禁用   1:正常',
  `create_user_id` bigint(20) COMMENT '创建者ID',
  `create_time` datetime COMMENT '创建时间',
  PRIMARY KEY (`user_id`),
  UNIQUE INDEX (`username`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户';

4.1.2 SysRoleEntity

SysRole ,角色实体类。代码如下:

// SysRoleEntity.java

@Data
@TableName("sys_role")
public class SysRoleEntity implements Serializable {
    
 private static final long serialVersionUID = 1L;

 /** 角色ID */
 @TableId
 private Long roleId;

 @NotBlank(message = "角色名称不能为空")
 private String roleName;

 /** 备注 */
 private String remark;

 /** 创建者ID  */
 private Long createUserId;

 /** 创建时间 */
 private Date createTime;

 @TableField(exist=false)
 private List<Long> menuIdList;
    
}
  • 每个字段比较简单,胖友自己根据注释理解下即可。

对应表的创建 SQL 如下:

CREATE TABLE `sys_role` (
  `role_id` bigint NOT NULL AUTO_INCREMENT,
  `role_name` varchar(100) COMMENT '角色名称',
  `remark` varchar(100) COMMENT '备注',
  `create_user_id` bigint(20) COMMENT '创建者ID',
  `create_time` datetime COMMENT '创建时间',
  PRIMARY KEY (`role_id`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET utf8mb4 COMMENT='角色';

4.1.3 SysUserRoleEntity

SysUserRoleEntity ,用户和角色关联实体类。代码如下:

// SysUserRoleEntity.java

public class SysUserRoleEntity {

    /** 用户ID */
    private Long userId;

    /** 角色ID */
    private Long roleId;
    
    // ...省略 set/get 方法
    
}
  • 每个字段比较简单,胖友自己根据注释理解下即可。

对应表的创建 SQL 如下:

-- 用户与角色对应关系
CREATE TABLE `sys_user_role` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint COMMENT '用户ID',
  `role_id` bigint COMMENT '角色ID',
  PRIMARY KEY (`id`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET utf8mb4 COMMENT='用户与角色对应关系';

4.1.4 SysMenuEntity

SysMenuEntity ,菜单权限实体类。代码如下:

// SysMenuEntity.java

@Data
@TableName("sys_menu")
public class SysMenuEntity implements Serializable {

    private static final long serialVersionUID = 1L;

    /** 菜单ID */
    @TableId
    private Long menuId;

    /** 父菜单ID,一级菜单为0 */
    private Long parentId;

    /** 父菜单名称 */
    @TableField(exist = false)
    private String parentName;

    /** 菜单名称 */
    private String name;

    /** 菜单URL */
    private String url;

    /** 授权(多个用逗号分隔,如:user:list,user:create) */
    private String perms;

    /** 类型     0:目录   1:菜单   2:按钮 */
    private Integer type;

    /** 菜单图标 */
    private String icon;

    /** 排序 */
    private Integer orderNum;

    /** ztree 属性 */
    @TableField(exist = false)
    private Boolean open;

    @TableField(exist = false)
    private List<?> list;

}
  • ???? 个人感觉,这个实体改成 SysResourceEntity 资源,更加合适,菜单仅仅是其中的一种。

  • 每个字段比较简单,胖友自己根据资源理解下即可。我们来重点看几个字段。

  • type 属性,定义了三种类型。其中,2 代表按钮,是为了做页面中的功能级的权限。

  • perms 属性,对应的权限标识字符串。一般格式为 ${大模块}:${小模块}:{操作} 。示例如下:

    用户查询:system:user:query
    用户新增:system:user:add
    用户修改:system:user:edit
    用户删除:system:user:remove
    用户导出:system:user:export
    用户导入:system:user:import
    重置密码:system:user:resetPwd
    
    • 对于前端来说,每个按钮在展示时,可以判断用户是否有该按钮的权限。如果没有,则进行隐藏。当然,前端在首次进入系统的时候,会请求一次权限列表到本地进行缓存。

    • 对于后端来说,每个接口上会添加 Shiro @RequiresPermissions("system:user:query") 注解。在请求接口时,会校验用户是否有该 URL 对应的权限。如果没有,则会抛出权限验证失败的异常。

    • 一个 perms 属性,可以对应多个权限标识,使用逗号分隔。例如说:"system:user:query,system:user:add" 。

对应表的创建 SQL 如下:

-- 菜单
CREATE TABLE `sys_menu` (
  `menu_id` bigint NOT NULL AUTO_INCREMENT,
  `parent_id` bigint COMMENT '父菜单ID,一级菜单为0',
  `name` varchar(50) COMMENT '菜单名称',
  `url` varchar(200) COMMENT '菜单URL',
  `perms` varchar(500) COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',
  `type` int COMMENT '类型   0:目录   1:菜单   2:按钮',
  `icon` varchar(50) COMMENT '菜单图标',
  `order_num` int COMMENT '排序',
  PRIMARY KEY (`menu_id`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET utf8mb4 COMMENT='菜单管理';

4.1.5 SysRoleMenuEntity

SysRoleMenuEntity ,角色和菜单关联实体类。代码如下:

// SysRoleMenu.java

@Data
@TableName("sys_role_menu")
public class SysRoleMenuEntity implements Serializable {

 private static final long serialVersionUID = 1L;

 @TableId
 private Long id;

 /** 角色ID */
 private Long roleId;

 /** 菜单ID */
 private Long menuId;

}
  • 每个字段比较简单,胖友自己根据注释理解下即可。

对应表的创建 SQL 如下:

-- 角色与菜单对应关系
CREATE TABLE `sys_role_menu` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `role_id` bigint COMMENT '角色ID',
  `menu_id` bigint COMMENT '菜单ID',
  PRIMARY KEY (`id`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET utf8mb4 COMMENT='角色与菜单对应关系';

4.1.6 SysUserTokenEntity

SysUserTokenEntity ,用户 Token 实体类。代码如下:

// SysUserTokenEntity.java

@Data
@TableName("sys_user_token")
public class SysUserTokenEntity implements Serializable {

 private static final long serialVersionUID = 1L;

 //用户ID
 @TableId(type = IdType.INPUT)
 private Long userId;
 //token
 private String token;
 //过期时间
 private Date expireTime;
 //更新时间
 private Date updateTime;

}
  • 每个字段比较简单,胖友自己根据注释理解下即可。

  • 用户使用 username 和 password 登陆成功后,会生成 SysUserTokenEntity 记录到数据库中。后续的请求,使用 SysUserTokenEntity.token 作为身份标识。

对应表的创建 SQL 如下:

-- 系统用户Token
CREATE TABLE `sys_user_token` (
  `user_id` bigint(20) NOT NULL,
  `token` varchar(100) NOT NULL COMMENT 'token',
  `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `token` (`token`)
) ENGINE=`InnoDB` DEFAULT CHARACTER SET utf8mb4 COMMENT='系统用户Token';

4.2 ShiroConfig

在 ShiroConfig 配置类,实现 Shiro 的自定义配置。代码如下:

// ShiroConfig.java

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public SecurityManager securityManager(OAuth2Realm oAuth2Realm) { /**省略代码**/ }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { /**省略代码**/ }

    @Bean("lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { /**省略代码**/ }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { /**省略代码**/ }

}
  • 一共有四个 Bean 的配置,我们逐个来看看。

4.2.1 Realm

在 renren-fast 中,自定义 AuthorizingRealm 的实现类 OAuth2Realm ,读取我们自定义的数据库表结构,提供认证和授权功能。

因为 OAuth2Realm 的类上,已经添加了 @Component 注解,所以就不需要在 ShiroConfig 中进行 Bean 的配置。

关于 OAuth2Realm 的代码详细解析,我们见「4.4 OAuth2Filter」和「4.5 权限验证」 。

4.2.2 SecurityManager

#securityManager() 方法,我们创建了 DefaultWebSecurityManager Bean 对象。代码如下:

// ShiroConfig.java

@Bean("securityManager")
public SecurityManager securityManager(OAuth2Realm oAuth2Realm) {
    // 创建 DefaultWebSecurityManager 对象
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    // 设置其使用的 Realm 为 OAuth2Realm
    securityManager.setRealm(oAuth2Realm);
    // 无需使用记住密码功能
    securityManager.setRememberMeManager(null);
    return securityManager;
}
  • 和「2.2.2 SecurityManager」基本一致。

4.2.3 ShiroFilter

在 #shiroFilterFactoryBean() 方法,我们创建了 ShiroFilterFactoryBean Bean 对象。代码如下:

// ShiroConfig.java

@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    // 创建 ShiroFilterFactoryBean 对象,用于创建 ShiroFilter 过滤器
    ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();

    // 设置 SecurityManager
    shiroFilter.setSecurityManager(securityManager);

    // <1> 创建 OAuth2Filter 过滤器,并设置名字为 oauth2 。
    Map<String, Filter> filters = new HashMap<>();
    filters.put("oauth2", new OAuth2Filter());
    shiroFilter.setFilters(filters);

    // <2> ...

    Map<String, String> filterMap = new LinkedHashMap<>();
    filterMap.put("/webjars/**", "anon");
    filterMap.put("/druid/**", "anon");
    filterMap.put("/app/**", "anon");
    filterMap.put("/sys/login", "anon"); // <3> 登陆接口
    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"); // <4> 默认剩余的 URL ,需要经过认证
    shiroFilter.setFilterChainDefinitionMap(filterMap);

    return shiroFilter;
}
  • 和「2.2.3 ShiroFilter」略有差别,我们逐个来说说。

  • <1> 处,创建 OAuth2Filter 过滤器,并设置名字为 "oauth2" 。该过滤器,用于对请求头带的 OAuth2 的 Token 进行认证。

  • <2> 处,我们无需设置各种 URL 。因为在前后端分离之后,我们可以结合前端一起,实现自定义的登陆流程。当然,如果继续使用 Shiro 定义的登陆流程,实际也是没问题的。

  • <3> 处,设置登陆接口 /sys/login 允许匿名访问,不然咱没法实现登陆逻辑哈。详细解析,见「4.3 登陆 API 接口」。

  • <4> 处,剩余的 URL ,我们设置为 oauth2 ,使用 OAuth2Filter 来基于请求头带的 OAuth2 的 Token 进行认证。如果认证不通过,则返回未认证的错误提示。详细解析,见「4.4 OAuth2Filter」。

下面,我们详细的来看看,各个配置的 Bean 的逻辑。

4.3 登陆 API 接口

SysLoginController#login(...)

在 SysLoginController 中,定义了 /login 接口,提供登陆功能。代码如下:

// SysLoginController.java

@Autowired
private SysUserService sysUserService;
@Autowired
private SysUserTokenService sysUserTokenService;
@Autowired
private SysCaptchaService sysCaptchaService;

@PostMapping("/sys/login")
public Map<String, Object> login(@RequestBody SysLoginForm form) {
    // <1> 验证图片验证码的正确性
    boolean captcha = sysCaptchaService.validate(form.getUuid(), form.getCaptcha());
    if (!captcha) {
        return R.error("验证码不正确");
    }

    // <2> 获得之地当用户名的 SysUserEntity
    SysUserEntity user = sysUserService.queryByUserName(form.getUsername());
    if (user == null || !user.getPassword().equals(new Sha256Hash(form.getPassword(), user.getSalt()).toHex())) { // 账号不存在、密码错误
        return R.error("账号或密码不正确");
    }
    if (user.getStatus() == 0) { // 账号锁定
        return R.error("账号已被锁定,请联系管理员");
    }

    // <3> 生成 Token ,并返回结果
    return sysUserTokenService.createToken(user.getUserId());
}
  • <1> 处,验证图片验证码的正确性。该验证码会存储在 MySQL 数据库中,通过 uuid 作为对应的标识。生成的逻辑,胖友自己看 SysLoginController 提供的 /captcha.jpg 接口。

  • <2> 处,调用 SysUserService 的 #queryByUserName(String username) 方法,获得指定用户名的 SysUserEntity ,然后进行校验。详细解析,见「4.3.1 加载用户信息」。

  • <3> 处,调用 SysUserTokenService 的 #createToken(long userId) 方法,给认证通过的用户,生成其对应的认证 Token 。这样,该用户的后续请求,就使用该 Token 作为身份标识进行认证。

4.3.1 加载用户信息

在 SysUserServiceImpl 中,实现 SysUserService 接口定义的 #queryByUserName(String username) 方法,获得指定用户名的 SysUserEntity 。代码如下:

// SysUserServiceImpl.java

@Override
public SysUserEntity queryByUserName(String username) {
    // baseMapper 由 MyBatis-Plus 提供
 return baseMapper.queryByUserName(username);
}

// SysUserDao.XML
<select id="queryByUserName" resultType="io.renren.modules.sys.entity.SysUserEntity">
 select * from sys_user where username = #{username}
</select>
  • 通过查询 sys_user 表,将 username 对应的 SysUser 查询出来。

4.3.2 创建认证 Token

在 SysUserTokenServiceImpl 中,实现 SysUserTokenService 接口定义的 #createToken(LoginUser loginUser) 方法,给认证通过的用户,生成其对应的认证 Token 。代码如下:

// SysUserTokenServiceImpl.java

// 12小时后过期
private final static int EXPIRE = 3600 * 12;

@Override
public R createToken(long userId) {
    // <1> 生成一个 token
    String token = TokenGenerator.generateValue();

    // <2> 当前时间
    Date now = new Date();
    // <2> 过期时间
    Date expireTime = new Date(now.getTime() + EXPIRE * 1000);

    // <3> 判断是否生成过 token
    SysUserTokenEntity tokenEntity = this.getById(userId);
    if (tokenEntity == null) { // 新增 SysUserTokenEntity
        tokenEntity = new SysUserTokenEntity();
        tokenEntity.setUserId(userId);
        tokenEntity.setToken(token);
        tokenEntity.setUpdateTime(now);
        tokenEntity.setExpireTime(expireTime);

        // 保存 token
        this.save(tokenEntity);
    } else { // 更新 SysUserTokenEntity
        tokenEntity.setToken(token);
        tokenEntity.setUpdateTime(now);
        tokenEntity.setExpireTime(expireTime);

        // 更新 token
        this.updateById(tokenEntity);
    }

    // <4> 返回 token 和过期时间
    return R.ok().put("token", token).put("expire", EXPIRE);
}
  • <1> 处,调用 TokenGenerator 的 #generateValue() 方法,生成一个 token 。其内部逻辑是生成 UUID 后,再进行一次 MD5 编码。感兴趣的胖友,自己去瞅瞅。

  • <2> 处,获得当前时间,并计算 token 的过期时间为 12 小时后。

  • <3> 处,根据该用户是否已经有存在的 SysUserTokenEntity ,进行插入或更新。在 renren-fast 项目中,一个 SysUserEntity 有且仅有一个对应的 SysUserTokenEntity 。如果胖友希望用户登陆后,老的 token 不要作废,则这里可以改成插入 SysUserTokenEntity 即可。

  • <4> 处,返回 token 和过期时间。

4.4 OAuth2Filter

在 OAuth2Filter 中,继承 Shiro AuthenticatingFilter 过滤器,实现了基于 Token 的认证。代码如下:

// OAuth2Filter.java

public class OAuth2Filter extends AuthenticatingFilter {

    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { /**省略代码**/ }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { /**省略代码**/ }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { /**省略代码**/ }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { /**省略代码**/ }

}
  • 通过继承 Shiro AuthenticatingFilter 过滤器,可以简化实现整个认证过程的代码。FormAuthenticationFilter 和 BasicHttpAuthenticationFilter 就是继承自 AuthenticatingFilter 。

下面,我们逐个看看 OAuth2Filter 的每一个方法的实现。

4.4.1 isAccessAllowed

#isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) 方法,判断是否允许访问。代码如下:

// OAuth2Filter.java

@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    return ((HttpServletRequest) request).getMethod().equals(RequestMethod.OPTIONS.name());
}
  • 在这里,只允许 OPTIONS 类型的请求可以直接允许访问。

  • 在返回 false 时,就可以进入「4.4.3 onAccessDenied」的流程,根据请求带的 Token 进行认证。如果认证通过,说明可以访问。

4.4.2 createToken

#createToken(ServletRequest request, ServletResponse response) 方法,创建认真使用的 AuthenticationToken 。代码如下:

// OAuth2Filter.java

@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
    // <1> 获取请求中的 token
    String token = getRequestToken((HttpServletRequest) request);
    // 如果不存在,则返回 null
    if (StringUtils.isBlank(token)) {
        return null;
    }

    // <2> 创建 OAuth2Token 对象
    return new OAuth2Token(token);
}
  • <1> 处,调用 #getRequestToken(HttpServletRequest httpRequest) 方法,获得请求中的 token 。代码如下:

    // OAuth2Filter.java
    
    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;
    }
    
  • <2> 处,创建自定义的 OAuth2Token 。

4.4.3 onAccessDenied

#onAccessDenied(ServletRequest request, ServletResponse response) 方法,根据请求带的 Token 进行认证。如果认证通过,说明可以访问。代码如下:

// OAuth2Filter.java
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    // <1> 获取请求中的 token 。如果 token 不存在,直接返回 401 ,认证不通过
    String token = getRequestToken((HttpServletRequest) request);
    if (StringUtils.isBlank(token)) {
        // 设置响应 Header
        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);

        // 返回 false
        return false;
    }

    // <2> 执行登陆逻辑,实际执行的是基于 Token 进行认证。
    return executeLogin(request, response);
}
  • <1> 处,获取请求中的 token 。如果 token 不存在,直接返回 401 ,认证不通过的 JSON 提示。

  • <2> 处,调用父类 AuthenticatingFilter 的 #executeLogin(request, response) 方法,执行登陆逻辑。实际上在方法内部,调用 OAuth2Realm 的 #doGetAuthenticationInfo(AuthenticationToken token) 方法,执行基于 Token 进行认证。代码如下:

    // OAuth2Realm.java
    
    @Autowired
    private ShiroService shiroService;
        
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String accessToken = (String) token.getPrincipal();
        // <1> 根据 accessToken ,查询用户信息
        SysUserTokenEntity tokenEntity = shiroService.queryByToken(accessToken);
        // token 失效
        if (tokenEntity == null || tokenEntity.getExpireTime().getTime() < System.currentTimeMillis()) {
            throw new IncorrectCredentialsException("token失效,请重新登录");
        }
    
        // <2> 查询用户信息
        SysUserEntity user = shiroService.queryUser(tokenEntity.getUserId());
        // 账号锁定
        if (user.getStatus() == 0) {
            throw new LockedAccountException("账号已被锁定,请联系管理员");
        }
    
        // <3> 创建 SimpleAuthenticationInfo 对象
        return new SimpleAuthenticationInfo(user, accessToken, getName());
    }
    
    • <1> 处,调用 ShiroService 的 #queryByToken(String token) 方法,查询 Token (在 OAuth2 中,Token 为访问令牌 accessToken )对应的 SysUserTokenEntity 。如果不存在或者已过期,抛出 IncorrectCredentialsException 异常。代码如下:

      // ShiroServiceImpl.java
      @Autowired
      private SysUserTokenDao sysUserTokenDao;
      
      @Override
      public SysUserTokenEntity queryByToken(String token) {
          return sysUserTokenDao.queryByToken(token);
      }
      
      // SysUserTokenDao.XML
      <select id="queryByToken" resultType="io.renren.modules.sys.entity.SysUserTokenEntity">
       select * from sys_user_token where token = #{value}
      </select>
      
    • <2> 处,调用 ShiroService 的 #queryUser(Long userId)  方法,查询用户编号对应的 SysUserEntity 。如果已禁用,抛出 LockedAccountException 异常。代码如下:

      // ShiroServiceImpl.java
      @Autowired
      private SysUserDao sysUserDao;
          
      @Override
      public SysUserEntity queryUser(Long userId) {
          return sysUserDao.selectById(userId);
      }
      
    • <3> 处,创建 Shiro SimpleAuthenticationInfo 对象,为当前用户的认证信息。

至此,我们完成了基于 Token 进行认证的代码,胖友可以自己在理一理,顺一瞬。

4.4.4 onLoginFailure

#onLoginFailure(ServletRequest request, ServletResponse response) 方法,处理「4.4.3 onAccessDenied」 中,认证失败的时候,返回 401 ,认证不通过的 JSON 提示。代码如下:

// OAuth2Filter.java

@Override
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
    // 设置响应 Header
    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 ignored) {
    }

    // 返回 false
    return false;
}
  • 代码实现上,和「4.4.3 onAccessDenied」的请求 Token 不存在时的逻辑是一样的。

4.5 权限验证

在 renren-fast 中,使用「3. Shiro 注解」,实现每个 URL 的自定义权限。例如:

// SysConfigController.java

@GetMapping("/list")
@RequiresPermissions("sys:config:list")
public R list(@RequestParam Map<String, Object> params) { /**省略代码**/ }

因为要验证权限,所以会调用到 OAuth2Realm 的 #doGetAuthorizationInfo(PrincipalCollection principals) 方法,进行鉴权,获得用户拥有的权限。代码如下:

// OAuth2Realm.java

@Autowired
private ShiroService shiroService;
    
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // <1> 获得 SysUserEntity 对象
    SysUserEntity user = (SysUserEntity) principals.getPrimaryPrincipal();
    Long userId = user.getUserId();

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

    // <3> 创建 SimpleAuthorizationInfo 对象
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.setStringPermissions(permsSet);
    return info;
}
  • <1> 处,获得 SysUserEntity 对象。该对象就是我们在 OAuth2Realm 的 #doGetAuthenticationInfo(AuthenticationToken token) 方法中,所认证获得的。

  • <2> 处,调用 ShiroService 的 getUserPermissions(long userId) 方法,获得该用户拥有的权限数组。代码如下:

    // OAuth2Realm.java
    
    @Autowired
    private SysMenuDao sysMenuDao;
    @Autowired
    private SysUserDao sysUserDao;
    
    @Override
    public Set<String> getUserPermissions(long userId) {
        List<String> permsList;
        // <1.1> 系统管理员,拥有最高权限
        if (userId == Constant.SUPER_ADMIN) {
            // 如果是管理员,则查询所有 SysMenuEntity 数组
            List<SysMenuEntity> menuList = sysMenuDao.selectList(null);
            permsList = new ArrayList<>(menuList.size());
            for (SysMenuEntity menu : menuList) {
                permsList.add(menu.getPerms());
            }
        // <1.2>
        } else {
            // 如果是普通用户,则查询其拥有的 SysMenuEntity 数组
            permsList = sysUserDao.queryAllPerms(userId);
        }
        // <2> 用户权限列表
        Set<String> permsSet = new HashSet<>();
        for (String perms : permsList) {
            if (StringUtils.isBlank(perms)) {
                continue;
            }
            // 使用逗号分隔,每一个 perms
            permsSet.addAll(Arrays.asList(perms.trim().split(",")));
        }
        return permsSet;
    }
    
    • 通过 sys_user_role 连接 sys_role_menu 和 sys_menu 表,实现查询用户的所有权限。

    • <1.1> 处,如果是管理员( id == Constant.SUPER_ADMIN == 1 )时,调用 SysMenuDao 的 #selectList(Wrapper<T> queryWrapper) 方法,查询所有 SysMenuEntity 数组,从而实现管理员拥有全部权限。

    • <1.2> 处,如果是普通用户,调用 SysMenuDao 的 #queryAllPerms(Long userId) 方法,查询该用户拥有的 SysMenuEntity 数组。代码如下:

      // SysMenuDao.java
      
      <select id="queryAllPerms" resultType="string">
       select m.perms from sys_user_role ur 
        LEFT JOIN sys_role_menu rm on ur.role_id = rm.role_id 
        LEFT JOIN sys_menu m on rm.menu_id = m.menu_id 
       where ur.user_id = #{userId}
      </select>
      
    • <2> 处,返回用户权限列表。因为一个 SysMenuEntity.perms 可能对应多个权限,使用逗号分隔,所以这里需要做处理。

  • <3> 处,创建 Shiro SimpleAuthorizationInfo 对象,为当前用户的授权信息。

  • 另外,如果胖友想要使用 Shiro 的 @RequiresRoles 注解,需要读取用户拥有的角色。因为 renren-fast 目前暂时未使用该注解,所以并没有实现该逻辑。

4.6 获得权限 API 接口

在 SysMenuController 中,定义了 /sys/menu/nav 接口,获得当前登陆用户的菜单和权限。代码如下:

// SysMenuController.java

@Autowired
private SysMenuService sysMenuService;
@Autowired
private ShiroService shiroService;

/**
 * 导航菜单
 */
@GetMapping("/nav")
public R nav() {
    // <1> 获得用户的菜单数组
    List<SysMenuEntity> menuList = sysMenuService.getUserMenuList(getUserId());
    // <2> 获得用户的权限集合
    Set<String> permissions = shiroService.getUserPermissions(getUserId());
    // <3> 返回
    return R.ok().put("menuList", menuList)
            .put("permissions", permissions);
}
  • <1> 处,调用 SysMenuService 的 #getUserMenuList(Long userId) 方法,获得用户的菜单数组。代码如下:

    // SysMenuServiceImpl.java
    
    @Autowired
    private SysUserService sysUserService;
        
    @Override
    public List<SysMenuEntity> getUserMenuList(Long userId) {
        // 系统管理员,拥有最高权限
        if (userId == Constant.SUPER_ADMIN) {
            return getAllMenuList(null);
        }
    
        // 用户菜单列表
        List<Long> menuIdList = sysUserService.queryAllMenuId(userId);
        return getAllMenuList(menuIdList);
    }
    
    /**
     * 获取所有菜单列表
     */
    private List<SysMenuEntity> getAllMenuList(List<Long> menuIdList) {
        // 查询根菜单列表
        List<SysMenuEntity> menuList = queryListParentId(0L, menuIdList);
        // 递归获取子菜单
        getMenuTreeList(menuList, menuIdList);
        return menuList;
    }
    
    /**
     * 递归
     */
    private List<SysMenuEntity> getMenuTreeList(List<SysMenuEntity> menuList, List<Long> menuIdList) {
        List<SysMenuEntity> subMenuList = new ArrayList<SysMenuEntity>();
        for (SysMenuEntity entity : menuList) {
            // 目录
            if (entity.getType() == Constant.MenuType.CATALOG.getValue()) {
                entity.setList(getMenuTreeList(queryListParentId(entity.getMenuId(), menuIdList), menuIdList)); // <X>
            }
            subMenuList.add(entity);
        }
        return subMenuList;
    }
    
    • 这块代码写的比较糟糕,在 <X> 处存在递归查询,在菜单量大的时候,会导致性能较差。可以考虑将用户拥有的菜单一次性查询出来,然后在内存中拼接树形结构。

  • <2> 处,调用 ShiroService 的 getUserPermissions(long userId) 方法,获得该用户拥有的权限数组。

  • <3> 处,返回用户拥有的菜单和权限。

4.7 退出 API 接口

在 SysLoginController 中,定义了 /logout 接口,提供退出功能。代码如下:

// SysLoginController.java

@Autowired
private SysUserTokenService sysUserTokenService;

@PostMapping("/sys/logout")
public R logout() {
    sysUserTokenService.logout(getUserId());
    return R.ok();
}
  • 调用 SysUserTokenServiceImpl 的 #logout(long userId) 方法,实现用户的退出。代码如下:

    // SysUserTokenServiceImpl.java
    
    @Override
    public void logout(long userId) {
        // 生成一个token
        String token = TokenGenerator.generateValue();
    
        // 修改token
        SysUserTokenEntity tokenEntity = new SysUserTokenEntity();
        tokenEntity.setUserId(userId);
        tokenEntity.setToken(token);
        this.updateById(tokenEntity);
    }
    
    • 通过创建一个新的 token 值,修改该用户的 SysUserTokenEntity ,从而使用户当前的 Token 失效。

    • ???? 有点尴尬的实现~胖友可以给 SysUserTokenEntity 增加一个标记删除的字段,或者修改过期时间。

4.8 权限管理

如下的 Controller ,提供了 renren-fast 的权限管理功能,比较简单,胖友自己去瞅瞅即可。

  • 用户管理 SysUserController :用户是系统操作者,该功能主要完成系统用户配置。

  • 角色管理 SysRoleController :角色菜单权限分配、设置角色按机构进行数据范围权限划分。

  • 菜单管理 SysMenuController :配置系统菜单,操作权限,按钮权限标识等。

4.9 小小的建议

至此,我们完成了对 renren-fast 权限相关功能的源码进行解读,希望对胖友有一定的胖友。如果胖友项目中需要权限相关的功能,建议不要直接拷贝 renren-fast 的代码,而是按照自己的理解,一点点“重新”实现一遍。在这个过程中,我们会有更加深刻的理解,甚至会有自己的一些小创新。

另外,RuoYi 也是一个基于 Shiro 实现权限管理的开源项目。胖友也可以去借鉴学习下。a

666. 彩蛋

相对还是比较良心的一篇文章,胖友你说是不是,嘿嘿。

这里额外在推荐一些 Shiro 不错的内容:

  • 《Shiro 实现原理与源码解析系统 —— 精品合集》

  • 《如何设计权限管理模块(附表结构)?》

  • 《Spring Boot + Vue + Shiro 实现前后端分离、权限控制》

  • 《学习如何使用 Shiro,从架构谈起,到框架集成!》

  • 《SpringBoot + Shiro + Redis 共享 Session 实例》

  • 《SpringBoot 整合 Shiro 实现动态权限加载更新+ Session 共享 + 单点登录》

不过艿艿实际项目中,并未采用 Spring Security 或是 Shiro 安全框架,而是自己团队开发了一个相对轻量级的组件。主要考虑,目前前后端分离之后,Shiro 内置的很多功能,已经不太需要,在加上拓展一些功能不是非常方便,有点“曲折”,所以才选择自己开发。



欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:

已在知识星球更新源码解析如下:

最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。

提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。

获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

兄弟,艿一口,点个赞!????

科技中介服务机构在协助企业对接科研成果时,如何提升服务响应速度与专业化水平.docx 科技中介服务机构在协助企业对接科研成果时,如何提升服务响应速度与专业化水平 立即下载

相关推荐

国央企创新负责人在推动内部技术成果转化与外部协同创新时,如何实现资源的高效整合与协同联动?.docx

国央企创新负责人在推动内部技术成果转化与外部协同创新时,如何实现资源的高效整合与协同联动?

2026最新基于Aria2下载器解决百度网盘限速问题

Aria下载器

机器人入门实战:用 ROS2 和 Gazebo 先把差速车 URDF 跑起来,再做一版 SLAM 建图(含教学手册与源码骨架)

原创教学资源包(VIP专属)。围绕机器人开发项目实战搭建可复现闭环:用差速车 URDF 把 base_link、左右轮和激光连杆的关节树写自洽,配一间教学库房的 Gazebo 世界与 launch 骨架;再提供一版合成二维占用栅格建图,对比纯里程计写入和扫描匹配修正,用来分辨 odom 系漂移与 map 系 SLAM 输出。内含中文实战手册与可 make 抽检的 ROS 2 源码骨架。实现均为教学桩,注释标明不是完整 Navigation2 栈,不能替代实机传感器标定,也不是 Cartographer / slam_toolbox 的生产方案。 使用方法:解压后进入 src/ 目录(需 Linux、Python 3;不必安装 ROS 2 或 Gazebo 也能抽检)。先执行 make clean && make test,或运行 python3 scripts/smoke_test.py,看到 ALL SMOKE TESTS PASSED 后再阅读手册.md、README.md 与源码注释。若本机已安装 ROS 2 Humble/Jazzy 与 Gazebo,再把 src/diffbot_slam_tutor 放入工作空间,按 README 中「读者本地环境再跑」的 launch 命令启动仿真。细节见手册.md 与 src/README_SRC.txt。 适合作为成长任务「机器人开发项目实战」的自制合规 VIP 资源上传。建议售价 9.90 元。模型与数据均为虚构示例,不含刷量、搬运或真实个人信息。

港股十档行情tick数据,包含逐笔成交与买卖委托快照 逐笔记录含成交时间、价格、成交量、成交方向及委托单类型 十档委托快照提供买卖各十个价位的挂单价格与挂单数量,可观察盘口深度变化 分钟数据以一分钟为

港股十档行情tick数据,包含逐笔成交与买卖委托快照。逐笔记录含成交时间、价格、成交量、成交方向及委托单类型。十档委托快照提供买卖各十个价位的挂单价格与挂单数量,可观察盘口深度变化。分钟数据以一分钟为单位,包含开盘价、最高价、最低价、收盘价、成交量及成交额。历史行情覆盖近十五年,提供完整日线csv表格,字段包含日期、开盘价、最高价、最低价、收盘价、成交量、成交额,部分还包含调整因子或前收盘价。所有数据均为非实时历史数据。 数据源:CMES金融数据库

【云原生开发】基于Go语言高并发与容器化技术的微服务系统设计与实战

内容概要:本文系统讲解了Go语言在高并发与云原生领域的核心技术与实战应用,从Goroutine调度器、Channel底层实现、CSP并发模型等原理出发,深入剖析GMP调度机制、内存模型、同步原语与Context控制流,并结合Worker Pool、Pipeline、Fan-out/Fan-in等经典并发模式,逐步过渡到云原生实践。内容涵盖Docker容器化、Kubernetes编排、微服务架构设计、可观测性体系建设(日志、监控、链路追踪),并通过一个完整的高并发短链接服务项目,演示从代码实现、性能调优到云原生部署的全流程。; 适合人群:具备一定Go语言基础,有1-3年后端开发经验,希望深入理解并发原理并转型云原生方向的工程师;熟悉Java/Python等语言并计划转向Go生态的开发者;以及需要掌握Kubernetes内部机制的平台工程师。; 使用场景及目标:①理解Go并发核心机制如Goroutine、Channel、GMP调度、Context取消传播的底层原理与最佳实践;②掌握高并发场景下的Worker Pool、Pipeline等模式设计与性能调优方法;③实现服务的容器化打包、Kubernetes部署、自动扩缩容与可观测性集成,构建可投入生产的云原生系统。; 阅读建议:建议遵循“原理→模式→实战”的学习路径,结合书中代码示例动手实践,重点理解并发模型与云原生架构的设计思想。在学习过程中应使用pprof、trace、-race等工具进行调试与验证,强化对并发安全与性能瓶颈的认知。

两路半桥LLC谐振变换器软开关Simulink仿真(模拟谐振参数不同、分裂电容)

内容概要:本文介绍了“两路半桥LLC谐振变换器软开关Simulink仿真”资源包,重点在于基于Simulink平台对两路半桥LLC谐振变换器进行高精度建模与动态仿真,特别考虑了谐振参数不一致及分裂电容等非理想工况对系统性能的影响。该仿真模型不仅涵盖了主电路拓扑、软开关实现机制与谐振网络设计,还深入分析了不同参数配置下系统的启动特性、稳态效率、动态响应及软开关条件的满足情况。通过参数扫描与对比仿真,帮助用户掌握LLC变换器的关键设计要素与优化方法,具备较强的工程指导价值和科研教学意义。; 适合人群:电力电子、电气工程及其自动化等相关专业的高校师生,以及从事高频开关电源、新能源变换器、车载充电机等领域的研发工程师和技术人员。; 使用场景及目标:① 掌握两路并联半桥LLC谐振变换器的建模与仿真方法;② 理解软开关技术在提升变换器效率与降低EMI方面的关键作用;③ 分析谐振参数偏差与分裂电容结构对系统均流特性、增益曲线及软开关范围的影响;④ 支持科研项目中的拓扑验证、控制策略开发与故障工况分析; 阅读建议:建议结合Simulink仿真模型与配套说明文档进行学习,重点关注谐振腔参数设置、驱动时序设计及软开关波形观测;可通过调整谐振电感、电容值或引入参数失配,观察系统行为变化,深入理解LLC变换器的稳定边界与优化设计原则。

高校技术转移办公室人员如何提升科技成果推介效率与质量?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

双向全桥CLLC谐振变换器仿真模型(Simulink仿真实现)

内容概要:本文系统介绍了双向全桥CLLC谐振变换器的Simulink仿真模型,重点阐述了变频控制、闭环仿真及其与多种电力电子拓扑(如维也纳整流器、DAB变换器、图腾柱PFC等)的联合仿真设计。资源包涵盖2.5kW双向全桥LLC、V2G电动汽车充放电系统、三相PFC、多相交错LLC等多种变换器的仿真案例,并深入展示了三电平VIENNA整流器在双闭环PI控制与SVPWM调制策略下,结合上下电容电压差值补偿实现中点电位平衡的先进控制方法。通过仿真验证,该策略不仅有效维持了800V直流母线电压的稳定,还在动态工况下显著提升了电容电压的均衡度,同时保持了较低的输入电流THD,充分体现了SVPWM与中点平衡控制协同优化的技术优势。; 适合人群:具备电力电子与自动控制理论基础,从事新能源发电、电动汽车、工业电源、微电网等领域的研究生、工程师及科研人员。; 使用场景及目标:① 学习并构建高频谐振变换器(如LLC、CLLC)的Simulink仿真模型,掌握其变频与混合控制策略;② 深入理解并应用中点电位平衡、双闭环PI控制、SVPWM调制等核心技术于VIENNA等三电平变换器中;③ 复现高水平期刊论文中的控制算法,服务于科研项目申报、学术论文撰写或工程产品开发。; 阅读建议:该资源为综合性仿真资料库,建议使用者结合自身研究方向(如电动汽车充电、新能源并网)选择对应案例进行精学,重点关注控制策略的设计逻辑、仿真参数的整定方法以及SVPWM与中点平衡的协同机制,并充分利用附带的计算书和参考文献深化理解,以实现从仿真模型到实际工程应用的顺利转化。

基于双向Buck-Boost电路的锂电池储能充电+放电控制Simulink仿真

内容概要:本文系统介绍了基于双向Buck-Boost电路的锂电池储能系统充放电控制的Simulink仿真设计,重点构建了储能系统中能量双向流动的控制模型,涵盖充电与放电两种工作模式。通过Simulink平台搭建主电路拓扑与闭环控制系统,实现对锂电池的安全、高效能量管理,确保系统在不同工况下的稳定性与动态响应性能。研究不仅聚焦于Buck-Boost变换器的能量转换机制,还结合多种先进控制策略,如模型预测控制、电压下垂控制等,提升系统的控制精度与鲁棒性。同时,文中列举了包括LLC谐振变换器、Vienna整流器、PWM整流器等多种电力电子变换器的仿真案例,体现了该资源在新能源、微电网及电力电子系统建模与控制领域的广泛适用性与技术综合性; 适合人群:具备电力电子、自动控制或电气工程等相关专业背景,熟悉MATLAB/Simulink仿真环境,从事新能源系统、储能技术、电力电子变换器设计及相关领域研究的研发人员与高校研究生; 使用场景及目标:①深入理解双向Buck-Boost电路在锂电池储能系统中的能量管理原理与系统建模方法;②掌握基于Simulink的充放电控制策略设计、闭环系统搭建与仿真验证流程;③拓展至其他主流DC-DC与AC-DC变换器(如LLC、Vienna、DAB等)的建模与控制研究,服务于科研项目、课程设计、毕业课题或工程开发应用; 阅读建议:建议结合文中提供的仿真模型与参考资料,动手实践Simulink建模与参数调试过程,重点关注控制环路的设计逻辑、控制器参数整定方法及系统动态响应特性;同时可参考同系列其他变换器的仿真案例,横向比较不同拓扑的控制策略差异,以拓宽技术视野与工程应用能力。

全桥LLC谐振(变频+移相混合控制)变换器闭环仿真模型(Simulink仿真实现)

全桥LLC谐振(变频+移相混合控制)变换器闭环仿真模型(Simulink仿真实现)

虚拟同步发电机(VSG)惯量阻尼自适应控制仿真模型(simulink仿真实现)

内容概要:本文提供了“虚拟同步发电机(VSG)惯量阻尼自适应控制仿真模型”的Simulink仿真实现方法,系统阐述了如何通过仿真手段研究VSG在电力系统中的动态响应特性,重点聚焦于其惯量与阻尼的自适应控制策略。文中详细说明了模型构建流程,涵盖控制算法设计、参数调节、根轨迹分析等内容,并通过对比不同调节系数下的系统响应,深入探讨了控制参数对系统稳定性与动态性能的影响。该仿真模型有效复现了VSG模拟传统同步发电机惯性与阻尼特性的能力,对于提升新能源并网系统的频率稳定性和抗扰动能力具有重要研究价值。; 适合人群:具备电力系统、自动化或相关领域基础知识,从事新能源并网、微电网控制、电力电子与电力系统仿真研究的研究生、科研人员及工程师。; 使用场景及目标:①用于教学与科研中深入理解虚拟同步发电机的工作原理及其在现代电力系统中的应用;②为新能源发电系统接入电网时的稳定性分析与控制策略设计提供仿真验证平台;③支持对VSG控制参数进行优化,提升系统应对负荷波动和扰动的能力。; 阅读建议:建议读者结合Matlab/Simulink软件动手实践,按照文档结构逐步搭建仿真模型,重点关注控制环路设计与参数整定部分,同时参考提供的代码与仿真结果进行对比分析,以加深对VSG自适应控制机制的理解。

高校技术转移办公室人员需要哪些工具来提升成果转化服务能力?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

两相交错并联Boost电路仿真模型(Simulink仿真实现)

内容概要:本文介绍了“两相交错并联Boost电路仿真模型(Simulink仿真实现)”的技术资源,属于电力电子变换器领域的仿真研究内容。该模型基于Simulink平台构建,旨在深入分析两相交错并联Boost电路的工作原理、动态响应特性及控制策略。交错并联结构通过错开各相开关管的导通时刻,有效降低输入电流纹波、提升系统效率与功率密度,适用于中高功率DC-DC变换应用场景。资源包含完整的主电路建模、PWM驱动信号生成、闭环控制器设计(如电压/电流双闭环控制)等关键模块,并可能涵盖软启动、均流控制等关键技术问题。整个仿真模型强调理论分析与工程实践的紧密结合,不仅有助于理解电路基本行为,也为优化控制算法提供验证平台,服务于科研与教学需求。; 适合人群:电气工程、自动化、电力电子及相关专业的高校本科生、研究生,以及从事电源系统研发的工程技术人员,具备一定的电路理论、自动控制原理和Simulink仿真基础者更佳。; 使用场景及目标:① 学习和掌握两相交错并联Boost电路的拓扑结构、工作机理及性能优势;② 利用Simulink进行电力电子电路的建模与仿真,深入理解PWM控制、闭环调节、均流控制等核心技术的应用;③ 为科研项目、课程设计或毕业设计提供可复用的仿真案例和技术支持,辅助开展中高功率电源系统的创新设计与优化研究。; 阅读建议:建议结合Simulink软件动手搭建并调试模型,通过调整占空比、开关频率、电感参数等变量,观察其对输入电流纹波、输出电压稳定性及动态响应的影响,以深化对电路特性的理解;同时可参考文中列出的其他相关资源(如LLC、PFC等仿真模型)进行横向对比学习,拓宽电力电子系统设计视野。

继电保护小电流接地系统故障仿真-中性点不接地与经消弧线圈接地仿真模型(Simulink仿真实现)

内容概要:本文基于Simulink平台构建了小电流接地系统中性点不接地与经消弧线圈接地的故障仿真模型,旨在深入研究单相接地故障下的系统动态行为。通过精确建模与仿真实验,系统地对比分析了两种接地方式在故障电流幅值、电压分布特性及暂态响应等方面的差异,重点探讨了其在故障检测灵敏度、电弧自熄能力以及过电压抑制效果上的各自优势与局限性。研究不仅有助于深化对小电流接地系统运行机理的理解,也为继电保护策略的优化设计、故障选线算法的开发及配电网安全稳定运行提供了重要的理论依据和仿真验证手段。; 适合人群:具备电力系统分析、继电保护原理等相关基础知识,从事配电网运行维护、故障诊断、继电保护装置研发或相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:① 掌握小电流接地系统的故障特征与精细化仿真建模方法;② 对比分析中性点不接地与经消弧线圈接地两种方式在供电可靠性、设备绝缘要求和人身安全方面的综合性能;③ 为新型继电保护方案的设计、故障定位与隔离算法的验证以及配电网运行方式的优化决策提供可靠的仿真测试平台。; 阅读建议:此资源以Simulink仿真为核心,建议读者在学习过程中务必结合电力系统故障分析理论,亲自动手搭建或调试仿真模型,重点关注故障发生前后关键电气量(如零序电压、零序电流)的变化规律,并尝试调整系统参数(如线路长度、过渡电阻、消弧线圈补偿度)以观察其对故障特性的影响,从而深刻理解不同接地方式的本质区别与适用场景。

区域电网含风光火压缩空气储能抽水蓄能以及电化学储能的联合调度,加入IEEE39系统,进行潮流计算,分析电能质量研究(Matlab代码实现)

内容概要:本文研究了包含风能、太阳能、火电以及压缩空气储能、抽水蓄能和电化学储能等多种能源形式的区域电网联合调度问题,通过将系统接入IEEE 39节点标准测试系统,利用Matlab平台开展潮流计算与电能质量分析,旨在提升高比例可再生能源接入背景下电网运行的稳定性与供电品质。研究重点涵盖多类型电源与多种储能系统的协调优化调度策略设计,深入探讨储能系统在削峰填谷、平滑新能源出力波动方面的关键作用,并对复杂运行工况下电压偏差、频率波动、谐波污染等电能质量问题进行评估与改善。通过构建完整的仿真模型,系统性地分析了联合调度对电网电能质量的影响机制,为现代电力系统中多能互补与协同控制提供了理论依据和技术参考。; 适合人群:具备电力系统分析、优化调度及电能质量相关基础知识,熟悉Matlab/Simulink仿真环境的研究生、科研人员及电力系统相关领域的工程师。; 使用场景及目标:①为含多种储能的区域电网联合调度研究提供完整的Matlab仿真案例与开源代码参考;②分析大规模新能源接入对IEEE 39系统潮流分布及电能质量的综合影响,并定量评估不同类型储能系统的调控效果;③为电力系统优化调度、电能质量综合治理、多能协同控制等学术研究与工程项目提供可复现的技术支撑与解决方案。; 阅读建议:本资源以Matlab代码实现为核心,读者应在掌握电力系统基本理论的基础上,重点关注联合调度的数学建模过程、储能系统充放电策略的制定逻辑以及电能质量各项评价指标的计算方法。建议在学习过程中动手调试仿真代码,尝试调整系统参数、优化目标权重或改变新能源渗透率,以深入理解系统动态行为与控制策略的有效性。

吉林大学软件工程章节测验 第九章 答案+解析

4.【判断题】估算软件规模有代码行技术和功能点技术两种方法,其中代码行方法估计实现一个功能所需要的源程序行数,其不依赖项目所使用的开发语言,并且具有较好的直观性。3.【单选题】软件项目估算时,若计划人员对每个功能分别按最佳的、可能的、悲观的三种情况给出功能点估计值,记作a, m, b,则估算的期望值E的公式为( )。8.【判断题】在工程网络图中,如果在由起始事件至终止事件所构成的一条路径上,所有事件的最早时刻都和最迟时刻都相同,那么这条路径就是关键路径。4.【单选题】配置管理贯穿软件开发的整个过程。

m0_51459931的博客 1433
上一篇: 分布式技术学习路线全攻略
下一篇: HTTPS 的工作原理
公众号-芋道源码
博客等级 码龄10年 1634粉丝 377原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值