单realm模式下前后端分离实现springboot+shiro+jwt+vue整合

shiro+jwt实现前后端分离

一、RBAC概念

基于角色的权限访问控制(Role-Based Access Control)作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。

这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。

角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。

第一类权限信息: 只有用户和权限表 1对多

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第1张图片

如果用户群体大, 是不是每一个用户都需要配置一遍?? 如果要去掉某一批基层员工的某个权限, 就需要去修改每个人的权限信息!

第二类加中间表: 和第一类 仅仅只是sql语句存储的数据不同

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第2张图片

第三类: 有角色表!

画图讲解^ ^用户 角色 权限 如果没有角色会怎样??

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第3张图片

在RBAC模型中,角色是系统根据管理中相对稳定的职权和责任来划分,每种角色可以完成一定的职能。用户通过饰演不同的角色获得角色所拥有的权限,一旦某个用户成为某角色的成员,则此用户可以完成该角色所具有的职能。通过将权限指定给角色而不是用户,在权限分派上提供了极大的灵活性和极细的权限指定粒度。

第四类[了解]: 加菜单

第五类[了解]: 租户 SAAS系统! 通用服务! 内部系统—定制化开发!

第六类:中台

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第4张图片

第七类:未来趋势DDD

二、常见的认证机制

1. HTTP Basic Auth

HTTP Basic Auth简单点说明就是每次请求API时都提供用户的username和password,简言之,Basic Auth是配合RESTful API 使用的最简单的认证方式,只需提供用户名密码即可,但由于有把用户名密码暴露给第三方客户端的风险,在生产环境下被使用的越来越少。因此,在开发对外开放的RESTful API时,尽量避免采用HTTP Basic Auth

2. Cookie Auth

Cookie认证机制就是为一次请求认证在服务端创建一个Session对象,同时在客户端的浏览器端创建了一个Cookie对象;通过客户端带上来Cookie对象来与服务器端的session对象匹配来实现状态管理的。默认的,当我们关闭浏览器的时候,cookie会被删除。但可以通过修改cookie 的expire time使cookie在一定时间内有效

3. OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。 OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用。

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第5张图片

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第6张图片

4. Token Auth

使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:

  1. 客户端使用用户名跟密码请求登录

  2. 服务端收到请求,去验证用户名与密码 + 盐

  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端

  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里

  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token

  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第7张图片Token Auth的优点:

1.支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
2.无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
3.更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
4.去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
5.更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
6.CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
7.性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
8.不需要为登录页面做特殊处理: 如果你使用postman做功能测试的时候,不再需要为登录页面做特殊处理.
9.基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby,Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft).

三、JWT入门概念

Json Web Token 是一种令牌!

1. 快速入门

1.1 token的创建

(1)创建maven工程,引入依赖

<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwtartifactId>
    <version>0.9.1version>
dependency>

(2)创建类CreateJwtTest,用于生成token

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

/**
 * @author: mayuhang  
* Date: 2021/4/29:1:45
* Description: */
public class CreateJwtTest { public static void main(String[] args) { JwtBuilder builder= Jwts.builder().setId("888") .setSubject("管理员") .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256,"woniuxy"); System.out.println( builder.compact() ); } }

(3)测试运行,输出如下:

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiLnrqHnkIblkZgiLCJpYXQiOjE2MTk2MzIwMzJ9.5de6yHqZn7SMBduX9Xxu04w4WfREjyRLp5e9iqzSnjE

1.2 token的解析

我们刚才已经创建了token ,在web应用中这个操作是由服务端进行然后发给客户端,客户端在下次向服务端发送请求时需要携带这个token(这就好像是拿着一张门票一样),那服务端接到这个token 应该解析出token中的信息(例如用户id),根据这些信息查询数据库返回相应的结果。
创建ParseJwtTest

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

import java.io.UnsupportedEncodingException;

/**
 * @author: mayuhang  
* Date: 2021/4/29:1:48
* Description: */
public class ParseJwtTest { public static void main(String[] args) { String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJBRE1JTiIsImlhdCI6MTYxOTYzMjQ3NH0.-TJRtXOjhjoU1oD7Y1VLAp2dSQeaVK7aBzRj0ZtTfPw"; Claims claims = Jwts.parser().setSigningKey("woniuxy").parseClaimsJws(token).getBody(); System.out.println("id:"+claims.getId()); System.out.println("subject:"+claims.getSubject()); System.out.println("IssuedAt:"+claims.getIssuedAt()); } }

1.3 自定义claims

我们刚才的例子只是存储了id和subject两个信息,如果你想存储更多的信息(例如角色)可以定义自定义claims
(1) 创建CreateJwtTest3,并存储指定的内容

import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

/**
 * @author: mayuhang  
* Date: 2021/4/29:1:45
* Description: */
public class CreateJwtTest { public static void main(String[] args) { //为了方便测试,我们将过期时间设置为1分钟 long now = System.currentTimeMillis();//当前时间 long exp = now + 1000*60;//过期时间为1分钟 JwtBuilder builder= Jwts.builder().setId("888") .setSubject("ADMIN") .setIssuedAt(new Date()) .signWith(SignatureAlgorithm.HS256,"woniuxy") .setExpiration(new Date(exp)) .claim("roles","admin") //自定义claims存储数据 .claim("logo","logo.png");; System.out.println( builder.compact() ); } }

(2) 解析

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;

import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author: mayuhang  
* Date: 2021/4/29:1:48
* Description: */
public class ParseJwtTest { public static void main(String[] args) { String token="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI4ODgiLCJzdWIiOiJBRE1JTiIsImlhdCI6MTYxOTYzMjg1NSwiZXhwIjoxNjE5NjMyOTE1LCJyb2xlcyI6ImFkbWluIiwibG9nbyI6ImxvZ28ucG5nIn0.D5SFUTBzCvGKp6MECT4-L3pIvU0Umnm__tm-zPSrf1U"; Claims claims = Jwts.parser().setSigningKey("woniuxy").parseClaimsJws(token).getBody(); System.out.println("id:"+claims.getId()); System.out.println("subject:"+claims.getSubject()); System.out.println("IssuedAt:"+claims.getIssuedAt()); System.out.println("roles:"+claims.get("roles")); System.out.println("logo:"+claims.get("logo")); SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); System.out.println("签发时间:"+sdf.format(claims.getIssuedAt())); System.out.println("过期时间:"+sdf.format(claims.getExpiration())); System.out.println("当前时间:"+sdf.format(new Date()) ); } }

四、Shiro权限框架

现在系统需要实现:
超级管理员登录系统,显示所有的功能
而普通管理员登录系统,显示他对应的功能

如果没有Shiro这种权限框架的情况,创建 role_info menu_info role_menu表

登录的时候,查询后端的权限,并根据权限展示页面

但这套逻辑需要自己写,很麻烦!

1. Shiro的简介

公司项目中,常见的权限框架:shiro | spring security
Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。

Apache Shiro 的首要目标是易于使用和理解。权限是很复杂的,甚至是痛苦的,但它没有必要这样。框架应该尽可能掩盖( 黑盒 | 空调)复杂的地方,露出一个干净而直观的 API(遥控器),来简化开发人员在使他们的应用程序安全上的努力。

Shiro能帮系统做什么:
1、做用户的身份认证(登录),判断用户是否系统用户(重点)
2、给系统用户授权,用来帮助系统实现不同的用户展示不同的功能(重点)
3、针对密码等敏感信息,进行加密处理(明文变成密文)(重点)
4、提供了Session管理,但是它的Session不是HttpSession,是它自己自带的
5、做授权信息的缓存管理,降低对数据库的授权访问
6、提供测试支持,因为它也是一个轻量级框架,它也可以直接针对代码进行使用Junit单元测试
7、提供Remember me的功能,可以做用户无需再次登录即可访问某些页面
8、启用单点登录(SSO)功能。

2. Shiro提供的10大功能

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第8张图片

  1. Authentication 身份认证/登录,验证用户是不是拥有相应的身份;

  2. Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

  3. Session Management 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

  4. Cryptography 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

  5. Web Support Web支持,可以非常容易的集成到Web环境;

  6. Caching 缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

  7. Concurrency shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

  8. Testing : 提供测试支持;

  9. Run As +:可以模仿一个账户。

  10. Remember Me 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了

3. Shiro的架构图

接下来从Shiro内部来看下Shiro的架构,如下图所示:
单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第9张图片

**Subject主体:**可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager: 相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

**Authenticator认证器:**负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证(登录)通过了;

Authrizer授权器: 或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm域(面试常问): 可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager: 如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;
接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可
以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDAO: DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager 缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography 密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。

4. Shiro框架的3个核心类

首先,从外部来看Shiro,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第10张图片

Subject主体: 需要登录系统的东西,都是主体。 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager,可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager安全管理器: 即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与其他组件进行交互,相当于DispatcherServlet前端控制器;

Realm域: 一个用来做身份认证,以及授权的对象 Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,就是一个跟权限数据有关的数据源。

记住一点: Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第11张图片

5. shiro入门程序

1.搭建基于ini文件的运行环境

​ pom中导入shiro坐标:(plugins插件仅仅只是为了处理maven和idea兼容导致的启动问题)


<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.0modelVersion>

    <groupId>com.woniuxygroupId>
    <artifactId>shiro01artifactId>
    <version>1.0-SNAPSHOTversion>
    <dependencies>
        <dependency>
            <groupId>org.apache.shirogroupId>
            <artifactId>shiro-coreartifactId>
            <version>1.7.1version>
        dependency>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
            <scope>testscope>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin> <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-resources-pluginartifactId>
                <version>2.7version>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.sharedgroupId>
                        <artifactId>maven-filteringartifactId>
                        <version>1.3version>
                    dependency>
                dependencies>
            plugin>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombokgroupId>
                            <artifactId>lombokartifactId>
                        exclude>
                    excludes>
                configuration>
            plugin>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-surefire-pluginartifactId>
                <version>2.22.2version>
                <configuration>
                    <skipTests>trueskipTests>
                configuration>
            plugin>
        plugins>
    build>
project>

2.用户认证 BasicIniEnvironment

认证:身份认证/登录,验证用户是不是拥有相应的身份。基于shiro的认证,是通过subject的login方法完成用户认证工作的
(1)在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-auth.ini)

[users]
#模拟从数据库查询的用户
#数据格式 用户名=密码
mayun=123456
mayuhang=654321

(2)测试用户认证

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.env.BasicIniEnvironment;
import org.apache.shiro.env.Environment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;

/**
 * @author: mayuhang  
* Date: 2021/4/29:0:32
* Description: */
public class TestLogin { @Test public void testLogin() throws Exception{ //1.加载ini配置文件创建SecurityManager Environment environment = new BasicIniEnvironment("classpath:shiro-auth.ini"); //2.获取securityManager SecurityManager securityManager = environment.getSecurityManager(); //3.将securityManager绑定到当前运行环境 SecurityUtils.setSecurityManager(securityManager); //4.创建主体(此时的主体还为经过认证) Subject subject = SecurityUtils.getSubject(); /** * 模拟登录,和传统登陆了方式等不同的是需要使用主体进行登录 */ //5.构造主体登录的凭证(即用户名/密码) //第一个参数:登录用户名,第二个参数:登录密码 UsernamePasswordToken token = new UsernamePasswordToken("mayun","123456"); //6.主体登录 subject.login(token); //7.验证是否登录成功 System.out.println("用户登录成功, 认证状态:"+subject.isAuthenticated()); //8.登录成功获取数据 //getPrincipal 获取登录成功的安全数据 System.out.println("从subject中获取用户信息:"+subject.getPrincipal()); } }

打印结果:

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第12张图片

3.用户授权

授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
(1)在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-prem.ini)

[users]
#模拟从数据库查询的用户
#数据格式 用户名=密码,角色1,角色2..
mayun=123456,role1,role2
mayuhang=654321,role3
[roles]
#模拟从数据库查询的角色和权限列表
#数据格式 角色名=权限1,权限2
role1=user:save,user:update
role2=user:update,user.delete
role3=user:find

(2)完成用户授权

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.env.BasicIniEnvironment;
import org.apache.shiro.env.Environment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;

/**
 * @author: mayuhang  
* Date: 2021/4/29:22:44
* Description: */
public class TestPerm { @Test public void testPerm() throws Exception { //1.加载ini配置文件创建SecurityManager Environment environment = new BasicIniEnvironment("classpath:shiro-auth.ini"); //2.获取securityManager SecurityManager securityManager = environment.getSecurityManager(); //3.将securityManager绑定到当前运行环境 SecurityUtils.setSecurityManager(securityManager); //4.创建主体(此时的主体还为经过认证) Subject subject = SecurityUtils.getSubject(); /** * 模拟登录,和传统等不同的是需要使用主体进行登录 */ //5.构造主体登录的凭证(即用户名/密码) //第一个参数:登录用户名,第二个参数:登录密码 UsernamePasswordToken token = new UsernamePasswordToken("mayuhang", "654321"); //6.主体登录 subject.login(token); //7.用户认证成功之后才可以完成授权工作 boolean hasPerm = subject.isPermitted("user:find"); System.out.println("从subject中获取用户信息:"+subject.getPrincipal()); System.out.println("用户是否具有find权限=" + hasPerm); } }

subject.login(UsernamePasswordToken … token) //执行shiro内置的登录认证

subject.isAuthenticated() //是否认证(登录)成功(成功就是true)

subject.getPrincipal() //获取登录的用户名

subject.hasRole(String … perm) //是否这个角色

subject.isPermitted(String …perm) //是否又这个权限

4.自定义域(重点 面试) 只要记住 继承AuthorizingRealm

Realm域:Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

(1)自定义域(Realm)

package com.woniuxy.shiro;

import org.apache.shiro.authc.*;
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 java.util.ArrayList;
import java.util.List;

/**
 * @author: mayuhang  
* Date: 2021/4/29:23:14
* Description: 自定义域 * 继承AuthorizingRealm * 重写 AuthorizationInfo Z 授权 * AuthenticationInfo C 认证 */
public class MyRealm extends AuthorizingRealm { @Override public void setName(String name){ super.setName("myRealm"); } /** * 授权:授权的主要目的就是查询 数据库获取当前用户的所有角色和权限信息 * * 在前后端不分离的情况下, * 在html页面使用|后端使用shiro的@checkRole,@checkPermission相关注解才能触发 * 其实前后端分离项目, 这个授权意义就不大了, 不通过controller加注解来控制权限 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 1.从principals获取已认证用户的信息 类比之前打印的方法 "从subject中获取用户信息:"+subject.getPrincipal(); String username = (String) principals.getPrimaryPrincipal(); /** * 正式系统:应该从数据库中根据用户名或者id查询 所有权限 * 这里为了演示,手动构造 */ // 2.模拟从数据库中查询的用户所有权限 List<String> permissions = new ArrayList<String>(); permissions.add("user:save");// 用户的创建 permissions.add("user:update");// 商品添加权限 // 3.模拟从数据库中查询的用户所有角色 List<String> roles = new ArrayList<String>(); roles.add("role1"); roles.add("role2"); // 4.构造权限数据(就是个实体类^ ^) SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); // 5.通过当前查询的权限数据保存到simpleAuthorizationInfo simpleAuthorizationInfo.addStringPermissions(permissions); // 6.通过当前用户,查询的角色数据保存到simpleAuthorizationInfo simpleAuthorizationInfo.addRoles(roles); return simpleAuthorizationInfo; } /** * 身份认证方法:认证的主要目的,比较用户输入的用户名密码是否和数据库中的一致 * * 需要在用户登录系统时触发 * 该方法将是我们主要的方法, 流程是登录后, 携带token 与权限信息 发送到 vue中 存储浏览器 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //1.一个简单的用户名/密码身份验证令牌,以支持使用最广泛的身份验证机制 UsernamePasswordToken upToken = (UsernamePasswordToken)token; //2.获取输入的用户名密码 String username = upToken.getUsername(); String password = new String(upToken.getPassword()); /** * 3.验证用户名密码是否正确 * 正式系统:应该从数据库中通过用户查询密码(一般会加密), 比较密码是否一致 * 此处测试,只要输入的密码为123456则登录成功 */ if(!password.equals("123456")) { throw new RuntimeException("用户名或密码错误");//抛出异常表示认证失败 }else { SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,password,this.getName()); return info; } } }

(2)配置shiro的ini配置文件(shiro-realm.ini)

[main]
#声明realm
permReam=com.woniuxy.shiro.MyRealm
#注册realm到securityManager中
securityManager.realms=$permReam

(3)测试验证流程

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.env.BasicIniEnvironment;
import org.apache.shiro.env.Environment;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

/**
 * @author: mayuhang  
* Date: 2021/4/29:23:43
* Description: */
public class TestRealm { @Test public void testPerm() throws Exception { //1.加载ini配置文件创建SecurityManager Environment environment = new BasicIniEnvironment("classpath:shiro-realm.ini"); //2.获取securityManager SecurityManager securityManager = environment.getSecurityManager(); //3.将securityManager绑定到当前运行环境 SecurityUtils.setSecurityManager(securityManager); //4.创建主体(此时的主体还为经过认证) Subject subject = SecurityUtils.getSubject(); /** * 模拟登录,和传统等不同的是需要使用主体进行登录 */ //5.构造主体登录的凭证(即用户名/密码) //第一个参数:登录用户名,第二个参数:登录密码 UsernamePasswordToken upToken = new UsernamePasswordToken("mayuhang", "123456"); //6.主体登录 subject.login(upToken); //7.用户认证成功之后才可以完成授权工作 //认证 是否登录成功 System.out.println("判断"+subject.getPrincipal()+"是否认证成功"+subject.isAuthenticated()); //是否有这个角色 或者 权限 System.out.println("判断是否有role1的角色"+subject.hasRole("role1")); System.out.println("判断是否有user:insert的权限"+subject.isPermitted("user:insert")); System.out.println("判断是否有user:add的权限"+subject.isPermitted("user:add")); } }

我们通过自定义realm实现了认证! 不再通过 ini文件写死账号密码来认证! 自定义域 也就是以后我们在项目开发过程中, 用的方式!

课上代码:

package com.woniuxy.shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.HashSet;
import java.util.Set;

/**
 * @author: mayuhang  
* Date: 2021/4/30:15:21
* Description: 自定义域 这个是给 单一用户 进行 认证和授权的! * 认证的数据放什么实体中: SimpleAuthenticationInfo * 授权的数据(角色和权限)放什么实体中: SimpleAuthorizationInfo * 这个两个对象去了哪里呢?? * SecurityManager 它管理了我们单一用户的认证授权信息 以及 所有的 subject */
public class MyRealm extends AuthorizingRealm{ @Override public String getName() { return "MyRealm"; } //授权 授予登录的这个角色和权限! @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { //通过形参 获取用户名 String username = (String) principals.getPrimaryPrincipal(); /* *通过用户名 去数据库查询 所有的角色 和 权限 * *先去数据库查询当前用户 所有的角色 * *再去通过 所有的角色 查询出所有的权限 * */ Set<String> roles = new HashSet(); roles.add("role1"); roles.add("role2"); Set<String> perms = new HashSet(); perms.add("user:add"); perms.add("user:delete"); perms.add("user:update"); perms.add("user:find"); perms.add("user:find"); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRoles(roles); simpleAuthorizationInfo.addStringPermissions(perms); return simpleAuthorizationInfo; } //认证 确定你的用户名 密码正确 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //自定义实现 登录验证 UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token; //我在自定义认证方法中,可以通过 入参 拿到我的账号和密码 String username = usernamePasswordToken.getUsername(); String password =new String(usernamePasswordToken.getPassword()); //1.用账号去数据库查询出你的密码! 密码一般加密的 //2.把当前变量的 password用同样的方式 加密, 然后 和数据库查出的密码进行比对! //3.比对两个密码! 我们一般使用的是非对称加密!(找回密码!忘记密码! 一般验证账号正确, 重置密码!) if(password.equals("123456")){ return new SimpleAuthenticationInfo(username,password,getName()); } return null; } }

五、基于JWT+shiro实现前后端分离认证开始

1. 引入依赖关系

        <!-- 引入shiro的相关依赖 就可以不用引入springboot-web启动器了 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.7.1</version>
        </dependency>
  <!-- 引入jwt的相关依赖 -->
         <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.10.3</version>
        </dependency>
	<!-- 额外的Mybatis  mysql驱动 lombok devtools热启动器 springboot启动器 -->
   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.49</version>
        </dependency>
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>
    </dependencies>     

2. JWT相关工具类

​ 引入JWT后呢, UsernamePasswordToken就不能拿来直接用了, 不过如果有多个自定义域的模式, 倒是可以写2套, 一套登录使用UsernamePasswordToken, 第二套, 验证使用MyJsonWebToken.

​ 我们不考虑多域模式, 以单一自定义域的方式开发!

2.1 用MyJsonWebToken替换UsernamePasswordToken 实体类

package com.woniuxy.shiro.utils;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.shiro.authc.AuthenticationToken;

/**
 * @author: mayuhang  
* Date: 2021/4/23:0:01
* Description: 存放token返回token 模仿UsernamePasswordToken 将替换原本的登录方式 * SimpleAuthenticationInfo对象中的Principal 和 Credentials 是根据你自定义域中的认证方法来的,你怎么放 */
@AllArgsConstructor @Data @NoArgsConstructor public class MyJsonWebToken implements AuthenticationToken { String token; @Override public Object getPrincipal() { return token; } @Override public Object getCredentials() { return token; } }

2.2 JWT工具类(了解即可)

package com.woniuxy.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.Date;

/**
 * @author: mayuhang  
* Date: 2021/4/22:23:41
* Description: 工具类 生成JWT 和 */
public class JWTUtil { //过期时间5分钟 private static final long EXPIRE_TIME = 5*60*1000; /** * Description : 校验token是否正确,错了就直接异常了 所以就直接返回true即可
* ChangeLog : 1. 创建 (2021/5/3 21:58 [mayuhang]); * @param token 密钥 * username 用户名 * secret 用户的密码 用于加密 作为解密条件 类似之前jwt教学中的signWith * //签名算法 以及签的名字 * .signWith(SignatureAlgorithm.HS256, "woniuxy"); * @return 是否正确 **/
public static boolean verify(String token,String username,String secret){ //这里需要和生成签名的签名算法保持一致~ Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build(); verifier.verify(token); return true; } /** * Description : 获得token中的信息无需secret解密也能获得
* ChangeLog : 1. 创建 (2021/5/3 22:01 [mayuhang]); * @param token * @return token中包含的用户名 **/
public static String getUserName(String token){ DecodedJWT jwt = JWT.decode(token); return jwt.getClaim("username").asString(); } /** * Description : 生成签名,5min后过期
* ChangeLog : 1. 创建 (2021/5/3 22:01 [mayuhang]); * @param username 用户名 * @param secret 用户的密码 * @return 加密的token **/
public static String sign(String username,String secret){ Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); Algorithm algorithm = Algorithm.HMAC256(secret); return JWT.create() .withClaim("username",username) .withExpiresAt(date) .sign(algorithm); } }

2.3 JWTFilter过滤器

还记得这个springmvc中的过滤器么? 代码执行流程如下:

preHandle->isAccessAllowed->isLoginAttempt->executeLogin

package com.woniuxy.shiro.utils;

import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: mayuhang  
* Date: 2021/4/23:0:24
* Description:继承官方的BasicHttpAuthenticationFilter,并且重写鉴权的方法 * 代码的执行流程preHandle->isAccessAllowed->isLoginAttempt->executeLogin */
@Component public class JWTFilter extends BasicHttpAuthenticationFilter { /** * 对跨域提供支持 */ @Override protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; HttpServletResponse httpServletResponse = (HttpServletResponse) response; httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE"); httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态 if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) { httpServletResponse.setStatus(HttpStatus.OK.value()); return false; } return super.preHandle(request, response); } /** * 这里我们详细说明下为什么最终返回的都是true,即允许访问 * 例如我们提供一个地址 GET /article * 登入用户和游客看到的内容是不同的 * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西 * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入 * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可 */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { if (isLoginAttempt(request, response)) { try { //如果有token executeLogin(request, response); } catch (Exception e) { //如果请求头没有带Authorization属性的token 就到这里咯 response401(request, response); //如果抛出异常,则不进入controller,否则及时异常,还是会进入controller执行代码 return false; } } return true; } /** * 判断用户是否想要登入。 * 检测header里面是否包含Authorization字段即可 */ @Override protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) { HttpServletRequest hsr = (HttpServletRequest) request; String authorization = hsr.getHeader("Authorization"); return authorization!=null; } @Override protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = (HttpServletRequest) request; //从请求头中获取token String authorization = httpServletRequest.getHeader("Authorization"); //放入对象中, redis或者数据库查询 等同于之前的UsernamePasswordToken MyJsonWebToken token = new MyJsonWebToken(authorization); // 提交给realm进行登入,如果错误他会抛出异常并被捕获 System.out.println("JWTFilter.executeLogintoken交给realm判断:"+token); //这个等同于直接的 subject.login(token) getSubject(request, response).login(token); // 如果没有抛出异常则代表登入成功,返回true return true; } /** * 将非法请求跳转到 /401 */ private void response401(ServletRequest req, ServletResponse resp) { try { HttpServletResponse httpServletResponse = (HttpServletResponse) resp; httpServletResponse.sendRedirect("/401"); } catch (IOException e) { System.err.println(e.getMessage()); } } }

3. 编写自定义域

package com.woniuxy.shiro.utils;

import com.woniuxy.entity.RbacManager;
import com.woniuxy.entity.RbacPerm;
import com.woniuxy.service.RbacPermService;
import com.woniuxy.service.impl.UserService;
import com.woniuxy.utils.JWTUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;

/**
 * @author: mayuhang  
* Date: 2021/4/23:0:02
* Description:自定义域 自己写登录验证 和授权 */
@Configuration public class MyRealm extends AuthorizingRealm { @Autowired UserService userService; @Autowired RbacPermService rbacPermService; @Override public void setName(String name){ super.setName("myRealm"); } // 这个需要注意下, 多域模式中, 每个域里面 请自己配置自己的token来源 // 这里的token是来自 自定义的JWTToken // 必须重写此方法,不然 传的jwt, shiro却使用的是UsernamePasswordToken进行解析, // 会报一个错, 你token格式不对(JWT的值是3部分, UsernamePasswordToken只有1节....) @Override public boolean supports(AuthenticationToken token) { return token instanceof MyJsonWebToken; } /** * 授权信息 * 在前后端不分离的情况下, 在html页面使用|后端使用shiro的@checkRole,@checkPermission相关注解才能触发 * 前后端分离的模式, 就直接用注解验证权限即可, 根据需求来 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("MyRealm.doGetAuthorizationInfo, 授权方法进入!!!"); //这里得去获取到这个token 不用token过滤器 直接从redis里面拿数据!! String userName = JWTUtil.getUserName(principals.toString()); //去数据库查出用户信息, 含角色(也可含权限, 看你实体怎么定义咯= =) RbacManager user = userService.getUser(userName); //授权信息集合 SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //1.如果需求是角色可以配置多个,则如下获取所有角色 并放入授权信息集合(模拟多角色查询,虽然目前是单的) String roles = String.valueOf(user.getRoleId()); if (!"".equals(roles)&&roles!=null){ String[] roles1 = roles.split(","); for (String role: roles1) { simpleAuthorizationInfo.addRole(role); } } //2.目前是单个角色则直接这样用吧..... // simpleAuthorizationInfo.addRole(user.getRoleId()+""); //3.通过角色, 去查询中间表 找到所有的权限 获取tyep字段,a接口权限link细节信息(同第1步) List<RbacPerm> forRole = rbacPermService.findForRole(roles); //4.转成HashSet去重存放 HashSet<String> permissions = new HashSet<>(); for (RbacPerm rbacPerm : forRole) { if(rbacPerm.getCode()!=null){ permissions.add(rbacPerm.getCode()); } } //5.放入simpleAuthorizationInfo对象中 simpleAuthorizationInfo.addStringPermissions(permissions); return simpleAuthorizationInfo; } /** * 身份认证方法 * 需要在用户登录系统时触发 * 该方法将是我们主要的方法, 流程是登录后, 携带token 与权限信息 发送到 vue中 存储浏览器 * 在页面渲染时,直接传入菜单和 页面按钮显示权限 * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException { System.out.println("MyRealm.doGetAuthenticationInfo, 用户认证方法进入!"); String myJWT = (String) auth.getCredentials(); String username = JWTUtil.getUserName(myJWT); if (username==null){ throw new AuthenticationException("token无效"); } //通过用户名,查出当前用户密码 RbacManager user = userService.getUser(username); if (user==null){ throw new AuthenticationException("User 不存在"); } //通过存入的myJWT与传入的账号密码进行验证, 账号密码是否正确(类比之前手动比较密码~~~~) if (JWTUtil.verify(myJWT,username,user.getPassword())) { return new SimpleAuthenticationInfo(myJWT,myJWT,this.getName()); }else { throw new AuthenticationException("用户名或者密码错误"); } } }

4. 编写配置类ShiroConfig

4.1 需要配置禁用session,安全管理器,shiro过滤器, 注解支持 ,生命周期,

 package com.woniuxy.shiro.utils;

import org.apache.shiro.mgt.SessionStorageEvaluator;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: mayuhang  
* Date: 2021/4/23:0:35
* Description: shiro核心配置, 中央大脑 SecurityManager 相关配置 等同于之前玩 shiro-realm.ini的时候 */
@Configuration public class ShiroConfig { /** * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。 * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session */ @Bean protected SessionStorageEvaluator sessionStorageEvaluator() { DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator(); sessionStorageEvaluator.setSessionStorageEnabled(false); return sessionStorageEvaluator; } //配置安全管理器, 传入自己写的自定义域 @Bean public DefaultWebSecurityManager securityManager(MyRealm realm) { //使用默认的安全管理器 DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm); //将自定义的realm交给安全管理器统一调度管理 securityManager.setRealm(realm); return securityManager; } //Filter工厂,设置对应的过滤条件和跳转条件 @Bean("shiroFilterFactoryBean") public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager securityManager) { //1.创建shiro过滤器工厂 ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean(); //2.设置安全管理器 filterFactory.setSecurityManager(securityManager); //3.通用配置,如果没有前后端分离配置这个(配置登录页面,登录成功页面,验证未成功页面) // filterFactory.setLoginUrl("/autherror?code=1"); //设置登录页面 // filterFactory.setUnauthorizedUrl("/autherror?code=2"); //授权失败跳转页面 //4.配置过滤器集合 /** * key :访问连接 * 支持通配符的形式 * value:过滤器类型 * shiro常用过滤器类型 * anno :匿名访问(表明此链接所有人可以访问) * authc :认证后访问(表明此链接需登录认证成功之后可以访问) */ // Map filterMap = new LinkedHashMap(); // 配置不会被拦截的链接 顺序判断 // filterMap.put("/user/home", "anon"); // filterMap.put("/login", "anon"); // filterMap.put("/user/**", "authc"); //5.设置自定义jwt过滤器 Map<String, Filter> jwt = new HashMap<>(); jwt.put("jwt",new JWTFilter()); filterFactory.setFilters(jwt); /* * 6设置所有的请求都经过我们的JWTfilter * 自定义url规则 * http://shiro.apache.org/web.html#urls- */ Map<String, String> filterRuleMap = new HashMap<>(); // 所有请求通过我们自己的JWT Filter filterRuleMap.put("/**", "jwt"); // 访问401和404页面不通过我们的Filter filterRuleMap.put("/401", "anon"); filterFactory.setFilterChainDefinitionMap(filterRuleMap); return filterFactory; } //开启shiro注解支持 @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } /** 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能 */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** * Description : 管理Subject主体对象,生命周期的组件,用户只是打印下生产销毁日志什么的,请参考spring中bean的生命周期
* ChangeLog : 1. 创建 (2021/4/27 0:25 [mayuhang]); * * @return org.apache.shiro.spring.LifecycleBeanPostProcessor **/
@Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } }

4.2 Shiro中过滤器的类型,以及ShiroConfig配置

单realm模式下前后端分离实现springboot+shiro+jwt+vue整合_第13张图片

默认拦截器名 拦截器类 说明(括号里的表示默认值)
身份验证相关的
authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter 基于表单的拦截器;如 “/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username); passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe); loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储 key(shiroLoginFailure);
authcBasic org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter Basic HTTP 身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);
logout org.apache.shiro.web.filter.authc.LogoutFilter 退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/); 示例 “/logout=logout”
user org.apache.shiro.web.filter.authc.UserFilter 用户拦截器,用户已经身份验证 / 记住我登录的都可;示例 “/**=user”
anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例 “/static/**=anon”
授权相关的
roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter 角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例 “/admin/**=roles[admin]”
perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter 权限授权拦截器,验证用户是否拥有所有权限;属性和 roles 一样;示例 “/user/**=perms[“user:create”]”
port org.apache.shiro.web.filter.authz.PortFilter 端口拦截器,主要属性:port(80):可以通过的端口;示例 “/test= port[80]”,如果用户访问该页面是非 80,将自动将请求端口改为 80 并重定向到该 80 端口,其他路径 / 参数等都一样
rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter rest 风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例 “/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete” 权限字符串进行权限匹配(所有都得匹配,isPermittedAll);
ssl org.apache.shiro.web.filter.authz.SslFilter SSL 拦截器,只有请求协议是 https 才能通过;否则自动跳转会 https 端口(443);其他和 port 拦截器一样;
其他
noSessionCreation org.apache.shiro.web.filter.session.NoSessionCreationFilter 不创建会话拦截器,调用 subject.getSession(false) 不会有什么问题,但是如果 subject.getSession(true) 将抛出 DisabledSessionException 异常;

5. Shiro 登录认证操作

Controller 修改登录方法

     @PostMapping("/login")
    public ResponseEntity login(@RequestParam("username") String username,
                                @RequestParam("password") String password) {
            //根据注册的时候 或者修改manager里面的加密方式来, 只要一样即可
            SimpleHash md5 = new SimpleHash("MD5", password, username);
            String newPassword = md5.toHex();
            //从数据库判断密码是否正确
            RbacManager user = userService.getUser(username);
            if (user.getPassword().equals(newPassword)) {
             //1.用了jwt而且不用多域模式  就不要用这个来登录了
//            Subject subject = SecurityUtils.getSubject();
//            UsernamePasswordToken token = new UsernamePasswordToken(username, newPassword);
//            System.out.println("UserController.login:"+token.toString());
//            subject.login(token);
                return new ResponseEntity(200, "登录成功", JWTUtil.sign(username, newPassword));
            } else {
                throw new UnauthorizedException();
            }
    }

完整的登录controller代码

package com.woniuxy.Controller;

import com.woniuxy.entity.RbacManager;
import com.woniuxy.entity.ResponseEntity;
import com.woniuxy.exception.UnauthorizedException;
import com.woniuxy.service.impl.UserService;
import com.woniuxy.utils.JWTUtil;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

/**
 * @author: mayuhang  
* Date: 2021/4/22:23:52
* Description: */
@RestController public class UserController { @Autowired UserService userService; @PostMapping("/login") public ResponseEntity login(@RequestParam("username") String username, @RequestParam("password") String password) { //根据注册的时候 或者修改manager里面的加密方式来, 只要一样即可 SimpleHash md5 = new SimpleHash("MD5", password, username); String newPassword = md5.toHex(); RbacManager user = userService.getUser(username); if (user.getPassword().equals(newPassword)) { //1.用了jwt而且不用多域模式 就不要用这个来登录了 // Subject subject = SecurityUtils.getSubject(); // UsernamePasswordToken token = new UsernamePasswordToken(username, newPassword); // System.out.println("UserController.login:"+token.toString()); // subject.login(token); return new ResponseEntity(200, "登录成功", JWTUtil.sign(username, newPassword)); } else { throw new UnauthorizedException(); } } /** * Description : 不携带token直接请求的话 就是游客咯
* ChangeLog : 1. 创建 (2021/5/3 22:15 [mayuhang]); * * @return com.woniuxy.entity.ResponseEntity **/
@GetMapping("/article") public ResponseEntity article() { Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { return new ResponseEntity(200, "您已经登录", null); } else { return new ResponseEntity(200, "You are guest", null); } } /** * Description : 加注解 判断是否认证过的, 等同于之前的subject.isAuthenticated()
* ChangeLog : 1. 创建 (2021/5/3 22:51 [mayuhang]); * * @return com.woniuxy.entity.ResponseEntity **/
@GetMapping("/require_auth") @RequiresAuthentication public ResponseEntity requireAuth() { return new ResponseEntity(200, "恭喜您登录成功,可以获取菜单数据了!", null); } /** * Description : 是否有某个角色, 等同于subject.hasRole,有多个的话 参考下面权限判断
* ChangeLog : 1. 创建 (2021/5/3 22:53 [mayuhang]); * * @return com.woniuxy.entity.ResponseEntity **/
@GetMapping("/require_role") @RequiresRoles("1") public ResponseEntity requireRole() { return new ResponseEntity(200, "你有超级管理员角色", null); } /** * Description : 是否有某个权限, 等同于subject.isPermitted
* ChangeLog : 1. 创建 (2021/5/3 22:53 [mayuhang]); * * @return com.woniuxy.entity.ResponseEntity **/
@GetMapping("/require_permission") @RequiresPermissions(logical = Logical.OR, value = {"role:view", "perm_manage"}) public ResponseEntity requirePermission() { return new ResponseEntity(200, "您正在访问权限,需要编辑,perm_manage权限的接口", null); } /** * Description : 所有401等权限认证, 错误的, 都到这里来~
* ChangeLog : 1. 创建 (2021/5/3 22:54 [mayuhang]); * * @return com.woniuxy.entity.ResponseEntity **/
@RequestMapping(path = "/401") @ResponseStatus(HttpStatus.UNAUTHORIZED) public ResponseEntity unauthorized() { return new ResponseEntity(401, "未经授权", null); } }

6. 全局异常处理

在一个项目中的异常我们我们都会统一进行处理的,那么如何进行统一进行处理呢?

新建一个类 GlobalDefaultExceptionHandler,

在 class 注解上@ControllerAdvice,

@CONTROLLERADVICE:即把@CONTROLLERADVICE 注解内部使用@EXCEPTIONHANDLER、@INITBINDER、

@MODELATTRIBUTE 注解的方法应用到所有的 @REQUESTMAPPING 注解的方法。非常简单,不过只有当使用

@EXCEPTIONHANDLER 最有用,另外两个用处不大。

在方法上注解上@ExceptionHandler(value = Exception.class),具体代码如下

package com.woniuxy.exception;
/** 
* @author: mayuhang  
* Date: 2021/4/22:23:54
* Description:自定义异常处理 */
public class UnauthorizedException extends RuntimeException { public UnauthorizedException(String message) { super(message); } public UnauthorizedException() { super(); }}

处理框架异常参考下方(这个可以不需要, 了解即可):

restful需要统一返回异常格式, 所以就处理下springboot的异常了…

@RestControllerAdvice
public class ExceptionController {

// 捕捉shiro的异常
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(ShiroException.class)
public ResponseBean handle401(ShiroException e) {
	return new ResponseBean(401, e.getMessage(), null);
}

// 捕捉UnauthorizedException
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ExceptionHandler(UnauthorizedException.class)
public ResponseBean handle401() {
	return new ResponseBean(401, "Unauthorized", null);
}

// 捕捉其他所有异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
	return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
}

private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
	if (statusCode == null) {
		return HttpStatus.INTERNAL_SERVER_ERROR;
	}
		return HttpStatus.valueOf(statusCode);
	}
}

7. 自定义ResponseEntity

package com.woniuxy.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author: mayuhang  
* Date: 2021/4/22:23:51
* Description: 这个 mvc里面讲过了哦 */
@Data @AllArgsConstructor @NoArgsConstructor public class ResponseEntity { // http 状态码 private int code; // 返回信息 private String msg; // 返回的数据 private Object data; }

你可能感兴趣的:(Springboot,shiro,spring,boot,vue.js,后端)