Spring Boot:整合Shiro权限框架

综合概述

Shiro 是 Apache 旗下的一个开源项目,它是一个非常易用的安全框架,提供了包括认证、授权、加密、会话管理等功能,与 Spring Security 一样属基于权限的安全框架,但是与 Spring Security 相比,Shiro 使用了比较简单易懂易于使用的授权方式。Shiro 属于轻量级框架,相对于 Spring Security 简单很多,并没有 security 那么复杂。

优势特点

它是一个功能强大、灵活的、优秀的、开源的安全框架。

它可以胜任身份验证、授权、企业会话管理和加密等工作。

它易于使用和理解,与 Spring Security 相比,入门门槛低。

主要功能

  • 验证用户身份
  • 用户访问权限控制
  • 支持单点登录 (SSO) 功能
  • 可以响应认证、访问控制,或 Session 事件
  • 支持提供“Remember Me”服务
  • .......

框架体系

Shiro 的整体框架大致如下图所示(图片来自互联网):

Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)代表 Shiro 应用安全的四大基石。

它们分别是:

  • Authentication(认证):用户身份识别,通常被称为用户“登录”。
  • Authorization(授权):访问控制。比如某个用户是否具有某个操作的使用权限。
  • Session Management(会话管理):特定于用户的会话管理, 甚至在非 web 应用程序。
  • Cryptography(加密):在对数据源使用加密算法加密的同时,保证易于使用。

除此之外,还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。

特别是对以下的功能支持:

  • Web 支持:Shiro 提供的 web 支持 api ,可以很轻松的保护 web 应用程序的安全。
  • 缓存:缓存是 Apache Shiro 保证安全操作快速、高效的重要手段。
  • 并发:Apache Shiro 支持多线程应用程序的并发特性。
  • 测试:支持单元测试和集成测试,确保代码和预想的一样安全。
  • “Run As”:这个功能允许用户在许可的前提下假设另一个用户的身份。
  • “Remember Me”:跨 session 记录用户的身份,只有在强制需要时才需要登录。

主要流程

在概念层,Shiro 架构包含三个主要的理念:Subject, SecurityManager 和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。

Shiro 执行流程图(图片来自互联网)

三个主要理念:

  • Subject:代表当前用户,Subject 可以是一个人,也可以是第三方服务、守护进程帐户、时钟守护任务或者其它当前和软件交互的任何事件。
  • SecurityManager:管理所有 Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
  • Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到 Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个 Realm 来实现认证(authentication)和 / 或授权(authorization)。

我们需要实现 Realms 的 Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。

实现案例

接下来,我们就通过一个具体的案例,来讲解如何进行 Shiro 的整合,然后借助 Shiro 实现登录认证和访问控制。

生成项目模板

为方便我们初始化项目,Spring Boot 给我们提供一个项目模板生成网站。

1.  打开浏览器,访问:https://start.spring.io/

2.  根据页面提示,选择构建工具,开发语言,项目信息等。

3.  点击 Generate the project,生成项目模板,生成之后会将压缩包下载到本地。

4.  使用 IDE 导入项目,我这里使用 Eclipse,通过导入 Maven 项目的方式导入。

添加相关依赖

清理掉不需要的测试类及测试依赖,添加 Maven 相关依赖,这里需要添加上 WEB、Swagger、JPA 和 Shiro 的依赖,Swagger 的添加是为了方便接口测试。

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">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.louis.springboot</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
&lt;properties&gt;
    &lt;java.version&gt;<span style="color: rgba(128, 0, 128, 1)">1.8</span>&lt;/java.version&gt;
&lt;/properties&gt;

&lt;dependencies&gt;
    &lt;!-- web --&gt;
    &lt;dependency&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
     &lt;/dependency&gt;
    &lt;!-- swagger --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.springfox&lt;/groupId&gt;
        &lt;artifactId&gt;springfox-swagger2&lt;/artifactId&gt;
        &lt;version&gt;<span style="color: rgba(128, 0, 128, 1)">2.9</span>.<span style="color: rgba(128, 0, 128, 1)">2</span>&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.springfox&lt;/groupId&gt;
        &lt;artifactId&gt;springfox-swagger-ui&lt;/artifactId&gt;
        &lt;version&gt;<span style="color: rgba(128, 0, 128, 1)">2.9</span>.<span style="color: rgba(128, 0, 128, 1)">2</span>&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!-- jpa --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-data-jpa&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- mysql --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;mysql&lt;/groupId&gt;
        &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- shiro --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.apache.shiro&lt;/groupId&gt;
        &lt;artifactId&gt;shiro-spring&lt;/artifactId&gt;
        &lt;version&gt;<span style="color: rgba(128, 0, 128, 1)">1.4</span>.<span style="color: rgba(128, 0, 128, 1)">1</span>&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
    &lt;plugins&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
            &lt;artifactId&gt;spring-boot-maven-plugin&lt;/artifactId&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
    &lt;!-- 打包时拷贝MyBatis的映射文件 --&gt;
    &lt;resources&gt;
        &lt;resource&gt;
            &lt;directory&gt;src/main/java&lt;/directory&gt;
            &lt;includes&gt;
                &lt;include&gt;**/sqlmap<span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">.xml&lt;/include&gt;
            &lt;/includes&gt;
            &lt;filtering&gt;false&lt;/filtering&gt;
        &lt;/resource&gt;
        &lt;resource&gt;  
            &lt;directory&gt;src/main/resources&lt;/directory&gt;  
                &lt;includes&gt; 
                    &lt;include&gt;*</span><span style="color: rgba(0, 128, 0, 1)">*/</span>*.*&lt;/include&gt;  
                &lt;/includes&gt; 
                &lt;filtering&gt;<span style="color: rgba(0, 0, 255, 1)">true</span>&lt;/filtering&gt;  
        &lt;/resource&gt; 
    &lt;/resources&gt;
&lt;/build&gt;

</project>

添加相关配置

1. 添加数据源和 jpa 相关配置

将 application.properties 文件改名为 application.yml ,并在其中添加 MySQL 数据源连接信息。

注意:

这里需要首先创建一个 MySQL 数据库,并输入自己的用户名和密码。这里的数据库是 springboot。

另外,如果你使用的是 MySQL 5.x 及以前版本,驱动配置 driverClassName 是 com.mysql.jdbc.Driver。

application.yml 

server:
  port: 8080
spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8
    username: root
    password: 123456
  jpa:
    show-sql: true # 默认 false,在日志里显示执行的 sql 语句
    database: mysql
    hibernate.ddl-auto: update #指定为 update,每次启动项目检测表结构有变化的时候会新增字段,表不存在时会新建,如果指定 create,则每次启动项目都会清空数据并删除表,再新建
    properties.hibernate.dialect: org.hibernate.dialect.MySQL5Dialect
    database-platform: org.hibernate.dialect.MySQL5Dialect
    hibernate:
      naming:
        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl #指定 jpa 的自动表生成策略,驼峰自动映射为下划线格式
        #physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

2. 添加 swagger 配置

添加一个 swagger 配置类,在工程下新建 config 包并添加一个 SwaggerConfig 配置类。

SwaggerConfig.java

package com.louis.springboot.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

@Bean
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Docket createRestApi(){
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .</span><span style="color: rgba(0, 0, 255, 1)">select</span><span style="color: rgba(0, 0, 0, 1)">()
            .apis(RequestHandlerSelectors.any())
            .paths(PathSelectors.any()).build();
}

</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> ApiInfo apiInfo(){
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> ApiInfoBuilder()
            .title(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">SpringBoot API Doc</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
            .description(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">This is a restful api document of Spring Boot.</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
            .version(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">1.0</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
            .build();
}

}

编写业务代码

添加一个用户类 User,包含用户名和密码,用来进行登录认证,另外用户可以拥有角色。

User.java

package com.louis.springboot.demo.model;

import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;

@Entity
public class User {

@Id
@GeneratedValue(strategy </span>=<span style="color: rgba(0, 0, 0, 1)"> GenerationType.AUTO)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Long id;
@Column(unique </span>= <span style="color: rgba(0, 0, 255, 1)">true</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String name;
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String password;
@OneToMany(cascade </span>= CascadeType.ALL,mappedBy = <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">user</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">private</span> List&lt;Role&gt;<span style="color: rgba(0, 0, 0, 1)"> roles;

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Long getId() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setId(Long id) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id =<span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getName() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> name;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setName(String name) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.name =<span style="color: rgba(0, 0, 0, 1)"> name;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> List&lt;Role&gt;<span style="color: rgba(0, 0, 0, 1)"> getRoles() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> roles;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> setRoles(List&lt;Role&gt;<span style="color: rgba(0, 0, 0, 1)"> roles) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.roles =<span style="color: rgba(0, 0, 0, 1)"> roles;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getPassword() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> password;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setPassword(String password) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.password =<span style="color: rgba(0, 0, 0, 1)"> password;
}

}

添加一个角色类 Role,表示用户角色,角色拥有可操作的权限集合。

Role.java

package com.louis.springboot.demo.model;

import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;

@Entity
public class Role {

@Id
@GeneratedValue(strategy </span>=<span style="color: rgba(0, 0, 0, 1)"> GenerationType.AUTO)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Long id;
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String roleName;
@ManyToOne(fetch </span>=<span style="color: rgba(0, 0, 0, 1)"> FetchType.EAGER)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> User user;
@OneToMany(cascade </span>= CascadeType.ALL,mappedBy = <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">role</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">private</span> List&lt;Permission&gt;<span style="color: rgba(0, 0, 0, 1)"> permissions;

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Long getId() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setId(Long id) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id =<span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getRoleName() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> roleName;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setRoleName(String roleName) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.roleName =<span style="color: rgba(0, 0, 0, 1)"> roleName;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> User getUser() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> user;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setUser(User user) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.user =<span style="color: rgba(0, 0, 0, 1)"> user;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> List&lt;Permission&gt;<span style="color: rgba(0, 0, 0, 1)"> getPermissions() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> permissions;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span> setPermissions(List&lt;Permission&gt;<span style="color: rgba(0, 0, 0, 1)"> permissions) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.permissions =<span style="color: rgba(0, 0, 0, 1)"> permissions;
}

}

添加一个权限类 Permission,表示资源访问权限。

Permission.java

package com.louis.springboot.demo.model;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Permission {

@Id
@GeneratedValue(strategy </span>=<span style="color: rgba(0, 0, 0, 1)"> GenerationType.AUTO)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Long id;
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> String permission;
@ManyToOne(fetch </span>=<span style="color: rgba(0, 0, 0, 1)"> FetchType.EAGER)
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> Role role;

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Long getId() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setId(Long id) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.id =<span style="color: rgba(0, 0, 0, 1)"> id;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String getPermission() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> permission;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setPermission(String permission) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.permission =<span style="color: rgba(0, 0, 0, 1)"> permission;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> Role getRole() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span><span style="color: rgba(0, 0, 0, 1)"> role;
}

</span><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">void</span><span style="color: rgba(0, 0, 0, 1)"> setRole(Role role) {
    </span><span style="color: rgba(0, 0, 255, 1)">this</span>.role =<span style="color: rgba(0, 0, 0, 1)"> role;
}

}

添加一个 DAO 基础接口,用来被其他 DAO 继承。

BaseDao.java

package com.louis.springboot.demo.dao;

import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;

@NoRepositoryBean
public interface BaseDao<T, I extends Serializable>
extends PagingAndSortingRepository
<T, I>, JpaSpecificationExecutor<T> {

}

添加一个 UserDao,用来操作用户信息。

UserDao.java

package com.louis.springboot.demo.dao;

import com.louis.springboot.demo.model.User;

public interface UserDao extends BaseDao<User, Long> {

User findByName(String name);

}

添加一个 RoleDao,用来操作角色信息。

RoleDao.java

package com.louis.springboot.demo.dao;

import com.louis.springboot.demo.model.Role;

public interface RoleDao extends BaseDao<Role, Long> {

}

添加一个 LoginService 服务接口。

LoginService.java

package com.louis.springboot.demo.service;

import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;

public interface LoginService {

User addUser(User user);

Role addRole(Role role);

User findByName(String name);

}

添加一个 LoginServiceImpl,实现服务功能,这里为了方便,在插入角色的时候会默认设置其权限。

LoginServiceImpl.java

package com.louis.springboot.demo.service.impl;

import java.util.ArrayList;
import java.util.List;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.louis.springboot.demo.dao.RoleDao;
import com.louis.springboot.demo.dao.UserDao;
import com.louis.springboot.demo.model.Permission;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;
import com.louis.springboot.demo.service.LoginService;

@Service
@Transactional
public class LoginServiceImpl implements LoginService {

@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> UserDao userDao;
@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> RoleDao roleDao;

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加用户</span>

@Override
public User addUser(User user) {
userDao.save(user);
return user;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">添加角色</span>

@Override
public Role addRole(Role role) {
User user
= userDao.findByName(role.getUser().getName());
role.setUser(user);
Permission permission1
= new Permission();
permission1.setPermission(
"create");
permission1.setRole(role);
Permission permission2
= new Permission();
permission2.setPermission(
"update");
permission2.setRole(role);
List
<Permission> permissions = new ArrayList<Permission>();
permissions.add(permission1);
permissions.add(permission2);
role.setPermissions(permissions);
roleDao.save(role);
return role;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">查询用户通过用户名</span>

@Override
public User findByName(String name) {
return userDao.findByName(name);
}
}

添加一个登录控制器,编写相关的接口。create 接口添加了 @RequiresPermissions("create"),用于进行权限注解测试。

LoginController.java

package com.louis.springboot.demo.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;
import com.louis.springboot.demo.service.LoginService;

@RestController
public class LoginController {

@Autowired
</span><span style="color: rgba(0, 0, 255, 1)">private</span><span style="color: rgba(0, 0, 0, 1)"> LoginService loginService;

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
 * POST登录
 * @param map
 * @return
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@PostMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/login</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String login(@RequestBody User user) {
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 添加用户认证信息</span>
    UsernamePasswordToken usernamePasswordToken = <span style="color: rgba(0, 0, 255, 1)">new</span><span style="color: rgba(0, 0, 0, 1)"> UsernamePasswordToken(user.getName(), user.getPassword());
    </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 进行验证,这里可以捕获异常,然后返回对应信息</span>

SecurityUtils.getSubject().login(usernamePasswordToken);
return "login ok!";
}

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
 * 添加用户
 * @param user
 * @return
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@PostMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/addUser</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String addUser(@RequestBody User user) {
    user </span>=<span style="color: rgba(0, 0, 0, 1)"> loginService.addUser(user);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">addUser is ok! \n</span><span style="color: rgba(128, 0, 0, 1)">"</span> +<span style="color: rgba(0, 0, 0, 1)"> user;
}

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
 * 添加角色
 * @param role
 * @return
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@PostMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/addRole</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String addRole(@RequestBody Role role) {
    role </span>=<span style="color: rgba(0, 0, 0, 1)"> loginService.addRole(role);
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">addRole is ok! \n</span><span style="color: rgba(128, 0, 0, 1)">"</span> +<span style="color: rgba(0, 0, 0, 1)"> role;
}

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
 * 注解的使用
 * @return
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@RequiresRoles(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">admin</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
@RequiresPermissions(</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">create</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
@GetMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/create</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String create() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">Create success!</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
}

@GetMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/index</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String index() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">index page!</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
}

@GetMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/error</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String error() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">error page!</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
}

</span><span style="color: rgba(0, 128, 0, 1)">/*</span><span style="color: rgba(0, 128, 0, 1)">*
 * 退出的时候是get请求,主要是用于退出
 * @return
 </span><span style="color: rgba(0, 128, 0, 1)">*/</span><span style="color: rgba(0, 0, 0, 1)">
@GetMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/login</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String login() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">login</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
}

@GetMapping(value </span>= <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">/logout</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 0, 255, 1)">public</span><span style="color: rgba(0, 0, 0, 1)"> String logout() {
    </span><span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(128, 0, 0, 1)">logout</span><span style="color: rgba(128, 0, 0, 1)">"</span><span style="color: rgba(0, 0, 0, 1)">;
}

}

添加一个 MyShiroRealm 并继承 AuthorizingRealm,实现其中的两个方法。

doGetAuthenticationInfo:实现用户认证,通过服务加载用户信息并构造认证对象返回。

doGetAuthorizationInfo:实现权限认证,通过服务加载用户角色和权限信息设置进去。

MyShiroRealm.java

package com.louis.springboot.demo.config;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import com.louis.springboot.demo.model.Permission;
import com.louis.springboot.demo.model.Role;
import com.louis.springboot.demo.model.User;
import com.louis.springboot.demo.service.LoginService;

/

  • 实现 AuthorizingRealm 接口用户用户认证

  • @author Louis

  • @date Jun 20, 2019
    */
    public class MyShiroRealm extends AuthorizingRealm {

    @Autowired
    private LoginService loginService;

    /

    • 用户认证
      */
      @Override
      protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
      throws AuthenticationException {
      // 加这一步的目的是在 Post 请求的时候会先进认证,然后在到请求
      if (authenticationToken.getPrincipal() == null) {
      return null;
      }
      // 获取用户信息
      String name = authenticationToken.getPrincipal().toString();
      User user
      = loginService.findByName(name);
      if (user == null) {
      // 这里返回后会报出对应异常
      return null;
      }
      else {
      // 这里验证 authenticationToken 和 simpleAuthenticationInfo 的信息
      SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(name,
      user.getPassword().toString(), getName());
      return simpleAuthenticationInfo;
      }
      }

    /

    • 角色权限和对应权限添加
      */
      @Override
      protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
      // 获取登录用户名
      String name = (String) principalCollection.getPrimaryPrincipal();
      // 查询用户名称
      User user = loginService.findByName(name);
      // 添加角色和权限
      SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
      for (Role role : user.getRoles()) {
      // 添加角色
      simpleAuthorizationInfo.addRole(role.getRoleName());
      for (Permission permission : role.getPermissions()) {
      // 添加权限
      simpleAuthorizationInfo.addStringPermission(permission.getPermission());
      }
      }
      return simpleAuthorizationInfo;
      }

}

添加一个 Shiro 配置类,主要配置路由的访问控制,以及注入自定义的认证器 MyShiroRealm。

ShiroConfig.java

package com.louis.springboot.demo.config;

import java.util.HashMap;
import java.util.Map;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 将自己的验证方式加入容器</span>

@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm
= new MyShiroRealm();
return myShiroRealm;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 权限管理,配置主要是Realm的管理认证</span>

@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager
= new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> Filter工厂,设置对应的过滤条件和跳转条件</span>

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean
= new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map
<String, String> filterMap = new HashMap<String, String>();
// 登出
filterMap.put("/logout", "logout");
// swagger
filterMap.put("/swagger**/", "anon");
filterMap.put(
"/webjars/
", "anon");
filterMap.put(
"/v2/", "anon");
// 对所有用户认证
filterMap.put("/
", "authc");
// 登录
shiroFilterFactoryBean.setLoginUrl("/login");
// 首页
shiroFilterFactoryBean.setSuccessUrl("/index");
// 错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}

</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 加入注解的使用,不加入这个注解不生效</span>

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

编译测试运行

1.  右键项目 -> Run as -> Maven install,开始执行 Maven 构建,第一次会下载 Maven 依赖,可能需要点时间,如果出现如下信息,就说明项目编译打包成功了。

2.  右键文件 DemoApplication.java -> Run as -> Java Application,开始启动应用,如果一开始数据库没有对应的表,在应用启动时会创建,我们可以通过控制台查看到对应的 SQL 语句。

3.  打开浏览器,访问:http://localhost:8080/swagger-ui.html,进入 swagger 接口文档界面。

4.  首先用 MySQL 客户端,打开数据库,往用户表里面插入一条记录,{id=1, name="admin", password="123"}。

然后试着用 Swagger 调用 addUser 往用户表插入一条记录。

{
  "id": 2,
  "name": "xiaoming",
  "password": "123"
}

结果返回 "error page!",这是因为我们没有登录,还没有操作权限。

接着调用 POST 的 login 接口,输入以下用户信息进行登录。

{
  "name": "admin",
  "password": "123"
}

登录成功之后,返回“login ok!”信息。

再次调用 addUser 往用户表插入记录,发现记录已经可以成功插入了。

通过客户端工具我们也可以查看到记录已经插入进来了。

通过上面的测试,我们已经成功的验证了,受保护的接口需要在登录之后才允许访问。接下来我们来测试一下权限注解的效果,我们在 create 方法上加上了权限注解 @RequiresPermissions("create"),表示用户需要拥有 "create" 的权限才能访问。

先尝试调用以下 create 接口,发现尽管我们已经登录了,依然因为没有权限返回了“error page!”。

然后我们调用 addRole 插入以下角色记录,这个角色关联了我们当前登录 admin 用户,且角色在创建时我们代码默认设置拥有了“create”权限。

{
  "id": 1,
  "roleName": "admin",
  "user": {
    "name": "admin"
  }
}

如果执行正确的话,会返回如下信息,说明角色已经成功插入了。

然后我们再一次调用 create 接口,因为此刻 admin 用户拥有 admin 角色,而 admin 角色拥有“create”权限,所以已经具有接口访问权限了。

 

参考资料

项目主页:http://shiro.apache.org/

W3C 资料:https://www.w3cschool.cn/shiro/

百度百科:https://baike.baidu.com/item/shiro/17753571?fr=aladdin

相关导航

Spring Boot 系列教程目录导航

Spring Boot:快速入门教程

Spring Boot:整合 Swagger 文档

Spring Boot:整合 MyBatis 框架

Spring Boot:实现 MyBatis 分页

源码下载

码云:https://gitee.com/liuge1988/spring-boot-demo.git


作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/ 
版权所有,欢迎转载,转载请注明原文作者及出处。