本文章SSM项目地址:https://github.com/fenxianxian/shiro5/tree/master
很简单,我也不想废话,但我不得不说,不然这篇文章就不完美了。因为我相信,不用学,大家也知道权限管理是什么意思,毕竟你们也经常的浏览各种网站,肯定跑不过权限这一块,比如视频网站,看的视频是所有人都能看吗?不一定吧,你充VIP了吗?还有,在CSDN写文章,是所有人都能写的吗?不一定吧,你登陆了吗?众所周知,你不登陆,你就不能在我网站上访问各种资源,所以,我强迫你登陆,只有这样,我才能为你分配权限,让部分资源为你而敞开。
大白话就是控制某某人可以/不可以访问某某功能,这里可不可以就是你的权限了,所谓的权限就是根据身份的不同进行不同的操作,不同的操作就意味着权力的不同,这就是系统中的权限管理。
假设有一套教务管理系统,它的登陆页面大概如下:
主要看那三个单选框,在登陆之前我们都会根据自己的身份去选择对应的身份进行登录,这样做的目的是系统可以根据不同的身份跳转或生成对应的页面,页面的不同归根到底就是权限的不同,比如学生有查看自己成绩的权利,但是不能查看全班同学的成绩,而老师就不一样了,只要是他管理的班级,他班下的所有学生的成绩他都可以查看,甚至可以录入成绩,修改成绩等,如下:
大概就像上面那样,我只是意思意思,注意把以上内容看成一个菜单。可以看出,管理员是拥有所有权限的,而学生和教师是拥有一部分权限,这是根据他们的身份决定的,所以,应用到项目中,我们应该怎么去实现它?
1.基于页面的权限控制
在建立数据表时,不用说,肯定会有用户表,可以存放他的用户名啊,密码这些,但为了可以在登陆的过程中根据他的身份确定他的权限,并在页面显示出来,我们可以在用户表上加上一个字段,字段名就叫state,表示状态,比如,1代表学生,2代表教师,3代表管理员。就这样。好,只需要做这一步就行了,然后写三个首页,这三个首页对应着这三种身份,也就是说,在登陆成功后(前提是用户名,密码和状态码要正确),不是要跳转到首页吗?那跳到哪个首页呀,就看你的状态是多少了,你状态多少,我就跳到哪个首页,注意,一样是首页,但是身份不同,首页的内容也不同,所谓的内容就是我上图列举出来的内容,你有什么内容你就可以做什么事,就这么简单。到这最基本的权限控制也就体现出来了。
问题:
如上做法有什么问题吗?有,当然有,比如这时候我要对教师分配某种权限啥的,那么对于这套系统而言,如果没有提供一种可以为某位用户分配某种权限的话,是不是就无法完成了,但能完成吗,肯定可以,只不过作为管理员的我可能不懂代码,如果懂,那还好说,直接打开html,修改一下就行了,但我不懂啊,我只会操作界面,那怎么办,是不是要请工程师来完成,工程师针对这种情况,是不是可以在操作界面上给你增加一个按钮,以后要分配,点一下那个按钮就行了,即方便你,也方便我,多好。那么工程师会怎么完成呢?
2.基于权限的权限控制
这次不基于页面来进行权限控制了,每次分配都要修改页面代码,麻烦,有没有一种办法可以让其自动改变?嗯,我们可不可以这样,建一个权限表(资源表),该权限表有两个字段,一个是id,一个是权限名,姑且叫PermissionName,比如如下:
解读一下,像id为1002和1003的,还有1008和1009的,都有一个共同点,就是前面会有什么什么冒号,其实,冒号前面的是模板名来的,比如,像成绩管理这块,它下面有两个子模块,分别是成绩查询和录入成绩,成绩查询对应的就是select,录入成绩对应的就是add,说白了,就是让可读性好一点,没有什么特殊的。那么用户表这边呢,可以把state字段给去掉了,但为了在查询用户的时候顺便把他的权限也查出来,我们可以在用户和权限表之间再建一张表,叫用户权限表,也就是所谓的中间表(因为用户和权限是多对多的关系,所以需要一个中间表来表示),通过这张表,就可以让用户和权限联系起来,如下:
该表表示有一位用户id为1的人,它拥有的权限有六个,如上,是不是有六条记录,注意,以上六个权限id都是学生们所拥有的权限,可以拿着权限id去权限表查。好,那么到此为止,我们在登陆的时候,肯定是先校验用户名和密码的,这不用说,然后再根据用户id去用户权限表查找相应的权限,查出来的权限可以有多个,如上图,再把查出来的多个权限拿到权限表上进行查找,查找出来后,让页面根据查找出来的权限名进行动态的变动。
问题:
一样,这种做法有什么问题?问题就是,如果来了为新学生,那么该学生的权限肯定跟用户id为1所拥有的权限一样,因为用户id为1的用户也是一名学生,学生跟学生,权限肯定一样,好,假设新学生的用户id为2,那么在用户权限表上是不是会多出六条记录,相当于复制了一份,是不是极度冗余,如果学生过多的话,那么记录岂不是长到爆,这就是问题。
3.基于角色的权限控制
那以上问题如何解决?其实很简单,不是权限id这块冗余了吗,那么就把权限id这块冗余的地方提取出来,只不过这次不跟用户id对应了,而是跟角色对应,什么意思,让我们再再建一张表,叫角色表,如下:
有了角色表,为了可以根据角色来查权限,我们可以再再再一次建一张表,叫角色权限表,这个就不截图了,跟用户权限表一样一样的,只不过这次如果加入一位新学生进来,我们只为它分配一个角色就行了,所以在用户表上我们要加上一个字段,叫角色id,比如1110,那么,我们就可以根据角色id到角色权限表里查,把他的权限查出来,这样不就行了?
问题:
到此为止,有用户表,用户角色表,角色表,角色权限表,权限表这5张表,当然还少了用户权限表,但目前来看用户权限表被用户角色表给替代了,因为我说了,用户权限表有缺点,但是,问题来了,随着需求的变化,这次我不是为某一个角色分配权限,如果是的话,好办,我在角色权限表中增加一条记录,但这次,我是要为某一个特定的用户增加某种权限,不是针对角色的哦,那这次的用户权限表不就发挥他的作用了吗,所以,这点要注意。
收尾
以上,像基于角色的权限控制和基于权限的权限控制已经形成最基本的RBAC权限模型了。(ง ˙o˙)ว
LOGO:
这logo很形象,盾牌,意味着安全,那也就是说,Shiro是做安全这方面的框架,而事实也正是如此,只不过此时我们还没开始正式学习Shiro,所以体会不到,没关系,我们继续。
在前面我们曾提到过 “权限管理” 这个概念,而在意义上它是包含两部分的,也就是身份认证和授权。那什么是身份认证?身份认证就是判断你的身份是否是我的合法用户,非黑名单的那种。而授权则表示当你的身份认证通过后,你在我系统所拥有的操作权限,哪些可以为你放开,哪些不可以,不是说你登陆了之后,我就得把我系统里面的所有资源都给你享用,有些可是要充VIP的哦,像这些都得由我给你控制,授予。
1.身份认证
身份认证,就是判断一个用户是否为合法用户的处理过程。最简单的身份认证方式就是系统通过核对用户输入的用户名和密码,看其是否与系统中存储的该用户的用户名和密码一致,来判断用户身份是否正确。还有像指纹呀,刷脸啊,刷卡呀都属于身份认证的范畴。
对于身份认证,我们会抽取出如下几个对象,
如下:
当然了,在subject里除了有Principal和Credential,还有一个叫token,令牌的意思,那什么是令牌,公式:令牌=身份信息+凭证信息。但注意,令牌可不是简简单单的只是包含身份信息和凭证信息,还有其它。
2.授权
授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。授权可简单理解为who对what(which)进行how操作,如下:
什么是Shiro?
Shiro是一个基于java的开源的安全管理框架,可以完成认证,授权,会话管理,加密,缓存等功能。
架构图解释
首先,一看那架构图,涉及到的名词就很多,其中最引人注目的是中间那块,也就是叫Security Manager,翻译过来叫安全管理,它是Shiro架构的核心,所有与安全有关的操作都会与SecurityManager进行交互。好,我们看它里面的内容:
整体流程
新建一个普通的maven项目。新建完之后,在pom.xml中加入以下坐标:
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <!-- 防止报java.lang.NoClassDefFoundError: org/apache/commons/logging/LogFactory错误 --> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.2</version> </dependency>
然后在resources文件夹下创建shiro.ini文件,该文件是用来模拟数据库中的持久化数据的,也就是说,我们现在暂且不跟数据库打交道,而是通过一个文本文件shiro.ini来配置数据,到时候,如果我们跟数据库打交道了,就不用shiro.ini了。
让我们往shiro.ini加入点东西,如下:
[users] xiaochen=123456 chenxian=abcd
中括号中的user表示用户,加了s代表多个用户,也就是说,在它之下,我们可以配置多个用户,比如,用户名和密码,如果我们往下看的话,是不是猜都能猜出来,格式就是:用户名=密码,而事实也正是如此。我们还可以扩展一下格式:用户名=密码,角色1,角色2。
以上shiro.ini里的内容,就是数据库里的内容,还是那句话,模拟。好了,既然有了用户名和密码,我们是不是可以做一下认证,如下代码:
package com.cht.test; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.subject.Subject; //shiro认证 public class TestShiro { public static void main(String[] args) { //1. 要想认证,首先得先有安全管理器SecurityManager,我们可以看第4节的Shiro架构图,你就会发现, //我们那所谓的认证器,授权器都是SecurityManager里的一个部件,同时,在应用程序这端,不管 //是C,C++啥的,还有PHP,Python,甚至是Java都是需要经过SecurityManager的,有了SecurityManager。 //我们就可以开始做权限控制了,而我之前说过,权限控制包含身份认证,而身份认证这件事是不是交 //由SecurityManager里的Authenticator认证器来完成呀。 //1.1 先创建一个安全管理器工厂类(注:IniSecurityManagerFactory已废弃) IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //1.2 通过工厂获取安全管理器 SecurityManager securityManager = factory.getInstance(); //2. 有了securityManager就可以做认证了,但是做认证,你得有用户名和密码啥的,因为你不发用户名和密码给我, //我怎么对你进行校验,所以,我们得有一个subject对象,让它把用户名和密码携带过来 //2.1 将安全管理器告知SecurityUtils,或者说注入,那也就是说,在底层,将会用到securityManager SecurityUtils.setSecurityManager(securityManager); //2.2 获取Subject主体对象 //拿到Subject就好办了,因为通过Subject就可以完成我们Shiro几乎所有的功能 //(因为这几乎的所有功能都被SecurityManager给管理起来了,而Subject又是调 //SecurityManager的,所以功能都被间接的调用,当然不包括加密),往后,我们面 //对的都是Subject,通过Subject里的方法(底层调用的是SecurityManager,也就是 //说Subject充其量就是个传话的),就可以完成我们想要的功能 Subject subject = SecurityUtils.getSubject(); //2.3 获取token对象,把用户名和密码进行封装 AuthenticationToken authenticationToken = new UsernamePasswordToken("xiaochen","123456"); //2.4 携带token进行登录(跟携带用户名和密码本质一样),开始认证 try { subject.login(authenticationToken); System.out.println("认证成功"); }catch (AuthenticationException e){ //用户名错误,密码错误都能接收 System.out.println("认证失败"); e.printStackTrace(); //如果认证失败,走catch块 } } }
答案肯定是认证成功,如果我们把UsernamePasswordToken的第二个参数改成不是123456,那么就认证失败,或者我们把用户名改成其它的,只要不是Shiro.ini里的用户名就行。
注意,密码错误抛出的异常是IncorrectCredentialsException(Incorrect:无效,不正确; Credentials:凭证),而用户名错误的异常是UnknownAccountException。
补充subject对象里的方法
System.out.println(subject.getPrincipal()); //获取凭证,其实取的用户名 System.out.println(subject.isAuthenticated());//为true,表示已认证成功 System.out.println(subject.hasRole("student"));//判断是否为student角色 System.out.println(subject.isPermitted("insert"));//判断是否有添加的权限 subject.logout();//登出
扩展shiro.ini
[roles] #格式:角色名=权限名,权限名 #代表role1拥有user的添加,更新的权限。以下的值可以简写为"user:insert,update"(注:必须加引号) role1=user:insert,user:update #代表role2拥有添加,更新,删除权限 role2=insert,update,delete #值为user:*代表role3拥有user的所有权限 role3=user:* #值为*代表role4拥有所有权限 role4=*
[urls] #url地址=内置filter或自定义filter,角色名["权限1",...] # 访问时出现/login的url必须去认证,支持authc对应的对应的Filter /login = authc # 任意的url都不需要进行认证等功能 /** = anon # 所有的内容都必须保证用户已经登录 /** = user # 访问/abc时必须保证用户具有role1和role2的角色 /abc = roles["role1,role2"] # 访问/ab时必须保证有user:insert权限 /ab = perms["user:insert"]
注意,以上说的什么内置filter啥的翻译过来就是内置过滤器,指的是authc,anon,user这些,也就是说,这三个就代表了过滤器,只不过它采用的是别名,而真正的过滤器类如下:
public enum DefaultFilter { anon(AnonymousFilter.class), authc(FormAuthenticationFilter.class), authcBasic(BasicHttpAuthenticationFilter.class), logout(LogoutFilter.class), noSessionCreation(NoSessionCreationFilter.class), perms(PermissionsAuthorizationFilter.class), port(PortFilter.class), rest(HttpMethodPermissionFilter.class), roles(RolesAuthorizationFilter.class), ssl(SslFilter.class), user(UserFilter.class); }
这样你就会看到像authc所对应的过滤器类是谁了,你也可以点进去,主要看的是isAccessAllowed方法。
[main] #没有身份认证时的跳转地址 shiro.loginUrl = /user/login/page #角色和权限校验不通过时的跳转地址 shiro.unauthorizedUrl = /author/error #登出后的跳转地址,回首页 shiro.redirectUrl = /
多多少少了解下即可。
项目结构如下:
打开pom.xml,如下maven坐标:
<?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> <groupId>com.cht</groupId> <artifactId>shiro5</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.6.RELEASE</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.0</version> </dependency> </dependencies> </project>
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 接收所有请求,以通过请求路径,识别是否需要安全校验,如果需要则触发安全校验,做访问校验时,会遍历过滤器链。 (链中包含shiro.ini中urls内使用的过滤器) 会通过ThreadContext在当前线程中绑定一个subject和SecurityManager,供请求内使用,可以通过 SecurityUtils.getSubject()获取Subject 此处shiroFilter的用处: 1. 在进入DispatcherServlet之前多了一份拦截 2, 在项目启动时,会初始化好Shiro的环境,比如会把以下工作做好: IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); 这样我们就可以在程序中直接使用SecurityUtils.getSubject()了。比如UserController中的loginLogic方法 --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--在项目启动时,加载web-info或classpath下的shiro.ini,并构建WebSecurityManager。构建所有配置 中使用的过滤器链(anon,authc等),ShiroFilter会获取此过滤器链--> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/mvc.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
mvc.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd"> <context:component-scan base-package="com.cht"/> <mvc:annotation-driven/> <bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/" ></property> <property name="suffix" value=".jsp"></property> </bean> </beans>
User
package com.cht.pojo; public class User { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "User{" + "username='" + username + '\'' + ", password='" + password + '\'' + '}'; } }
UserController
package com.cht.controller; import com.cht.pojo.User; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; import java.util.Map; @Controller @RequestMapping("/user") public class UserController { @GetMapping("/login") public String login(){ System.out.println("去往登录页面"); return "login"; } @PostMapping("/login") public String loginLogic(User user,Map<String,Object> map){ ModelAndView modelAndView = new ModelAndView(); System.out.println("login logic");//登录 逻辑 //获取subject 调用login Subject subject = SecurityUtils.getSubject(); //创建用于登录的令牌 UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword()); // 登录失败会抛出异常,则交由异常解析器处理,登录成功,继续往下执行 try{ subject.login(token); //默认从Ini文件获取 }catch (IncorrectCredentialsException e){ map.put("message","密码错误"); return "error"; }catch (UnknownAccountException e){ map.put("message","用户名错误"); return "error"; } return "index"; } @RequestMapping("permission") public String permission(){ return "permission"; } @RequestMapping("all") public String all(){ return "all"; } }
shiro.ini
[users] xiaochen=123456 #chenxian具有role1角色 chenxian=abcd,role1 [roles] role1=user:query [urls] # 注意以下的书写顺序,比如不要把/** = user放在最前面, # 因为它管理的范围太大了,毕竟它是从上到下执行的,如果匹配了,就不会往下执行。 #去往登录的路上不用拦截,认证,所以用anon /user/login = anon /user/all = authc,perms["user:query"] # 所有的内容都必须保证用户已经登录, # 如果没登录,那么跳转地址看[main],在最下面 /** = user [main] #没有身份认证时的跳转地址 shiro.loginUrl = /user/login #角色和权限校验不通过时的跳转地址 shiro.unauthorizedUrl = /user/permission
all.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>查询所有</title> </head> <body> all,我进来了 </body> </html>
error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>错误</title> </head> <body> ${message} </body> </html>
index.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> index页面 </body> </html>
login.jsp
<%@ page contentType="text/html; charset=UTF-8" language="java" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>登录</title> </head> <body> <form action="/user/login" method="post"> 用户名: <input type="text" name="username"><br> 密码: <input type="text" name="password"><br> <input type="submit" value="登录"> </form> </body> </html>
permission.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>权限不足</title> </head> <body> 对不起,你的权限不足,不能进入all.jsp页面 </body> </html>
测试:
在项目启动后,无论是何种路径,比如像http://localhost:8080/sfdsfsdf这种随便写的路径都会自动跳转到登陆页面,也就是http://localhost:8080/user/login,因为还没有登陆,被限制了。自己去看shiro.ini中的[urls]和[main]即可。
用账号为xiaochen,密码是abcd测试,对照shiro.ini可知,有用户名为xiaochen的,但是密码却不是abcd,所以,当我们点击登陆按钮,将会弹出如下页面:
用户名错误就不演示了。
再用正确的用户名和密码登陆,如下:
测试http://localhost:8080/user/all,注意,当前是账号为xiaochen的环境,如下:
再用chenxian的账号去登陆,再来测试http://localhost:8080/user/all,如下:
shiro标签就是对页面元素的访问控制,比如有些菜单因为你的权限不足不足以查看,那么这些菜单就不应该出现,出现了也是摆设,而事实上确实有这种需求,就不应该出现。那么怎么做到,这就需要用到shiro的标签了。说白了,通过shiro标签,我们就可以在页面判断,哪些该显示,哪些不该显示,就像java中的逻辑判断一样。这样,同一个页面,不同的用户看到的内容就有所差别。
导入shiro标签库
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
知道加到jsp页面的哪个位置吧。一旦加上去的话,我们就可以在页面使用shiro标签了,如下:
身份认证
<shiro:authenticated >表示已登陆,如下,我们以上节跟web整合的案例演示,用login.jsp随便写上一句话,注意,只看效果,无特殊意思,如下:
做好后重启一下项目,项目启动后自动会进入login.jsp,到时候肯定是还未登陆状态,那么你就可以看我已经登录了
这句话是否会显示出来,然后再登陆,登陆完后再看一下那几句话有没有显示出来。如果未登陆状态没有显示,登陆后才显示那就是对的,标签起作用了。最后,与之相反的标签是<shiro:notAuthenticated >。
<shiro:user >,常用,包含已登录,且配合记住我,用户体验好。
<shiro:principal />,就是获取凭证的意思,显示的是你的用户名,一般嵌套在<shiro:authenticated >标签或<shiro:user >标签里,也就是说,你登陆了,那我就可以把你的用户名显示出来,或者你曾经点击过记住我,那我下次再打开的时候就不用登陆了,自动显示我的用户名。
<shiro:guest >,表示游客,也就是未登录或未记住我。
角色校验
权限校验
存在的问题:目前所有的用户,角色,权限数据都在ini文件中,不利于管理,实际项目开发中的这些信息,应该在数据库中,所以需要为这三类信息建表。
当前的默认Realm,是IniRealm,我们拆分解读一下,Ini指的就是ini文件,Realm我前面也提到过,它是用来获取数据的,相当于Dao,那也就是说,如果我们没有自定义Reaml,那么Shiro它需要数据就会调用IniReaml,让IniReaml去我们指定的ini文件读取数据,但是,在真实的项目开发中,我第一段也说了,数据是存在数据库里的,不是存在某一个文件里的,毕竟,数以万计的用户是不可能写在文件里的,所以,我们就不能用它默认的IniReaml了,而是用我们自定义的Reaml,让我们的这个自定义的Reaml去数据库里获取数据,所以,明白了吗?
既然这样,要查数据库,那么我们是不是得先在数据库里建表呀,我们就采用RBAC的思想来建吧,如下:
create table t_user( id int primary key auto_increment, username varchar(20) not null unique, password varchar(100) not NULL )engine=innodb default charset=utf8; create table t_role( id int primary key auto_increment, role_name varchar(50) not null unique, create_time timestamp not null )engine=innodb default charset=utf8; create table t_permission( id int primary key auto_increment, permission_name varchar(50) not null unique, create_time timestamp )engine=innodb default charset=utf8; create table t_user_role( id int primary key auto_increment, user_id int references t_user(id), role_id int references t_role(id), unique(user_id,role_id) )engine=innodb default charset=utf8; create table t_role_permission( id int primary key auto_increment, permission_id int references t_permission(id), role_id int references t_role(id), unique(permission_id,role_id) )engine=innodb default charset=utf8;
我们再插入几条数据,如下:
insert into t_user(username,password) values('小飞老师',123456),('小齐同学',436678); insert into t_role(role_name,create_time) values("teacher","2021/7/3"),("student","2021/7/4"); insert into t_permission(permission_name,create_time) values("student:manage","2020/10/9"),("student:study","2020/10/9");#分别是管理学生的权限和学生学习的权限 insert into t_user_role(user_id,role_id) values(1,1),(2,2); insert into t_role_permission(permission_id,role_id) values(1,1),(2,2);
下面我们接着第6节的项目继续下去,只不过这次我们采用ssm的方式来进行项目改造,并从原先默认的realm变为现在的自定义realm,但是却有个问题,因为我们学过ssm的都知道,配置文件是非常多的,而这么多的配置文件贴出来也没意义,我就把项目都上传到百度网盘了,地址为https://pan.baidu.com/s/1D4dQRZEWyoITdyy5CFPe4A,提取码为hh82。需要的自行下载研究,或者直接看我说的github地址也一样,只不过github里的项目是完整项目。而在这,我只贴重要的代码,如下:
//自定义Realm //Realm的职责就是为shiro加载用户,角色和权限数据,供内部校验使用,现在既然库中有数据了,就需要用自定义的Realm去加载。所以现在我们就自己建一个自定义的Realm类吧 //以下两个方法不是我们来调,而是securityManager需要数据时自动来调用 public class MyRealm extends AuthorizingRealm { //做权限,角色校验 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("进行权限,角色验证。。。"); //获取用户名 String username = (String) principalCollection.getPrimaryPrincipal(); //根据用户名查询权限和角色信息(这里注意一下,因为我此处是根据用户名去查询,所以我们得要求用户名是唯一的) RoleService roleService = ContextLoader.getCurrentWebApplicationContext().getBean("roleServiceImpl", RoleService.class); PermissionService permissionService = ContextLoader.getCurrentWebApplicationContext().getBean("permissionServiceImpl", PermissionService.class); Set<String> roles = roleService.queryAllRolenameByUsername(username); Set<String> permissions = permissionService.queryAllPermissionByUsername(username); //将查询出来的数据封装成AuthorizationInfo SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(roles); simpleAuthorizationInfo.addStringPermissions(permissions); return simpleAuthorizationInfo; } //做身份认证(查用户名,密码的) //何时触发:subject.login(token); @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("进行身份认证。。。"); //根据传进来的令牌token,获取用户名 String username = (String) authenticationToken.getPrincipal(); //查询用户信息 UserService userService = ContextLoader.getCurrentWebApplicationContext().getBean("userServiceImpl", UserService.class); //查询到用户信息 User user = userService.queryUserByUsername(username); //判断用户身份为空 if(user==null){ //如果为空 return null; //后续会抛出异常,也就是所谓的UnKnownAccountException } //将用户信息封装在AuthenticationInfo对象中 //第一个参数:数据库里的用户名 第二个参数:数据库里的密码 第三个参数:可以不太在意,可以理解为当前realm的标识,名字 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),this.getName()); return info; } }
以上就是自定义Realm类,该自定义Reaml类是不需要我们手动调用的,是SecurityManager需要数据自动调用的,那它是怎么知道有这么一个自定义类呢?这就需要shiro.ini文件了,shiro.ini文件有如下几句话,如下:
[main] ... #realm1是随便写的。下面为声明自定义的realm realm1 = com.cht.realm.MyRealm #安装Realm,关联到SecurityManager。注意,如果有多个realm,那么下面的值记得用逗号隔开,也就是$realm1,$realm2 securityManager.realms=$realm1
也就是说,shiro.ini这里已经配置好了有哪些自定义Realm,那么SecurityManager当然就知道咯。
以上自定义realm类说明一下,如果realm只做身份认证,则继承:AuthenticatingRealm。如果realm要负责身份认证和权限校验,则可以继承AuthorizingRealm。
方法体里所做的事,我就不多说了,反正就是调service,然后service调dao去数据库里查,然后比对,就这些,需要注意的是ContextLoader.getCurrentWebApplicationContext()
,写这句话的时候,别忘了去web.xml里加上这么一句话,如下:
<!--配置下面监听器的作用是让MyRealm类里的ContextLoader.getCurrentWebApplicationContext()发挥作用--> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param>
从subject.login(token);开始梳理:
用户的密码是不允许明文存储的,一旦泄露,用户的隐私信息就会暴露出来,所以,密码必须加密,生成密文,然后数据库中的密码存的也是密文。
在加密过程中需要使用到一些“不可逆加密”,如MD5,sha等。所谓不可逆是指:加密函数A,明文“abc”,A(“abc”)=密文,不能通过密文反推出“abc”,即使密文泄露密码仍然安全。
shiro支持hash(散列)加密,常见的如md5,sha等。
加密过程中建议使用salt,并制定迭代的次数,迭代次数的建议值1000+。
public static void main(String[] args) { String pwd = "1234"; String salt = UUID.randomUUID().toString();//盐 //将密码用md5加密,并加上盐,最后迭代1000次,最终加密出来的结果再转为Base64,就是最终的密文了 String s = new Md5Hash(pwd, salt, 1000).toBase64();//或者是toString() System.out.println(s); }
注册
有了上面的铺垫之后,注册就简单了,无非就是获取到用户的密码,然后加密,再把加密好的放到数据库里,这套流程就在service里添加用户时完成的,如下:
public Integer insertUser(User user) { String salt = UUID.randomUUID().toString(); String pwd = new Sha256Hash(user.getPassword(), salt, 1000).toBase64(); user.setPassword(pwd); return userMapper.insertUser(user); }
进行登陆时,肯定会用到密码,那么这时,我们获取用户传过来的密码,就要对它进行验证,怎么验证呢?首先,人家在注册时用的是md5加密还是sha加密你是不是得知道呀,也就是说,要统一,因为,相同的密码,经过相同的加密规则,最终得到的密文就是一样的,所以,在注册的时候既然用的是sha加密规则,那么在登陆的时候也得用sha的加密规则,并且要保证salt,和迭代次数一致,只有这样,我们在进行和数据库里的密文比对时,才会判断它传过来的明文密码是否正确。
但是却有一个问题,salt是随机的呀,那该如何解决?解决办法就是在注册时把用到的salt也存起来,那么这时我们就得修改一下我们的User实体类了,加上一个属性,就叫salt,然后数据库里的t_user表上也加上一个字段,也叫salt。所以以上代码要优化一下,如下:
public Integer insertUser(User user) { String salt = UUID.randomUUID().toString(); String pwd = new Sha256Hash(user.getPassword(), salt, 1000).toBase64(); user.setPassword(pwd); user.setSalt(salt); return userMapper.insertUser(user); }
登陆
在shiro.ini加上以下配置,如下:
#声明密码比对器 credentialsMatcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher credentialsMatcher.hashAlgorithmName = sha-256 credentialsMatcher.hashIterations = 1000 credentialsMatcher.hashSalted = true #true=hex格式,也就是toString() false=base64格式,也就是toBase64() credentialsMatcher.storedCredentialsHexEncoded = false #交由自定义reaml realm1.credentialsMatcher = $credentialsMatcher
然后再在自定义realm类下的doGetAuthenticationInfo方法下,把原先的SimpleAuthenticationInfo info
= new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),this.getName());改为SimpleAuthenticationInfo info
= new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()),this.getName());
Shiro一旦跟Spring集成,那么shiro.ini就可以去掉了,因为一切都交由Spring管理了。
maven坐标
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency>
编写spring-shiro.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd "> <!--shiro配置 Realm--> <bean id="myRealm" class="com.cht.realm.MyRealm"> <property name="userService" ref="userServiceImpl"/> <property name="roleService" ref="roleServiceImpl"/> <property name="permissionService" ref="permissionServiceImpl"/> <property name="credentialsMatcher" > <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="SHA-256"/> <property name="storedCredentialsHexEncoded" value="false"/> <property name="hashIterations" value="1000"/> </bean> </property> </bean> <!--DefaultWebSecurityManager是SecurityManager的默认实现--> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--把realm交由securityManager去接管--> <property name="realm" ref="myRealm"/> </bean> <!--shiroFilter 生产SpringShiroFilter (持有shiro的过滤相关规则,可进行请求的过滤校验,校验请求是否合法--> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <!--注入核心--> <property name="securityManager" ref="securityManager"/> <!--编写过滤器链,充分说明了ShiroFilter会来执行这些过滤器链, 执行过程中需要数据校验就会调用核心securityManager,核心又拥有realm,realm又拥有数据,自然可以校验--> <property name="filterChainDefinitions"> <value> /user/regist = anon /user/all = authc,perms["student:study"] /** = user </value> </property> <!--没有身份认证时的跳转地址--> <property name="loginUrl" value="/user/login"/> <!--角色和权限校验不通过时的跳转地址--> <property name="unauthorizedUrl" value="/user/permission"/> </bean> </beans>
注意spring-shiro.xml是通过applicationContext.xml来访问的,也就是如下:
<import resource="spring-shiro.xml"></import>
web.xml
在web.xml下增加如下过滤器,并把原先的ShiroFilter过滤器以及EnvironmentLoaderListener监听器注释掉。
<!--会从spring工厂中获取和它同名的Bean,(id="shiroFilter") 接到请求后调用bean的doFilter方法,进行访问控制--> <filter> <filter-name>shiroFilter</filter-name> <!--注意,这行的shiroFilter可不是随便乱写的,看上面的注释说明--> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <!--DelegatingFilterProxy不处理业务,传话的--> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
然后回到我们写的自定义Reaml,把出现过ContextLoader.getCurrentWebApplicationContext()的统统注释掉,我们不通过这种方式来获取service类,而是用我们以前学的注入service,也就是在该自定义Reaml加上以下三个属性:
private UserService userService; private RoleService roleService; private PermissionService permissionService;
注意要在类上加上@Setter注解,表示用set注入。
在登陆后,可以将用户名存在cookie中,下次访问时,可以先不登陆,就可以识别身份,在确定需要身份认证时,比如购买,支付或其它一些重要操作时,再要求用户登陆即可,用户体验好。由于可以保持用户信息,系统后台也可以更好的监控,记录用户行为,积累数据。
//如果需要记住我的话,需要在token中设置 token.setRememberMe(true);//shiro默认支持记住我,只要有此设置则自动运作,也就是说,当我们关闭浏览器,再次打开网页,是不用登陆的。但注意,记住我对过滤器链中的authc是无效的,该认证还是要认证,比如你访问/user/all。 subject.login(token);
以上记住我默认记265天,如果要想改为7天,如下:
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <!--rememberMe是cookie值中的key,value是用户名的密文 cookie["rememberMe":"deleteMe"] 此cookie每次登陆后都会写出,用于清除之前的cookie cookie["rememberMe":"username的密文"] 此cookie也会在登陆后写出,用于记录最新的username (如上设计,既能保证每次登陆后重新记录cookie,也能保证切换账号时,记录最新账号)--> <property name="name" value="rememberMe"/> <!--cookie只在http请求中可用,那么通过js脚本将无法读取到cookie信息,有效防止cookie被窃取--> <property name="httpOnly" value="true"/> <!--cookie生命周期,单位秒--> <property name="maxAge" value="2592000"/> <!--30天--> </bean> <!--记住我管理器--> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cookie" ref="rememberMeCookie"/> </bean>
最后注意securityManager这边,如下:
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--把realm交由securityManager去接管--> <property name="realm" ref="myRealm"/> <property name="rememberMeManager" ref="rememberMeManager"/> <!--默认记住我是1年,但此时已经关联了新的记住我,所以是7天--> </bean>
Shiro中的会话管理,其实跟java web里边的httpSession是一致的,都是表示客户端跟服务器的一次会话。一般我们在controller层使用httpSession,在非controller,比如service层我们可以用shiro给我们提供的session,照样可以获取httpSession里的数据。
Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如Web容器tomcat),不管Java SE或者Java EE都可以使用,提供了会话管理,会话事件监听,会话存储/持久化,容器无关的集群,失效/过期支持,对Web的透明支持,SSO单点登录的支持等特性。
相关API
在Java SE中使用
Subject subject = SecurityUtils.getSubject(); //获取session Session session = subject.getSession(); //session超时时间,单位,毫秒;0,马上过期;负数,不会过期,正数,对应毫秒后过期 session.setTimeout(10000); //session存取值 session.setAttribute("name","cht"); session.getAttribute("name"); //销毁session session.stop();
java EE环境
<!--增加session管理相关配置 会话cookie模板,默认可省--> <!--存sessionId的cookie--> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <!--cookie的key="sid"--> <property name="name" value="JSESSIONID"/> <!--只允许http请求访问cookie--> <property name="httpOnly" value="true"/> <!--cookie的过期时间,-1:存活一个会话,单位:秒,默认为-1--> <property name="maxAge" value="-1"/> </bean> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionIdCookie" ref="sessionIdCookie"/> <!--session全局超时时间,单位:毫秒,30分钟,默认值为1800000--> <property name="globalSessionTimeout" value="1800000"/> </bean>
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> ..... <property name="sessionManager" ref="sessionManager"/> </bean>
session监听
package com.cht.session; import org.apache.shiro.session.Session; import org.apache.shiro.session.SessionListenerAdapter; public class MySessionListener extends SessionListenerAdapter { //session创建时触发 @Override public void onStart(Session session) { System.out.println("session create"); } //session停止时触发 session.logout() session.stop() @Override public void onStop(Session session) { System.out.println("session stop"); } //session过期时触发,静默时间超过过期时间 //但注意它不会主动告知已过期,而是当你再一次访问的时候,它才会去校验是否过期,如果过期,就触发 @Override public void onExpiration(Session session) { System.out.println("session expire"); } }
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> .... <property name="sessionListeners"> <list> <bean class="com.cht.session.MySessionListener"></bean> </list> </property> </bean>
session检测
<bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> ... <!--session检测的时间间隔,单位毫秒,那15000毫秒就是15秒,也就是说,每15秒检测一次,看看session是否过期,如果过期了,将调用监听器里的onExpiration方法--> <property name="sessionValidationInterval" value="15000"/> </bean>
加在类上
@Controller @RequestMapping("/user") @RequiresAuthentication //类中的所有⽅法都需要身份认证 @RequiresRoles(value={"manager","admin"},logical= Logical.OR)//类中的所有⽅法都需要⻆⾊,"或" public class ShiroController { ... }
加在方法上
@Controller @RequestMapping("/user") public class ShiroController2{ ... @RequiresPermissions({"user:query","user:delete"}) //有对应权限,默认是 "且" @RequiresUser //记住我 或 已身份认证 public String hello(){ ... } @RequiresGuest //游客身份 public String hello2(){ ... } }