shiro+jwt实现前后端分离
一、RBAC概念
基于角色的权限访问控制(Role-Based Access Control)作为传统访问控制(自主访问,强制访问)的有前景的代替受到广泛的关注。在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。
这就极大地简化了权限的管理。在一个组织中,角色是为了完成各种工作而创造,用户则依据它的责任和资格来被指派相应的角色,用户可以很容易地从一个角色被指派到另一个角色。
角色可依新的需求和系统的合并而赋予新的权限,而权限也可根据需要而从某角色中回收。角色与角色的关系可以建立起来以囊括更广泛的客观情况。
第一类权限信息: 只有用户和权限表 1对多
如果用户群体大, 是不是每一个用户都需要配置一遍?? 如果要去掉某一批基层员工的某个权限, 就需要去修改每个人的权限信息!
第二类加中间表: 和第一类 仅仅只是sql语句存储的数据不同
第三类: 有角色表!
画图讲解^ ^用户 角色 权限 如果没有角色会怎样??
在RBAC模型中,角色是系统根据管理中相对稳定的职权和责任来划分,每种角色可以完成一定的职能。用户通过饰演不同的角色获得角色所拥有的权限,一旦某个用户成为某角色的成员,则此用户可以完成该角色所具有的职能。通过将权限指定给角色而不是用户,在权限分派上提供了极大的灵活性和极细的权限指定粒度。
第四类[了解]: 加菜单
第五类[了解]: 租户 SAAS系统! 通用服务! 内部系统—定制化开发!
第六类:中台
第七类:未来趋势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等应用,但是不太适合拥有自有认证权限管理的企业应用。
4. Token Auth
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
客户端使用用户名跟密码请求登录
服务端收到请求,去验证用户名与密码 + 盐
验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里
客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
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.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>
(2)创建类CreateJwtTest,用于生成token
import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;/*** @author: mayuhang <br/>* Date: /4/29:1:45 <br/>* 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( pact() );}}
(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 <br/>* Date: /4/29:1:48 <br/>* 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 <br/>* Date: /4/29:1:45 <br/>* 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( pact() );}}
(2) 解析
import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import java.io.UnsupportedEncodingException;import java.text.SimpleDateFormat;import java.util.Date;/*** @author: mayuhang <br/>* Date: /4/29:1:48 <br/>* 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大功能
Authentication 身份认证/登录,验证用户是不是拥有相应的身份;
Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support Web支持,可以非常容易的集成到Web环境;
Caching 缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing : 提供测试支持;
Run As +:可以模仿一个账户。
Remember Me 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
3. Shiro的架构图
接下来从Shiro内部来看下Shiro的架构,如下图所示:
**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完成工作。如下图:
Subject主体:需要登录系统的东西,都是主体。 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager,可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
SecurityManager安全管理器:即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与其他组件进行交互,相当于DispatcherServlet前端控制器;
Realm域:一个用来做身份认证,以及授权的对象 Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,就是一个跟权限数据有关的数据源。
记住一点:Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
5. shiro入门程序
1.搭建基于ini文件的运行环境
pom中导入shiro坐标:(plugins插件仅仅只是为了处理maven和idea兼容导致的启动问题)
<?xml version="1.0" encoding="UTF-8"?><project xmlns="/POM/4.0.0"xmlns:xsi="/2001/XMLSchema-instance"xsi:schemaLocation="/POM/4.0.0 /xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.woniuxy</groupId><artifactId>shiro01</artifactId><version>1.0-SNAPSHOT</version><dependencies><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>1.7.1</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope></dependency></dependencies><build><plugins><plugin> <groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>2.7</version><dependencies><dependency><groupId>org.apache.maven.shared</groupId><artifactId>maven-filtering</artifactId><version>1.3</version></dependency></dependencies></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.22.2</version><configuration><skipTests>true</skipTests></configuration></plugin></plugins></build></project>
2.用户认证 BasicIniEnvironment
认证:身份认证/登录,验证用户是不是拥有相应的身份。基于shiro的认证,是通过subject的login方法完成用户认证工作的
(1)在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-auth.ini)
[users]#模拟从数据库查询的用户#数据格式 用户名=密码mayun=123456mayuhang=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 <br/>* Date: /4/29:0:32 <br/>* Description:*/public class TestLogin {@Testpublic void testLogin() throws Exception{//1.加载ini配置文件创建SecurityManagerEnvironment environment = new BasicIniEnvironment("classpath:shiro-auth.ini");//2.获取securityManagerSecurityManager 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());}}
打印结果:
3.用户授权
授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
(1)在resource目录下创建shiro的ini配置文件构造模拟数据(shiro-prem.ini)
[users]#模拟从数据库查询的用户#数据格式 用户名=密码,角色1,角色2..mayun=123456,role1,role2mayuhang=654321,role3[roles]#模拟从数据库查询的角色和权限列表#数据格式 角色名=权限1,权限2role1=user:save,user:updaterole2=user:update,user.deleterole3=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 <br/>* Date: /4/29:22:44 <br/>* Description:*/public class TestPerm {@Testpublic void testPerm() throws Exception {//1.加载ini配置文件创建SecurityManagerEnvironment environment = new BasicIniEnvironment("classpath:shiro-auth.ini");//2.获取securityManagerSecurityManager 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 <br/>* Date: /4/29:23:14 <br/>* Description: 自定义域* 继承AuthorizingRealm* 重写 AuthorizationInfo Z 授权* AuthenticationInfo C 认证*/public class MyRealm extends AuthorizingRealm {@Overridepublic void setName(String name){super.setName("myRealm");}/*** 授权:授权的主要目的就是查询 数据库获取当前用户的所有角色和权限信息** 在前后端不分离的情况下,* 在html页面使用<shiro>|后端使用shiro的@checkRole,@checkPermission相关注解才能触发* 其实前后端分离项目, 这个授权意义就不大了, 不通过controller加注解来控制权限*/@Overrideprotected 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 = newSimpleAuthorizationInfo();// 5.通过当前查询的权限数据保存到simpleAuthorizationInfosimpleAuthorizationInfo.addStringPermissions(permissions);// 6.通过当前用户,查询的角色数据保存到simpleAuthorizationInfosimpleAuthorizationInfo.addRoles(roles);return simpleAuthorizationInfo;}/*** 身份认证方法:认证的主要目的,比较用户输入的用户名密码是否和数据库中的一致** 需要在用户登录系统时触发* 该方法将是我们主要的方法, 流程是登录后, 携带token 与权限信息 发送到 vue中 存储浏览器*/@Overrideprotected 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]#声明realmpermReam=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 <br/>* Date: /4/29:23:43 <br/>* Description:*/public class TestRealm {@Testpublic void testPerm() throws Exception {//1.加载ini配置文件创建SecurityManagerEnvironment environment = new BasicIniEnvironment("classpath:shiro-realm.ini");//2.获取securityManagerSecurityManager 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 <br/>* Date: /4/30:15:21 <br/>* Description: 自定义域 这个是给 单一用户 进行 认证和授权的!*认证的数据放什么实体中: SimpleAuthenticationInfo*授权的数据(角色和权限)放什么实体中: SimpleAuthorizationInfo*这个两个对象去了哪里呢??*SecurityManager 它管理了我们单一用户的认证授权信息 以及 所有的 subject*/public class MyRealm extends AuthorizingRealm{@Overridepublic String getName() {return "MyRealm";}//授权 授予登录的这个角色和权限!@Overrideprotected 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;}//认证 确定你的用户名 密码正确@Overrideprotected 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 <br/>* Date: /4/23:0:01 <br/>* Description: 存放token返回token 模仿UsernamePasswordToken 将替换原本的登录方式* SimpleAuthenticationInfo对象中的Principal 和 Credentials 是根据你自定义域中的认证方法来的,你怎么放*/@AllArgsConstructor@Data@NoArgsConstructorpublic class MyJsonWebToken implements AuthenticationToken {String token;@Overridepublic Object getPrincipal() {return token;}@Overridepublic 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 <br/>* Date: /4/22:23:41 <br/>* Description: 工具类 生成JWT 和*/public class JWTUtil {//过期时间5分钟private static final long EXPIRE_TIME = 5*60*1000;/*** Description : 校验token是否正确,错了就直接异常了 所以就直接返回true即可 <br/>* ChangeLog : 1. 创建 (/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解密也能获得 <br/>* ChangeLog : 1. 创建 (/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后过期 <br/>* ChangeLog : 1. 创建 (/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.ponent;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 <br/>* Date: /4/23:0:24 <br/>* Description:继承官方的BasicHttpAuthenticationFilter,并且重写鉴权的方法* 代码的执行流程preHandle->isAccessAllowed->isLoginAttempt->executeLogin*/@Componentpublic class JWTFilter extends BasicHttpAuthenticationFilter {/*** 对跨域提供支持*/@Overrideprotected 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 注解即可*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {if (isLoginAttempt(request, response)) {try {//如果有tokenexecuteLogin(request, response);} catch (Exception e) {//如果请求头没有带Authorization属性的token 就到这里咯response401(request, response);//如果抛出异常,则不进入controller,否则及时异常,还是会进入controller执行代码return false;}}return true;}/*** 判断用户是否想要登入。* 检测header里面是否包含Authorization字段即可*/@Overrideprotected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {HttpServletRequest hsr = (HttpServletRequest) request;String authorization = hsr.getHeader("Authorization");return authorization!=null;}@Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest httpServletRequest = (HttpServletRequest) request;//从请求头中获取tokenString authorization = httpServletRequest.getHeader("Authorization");//放入对象中, redis或者数据库查询 等同于之前的UsernamePasswordTokenMyJsonWebToken token = new MyJsonWebToken(authorization);// 提交给realm进行登入,如果错误他会抛出异常并被捕获System.out.println("JWTFilter.executeLogintoken交给realm判断:"+token);//这个等同于直接的 subject.login(token)getSubject(request, response).login(token);// 如果没有抛出异常则代表登入成功,返回truereturn 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 <br/>* Date: /4/23:0:02 <br/>* Description:自定义域 自己写登录验证 和授权*/@Configurationpublic class MyRealm extends AuthorizingRealm {@AutowiredUserService userService;@AutowiredRbacPermService rbacPermService;@Overridepublic void setName(String name){super.setName("myRealm");}// 这个需要注意下, 多域模式中, 每个域里面 请自己配置自己的token来源// 这里的token是来自 自定义的JWTToken// 必须重写此方法,不然 传的jwt, shiro却使用的是UsernamePasswordToken进行解析,// 会报一个错, 你token格式不对(JWT的值是3部分, UsernamePasswordToken只有1节....)@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof MyJsonWebToken;}/*** 授权信息* 在前后端不分离的情况下, 在html页面使用<shiro>|后端使用shiro的@checkRole,@checkPermission相关注解才能触发* 前后端分离的模式, 就直接用注解验证权限即可, 根据需求来*/@Overrideprotected 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*/@Overrideprotected 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 <br/>* Date: /4/23:0:35 <br/>* Description: shiro核心配置, 中央大脑 SecurityManager 相关配置 等同于之前玩 shiro-realm.ini的时候*/@Configurationpublic class ShiroConfig {/*** 禁用session, 不保存用户登录状态。保证每次请求都重新认证。* 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session*/@Beanprotected SessionStorageEvaluator sessionStorageEvaluator() {DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();sessionStorageEvaluator.setSessionStorageEnabled(false);return sessionStorageEvaluator;}//配置安全管理器, 传入自己写的自定义域@Beanpublic DefaultWebSecurityManager securityManager(MyRealm realm) {//使用默认的安全管理器DefaultWebSecurityManager securityManager = newDefaultWebSecurityManager(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<String,String> filterMap = new LinkedHashMap<String,String>();// 配置不会被拦截的链接 顺序判断// 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规则* /web.html#urls-*/Map<String, String> filterRuleMap = new HashMap<>();// 所有请求通过我们自己的JWT FilterfilterRuleMap.put("/**", "jwt");// 访问401和404页面不通过我们的FilterfilterRuleMap.put("/401", "anon");filterFactory.setFilterChainDefinitionMap(filterRuleMap);return filterFactory;}//开启shiro注解支持@Beanpublic AuthorizationAttributeSourceAdvisorauthorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = newAuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}/**开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能*/@Beanpublic DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();advisorAutoProxyCreator.setProxyTargetClass(true);return advisorAutoProxyCreator;}/*** Description : 管理Subject主体对象,生命周期的组件,用户只是打印下生产销毁日志什么的,请参考spring中bean的生命周期 <br/>* ChangeLog : 1. 创建 (/4/27 0:25 [mayuhang]);** @return org.apache.shiro.spring.LifecycleBeanPostProcessor**/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}}
4.2 Shiro中过滤器的类型,以及ShiroConfig配置
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 <br/>* Date: /4/22:23:52 <br/>* Description:*/@RestControllerpublic class UserController {@AutowiredUserService 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直接请求的话 就是游客咯 <br/>* ChangeLog : 1. 创建 (/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() <br/>* ChangeLog : 1. 创建 (/5/3 22:51 [mayuhang]);** @return com.woniuxy.entity.ResponseEntity**/@GetMapping("/require_auth")@RequiresAuthenticationpublic ResponseEntity requireAuth() {return new ResponseEntity(200, "恭喜您登录成功,可以获取菜单数据了!", null);}/*** Description : 是否有某个角色, 等同于subject.hasRole,有多个的话 参考下面权限判断 <br/>* ChangeLog : 1. 创建 (/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 <br/>* ChangeLog : 1. 创建 (/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等权限认证, 错误的, 都到这里来~ <br/>* ChangeLog : 1. 创建 (/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 <br/> * Date: /4/22:23:54 <br/> * Description:自定义异常处理 */public class UnauthorizedException extends RuntimeException {public UnauthorizedException(String message) {super(message); }publicUnauthorizedException() {super(); }}
处理框架异常参考下方(这个可以不需要, 了解即可):
restful需要统一返回异常格式, 所以就处理下springboot的异常了…
@RestControllerAdvicepublic 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 <br/>* Date: /4/22:23:51 <br/>* Description: 这个 mvc里面讲过了哦*/@Data@AllArgsConstructor@NoArgsConstructorpublic class ResponseEntity {// http 状态码private int code;// 返回信息private String msg;// 返回的数据private Object data;}