一、Shiro
以下引自百度百科
shiro(java安全框架)_百度百科
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。
(一)主要功能
三个核心组件:Subject,SecurityManager 和 Realms
1、Subject:
即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。
Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
2、SecurityManager:
它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
3、Realm:
Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。
(二)基本功能点
- Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
- **Session **Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
- Web Support:Web 支持,可以非常容易的集成到 Web 环境;
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
- Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
(三)特点
1、易于理解的 Java Security API
2、简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等)
3、对角色的简单的签权(访问控制),支持细粒度的签权
4、支持一级缓存,以提升应用程序的性能
5、内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境
6、异构客户端会话访问
7、非常简单的加密 API
8、不跟任何的框架或者容器捆绑,可以独立运行
二、Shiro 和 Spring Security区别
(一)Spring Security
Spring Security是一个灵活和强大的身份验证和访问控制框架,以确保基于Spring的Java Web应用程序的安全。
Spring Security是一个重量级的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成,并配备了流行的安全算法实现捆绑在一起。
Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。“认证”是为用户建立一个其声明的角色的过程,这个角色可以一个用户、一个设备或者一个系统。“验证”指的是一个用户在你的应用中能够执行某个操作。在到达授权判断之前,角色已经在身份认证过程中建立了。
特点
Shiro能实现的,Spring Security 基本都能实现,依赖于Spring体系,但是好处是Spring全家桶的一员,集成上更加契合,在使用上,比Shiro略功能强大(但是一般Shiro够用)
(二)Shiro 和 Spring Security区别
1、Shiro比Spring Security更容易使用,也就是实现上简单一些,同时基本的授权认证Shiro也基本够用
2、Spring Security社区支持度更高(但是安装Spring Security很难),Spring社区支持力度和更新维护上有优势,同时和Spring这一套的结合较好
3、Shiro 功能强大、且 简单、灵活。是Apache 下的项目比较可靠,且不跟任何的框架或者容器绑定,可以独立运行
个人理解
Shiro 首选 ,上手快 ,也足够用,自由度高,Spring Security中有的,Shiro也基本都有(项目没有使用Spring这一套,不用考虑,直接Shiro)
如果开发项目使用Spring这一套,用Spring Security可能更合适一些;虽然Spring Security 比较复杂,但与Spring 家族结合能力更强,是一个可以放心选择的框架结构
三、Spring Boot集成 Shiro
(一)创建Shiro环境
1、创建一个普通SpringBoot的Web工程
2、添加Shiro依赖
<!-- Shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
完整pom.xml文件
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.company</groupId>
<artifactId>shiro-boot</artifactId>
<version>1.0.0</version>
<name>shiro-boot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Shiro 依赖 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
(二)配置Shiro
1、定义配置类ShiroConfig
/**
* /当前类是一个Spring的配置类,用于模拟Spring的配置文件
*
* @Configuration: 表示当前类作为配置文件使用(配置容器,放在类的上面;等同 xml 配置文件,可以在其中声明 bean )
* @Bean: 将对象注入到Spring容器中(类似<bean>标签,放在方法上面);不指定对象的名称,默认是方法名是 id
*/
@Configuration
public class ShiroConfig {
/**
* 1、Subject:
* 即“当前操作用户”。它仅仅意味着“当前跟软件交互的东西”。
* Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
*
* 2、SecurityManager:
* 它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
*
* 3、Realm:
* Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。
*/
/**
* 配置Shiro的安全管理器
* @Bean: 将对象注入到Spring容器中(类似<bean>标签,放在方法上面);不指定对象的名称,默认是方法名是 id
*/
@Bean
public SecurityManager securityManager(Realm myRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//设置一个Realm,这个Realm是最终用于完成我们的认证号和授权操作的具体对象
securityManager.setRealm(myRealm);
return securityManager;
}
/**
* 配置一个自定义的Realm的bean,最终将使用这个bean返回的对象来完成我们的认证和授权
*/
@Bean
public MyRealm myRealm(){
MyRealm myRealm = new MyRealm();
return myRealm;
}
/**
* 配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
* 如什么样的请求可以访问,什么样的请求不可以访问等等
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
//创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
shiroFilter.setSecurityManager(securityManager);
//用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
//作用是用于通知Shiro我们可以使用这里路径转向到登录页面,但Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
//要求用户完成成功
shiroFilter.setLoginUrl("/");
//登录成功后转向页面,由于用户的登录后期需要交给Shiro完成,因此就需要通知Shiro登录成功之后返回到那个位置
shiroFilter.setSuccessUrl("/success");
//用于指定没有权限的页面,当用户访问某个功能是如果Shiro判断这个用户没有对应的操作权限,那么Shiro就会将请求
//转向到这个位置,用于提示用户没有操作权限
shiroFilter.setUnauthorizedUrl("/noPermission");
//定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问,什么样的请求不可以访问
Map<String,String> filterChainMap = new LinkedHashMap<String,String>();
// /login 表示某个请求的名字;anon 表示可以使用游客进行登录(这个请求不需要登录)
filterChainMap.put("/login","anon");
//我们可以在这里配置所有的权限规则,这列数据需要从数据库中读取出来
//或者在控制器中添加Shiro的注解
/**
/admin/** 表示一个请求名字的通配, 以admin开头的任意子路径下的所有请求
authc 表示这个请求需要进行认证(登录),只有认证(登录)通过才能访问
注:
** 表示任意子路径
* 表示任意的一个路径
? 表示 任意的一个字符
*/
filterChainMap.put("/admin/**","authc");
filterChainMap.put("/user/**","authc");
//表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
//如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
filterChainMap.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(filterChainMap);
return shiroFilter;
}
}
2、 自定义MyRealm类
/**
* 自定义Realm,用来实现用户的认证和授权
* Realm:父类抽象类
*/
public class MyRealm implements Realm {
@Override
public String getName() {
return null;
}
@Override
public boolean supports(AuthenticationToken authenticationToken) {
return false;
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
3、定义UserController类
@Controller
public class UserController {
@RequestMapping("/")
public String index(){
return "login";
}
@RequestMapping("/login")
public String login(String username, String password, Model model){
return "redirect:/success";
}
@RequestMapping("/success")
public String success(){
return "success";
}
@RequestMapping("/noPermission")
public String noPermission(){
return "noPermission";
}
@RequestMapping("/user/test")
public @ResponseBody
String userTest(){
return "这是userTest请求";
}
@RequestMapping("/admin/test")
public @ResponseBody String adminTest(){
return "这是adminTest请求";
}
@RequestMapping("/admin/add")
public @ResponseBody String adminAdd(){
Subject subject= SecurityUtils.getSubject();
return "这是adminAdd请求";
}
}
4、定义login.html 、nopermission.html、success.html
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="login" method="post">
账号<input type="text" name="username"><br>
密码<input type="text" name="password" id="password"><br>
<input type="submit" value="登录" id="loginBut">
</form>
<span style="color: red" th:text="${errorMessage}"></span>
</body>
</html>
success.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录成功</h1>
</body>
</html>
nopermission.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>对不起!您没有权限操作!</h1>
</body>
</html>
5、启动测试,浏览器输入 http://localhost:8080/
点击 登录没反应,即使输入 http://localhost:8080/success
访问也没反应
此时如果注释掉 ShiroConfig 类中的
//表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
//如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
//filterChainMap.put("/**","authc");
再次访问 localhost:8080 即可跳转到success页面
(三)配置Shiro认证账号
1、自定义MyAuthenticatingRealm类
/**
* 自定义Realm,用来实现用户的认证和授权
* AuthenticatingRealm 只负责认证(登录)的Realm父类
*/
public class MyAuthenticatingRealm extends AuthenticatingRealm {
/**
* Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
* @param authenticationToken 用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
* @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
* @throws AuthenticationException 如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
* 以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
* AuthenticationException 异常的子类 可以自己抛出
* AccountException 账号异常 可以自己抛出
* UnknownAccountException 账号不存在的异常 可以自己抛出
* LockedAccountException 账号异常锁定异常 可以自己抛出
* IncorrectCredentialsException 密码错误异常(这个异常会在Shiro进行密码验证时抛出)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取用户在浏览器中输入的账号
String userName = token.getUsername();
//认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbUserName = userName;
if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)){//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if("zhangsan".equals(userName)){
throw new LockedAccountException("账号被锁定");
}
//定义一个密码(这个密码应该来自数据库)
String dbpassword = "123456";
/**
* 创建密码认证对象,由Shiro自动认证密码
* 参数1 数据库中的账号(页面账号也可)
* 参数2 数据库中的密码
* 参数3 当前Relam的名字
* 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
*/
//认证密码是否正确
return new SimpleAuthenticationInfo(dbUserName,dbpassword,getName());
}
}
2、修改UserController类
@RequestMapping("/login")
public String login(String username, String password, Model model){
//创建一个shiro的Subject对象,利用这个对象来完成用户的登录认证
Subject subject = SecurityUtils.getSubject();
//判断当前用户是否已经认证过,如果已经认证过着不需要认证;如果没有认证过则完成认证
if(!subject.isAuthenticated()){
//创建一个用户账号和密码的Token对象,并设置用户输入的账号和密码
//这个对象将在Shiro中被获取
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
try {
//如账号不存在或密码错误等等,需要根据不同的异常类型来判断用户的登录状态,并给予友好的信息提示
//调用login后,Shiro就会自动执行自定义的Realm中的认证方法
subject.login(token);
} catch (UnknownAccountException e) {
//表示用户的账号错误,这个异常是在后台抛出
System.out.println("---------------账号不存在");
model.addAttribute("errorMessage","账号不存在");
return "login";
}catch (LockedAccountException e){
//表示用户的账号被锁定,这个异常是在后台抛出
System.out.println("===============账号被锁定");
model.addAttribute("errorMessage","账号被冻结");
return "login";
}catch (IncorrectCredentialsException e){
//表示用户的密码错误,这个异常是shiro在认证密码时抛出
System.out.println("***************密码不匹配");
model.addAttribute("errorMessage","密码错误");
return "login";
}
}
return "redirect:/success";
}
(四)认证缓存
当登录成功过一次以后,我们点击后退,此时输入任意账号和密码,无论输入什么信息,即使什么都不输入直接点击“登录”按钮,Shiro都会认为认证成功,这是因为Shiro在登录成功以后会将数据写入Shiro的缓存导致,因此应该在登录请求的控制器中在判断是否认证过之前添加一个登出操作,已清空缓存这样就可以重复测试登录
修改UserController
/创建一个shiro的Subject对象,利用这个对象来完成用户的登录认证
Subject subject= SecurityUtils.getSubject();
//登出方法调用,用于清空登录时的缓存信息,否则无法重复登录
subject.logout();
登出之前查看 subject.isAuthenticated() 状态为 true
登出之后查看 subject.isAuthenticated() 状态为 false
(五)密码加密
1、单纯后端加密
1、修改MyRealm类
/**
* 自定义Realm,用来实现用户的认证和授权
* AuthenticatingRealm 只负责认证(登录)的Realm父类
*/
public class MyAuthRealm extends AuthenticatingRealm {
/**
* AuthenticatingRealm
* Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
* @param authenticationToken 用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
* @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
* @throws AuthenticationException 如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
* 以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
* AuthenticationException 异常的子类 可以自己抛出
* AccountException 账号异常 可以自己抛出
* UnknownAccountException 账号不存在的异常 可以自己抛出
* LockedAccountException 账号异常锁定异常 可以自己抛出
* IncorrectCredentialsException 密码错误异常(这个异常会在Shiro进行密码验证时抛出)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取用户在浏览器中输入的账号
String userName = token.getUsername();
String password = new String(token.getPassword());//获取页面中的用户密码实际工作中基本不需要获取
System.out.println("账号:" + userName + " ----- 密码:" + password);
//认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbUserName = userName;
if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)){//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if("zhangsan".equals(userName)){
throw new LockedAccountException("账号被锁定");
}
//定义一个密码(这个密码应该来自数据库)
String dbpassword = "123456";
/**
* 数据密码加密主要是防止数据在浏览器访问后台服务器之间进行数据传递时被篡改或被截获,因此应该在前端到后台的过程中
* 进行加密,而这里的加密方式是将浏览器中获取后台的明码加密和对数据库中的数据进行加密
* 这就丢失了数据加密的意义 因此不建议在这里进行加密,应该在页面传递传递时进行加密
* 注:
* 建议浏览器传递数据时就加密数据,数据库中存在的数据也是加密数据,必须保证前端传递的数据
* 和数据主库中存放的数据加密次数以及盐规则都是完全相同的,否则认证失败
*/
//设置让当前登录用户中的密码数据进行加密
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1);
this.setCredentialsMatcher(credentialsMatcher);
/**
* 密码加密
* 参数 1 为加密算法 我们选择MD5加密
* 参数 2 为被加密的数据的数据
* 参数 3 为加密时的盐值 ,用于改变加密后数据结果
* 通常这个盐值需要选择一个表中唯一的数据,如表中的账号
* 参数 4 为需要对数据使用指定的算法加密多少次
*/
//对数据库中的密码进行加密
Object obj = new SimpleHash("MD5",dbpassword,"",1);
/**
* 创建密码认证对象,由Shiro自动认证密码
* 参数1 数据库中的账号(页面账号也可)
* 参数2 数据库中的密码
* 参数3 当前Relam的名字
* 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
*/
//认证密码是否正确
return new SimpleAuthenticationInfo(dbUserName,obj.toString()/*dbpassword*/,getName());
}
public static void main(String[] args) {
//使用Shiro提供的工具类对数据进行加密
//参数 1 为加密算法名 我们使用MD5这是一个不可逆的加密算法
//参数 2 为需要加密的数据
//参数 3 加密的盐值 用于改变加密结果的 不同的盐加密的数据是不一致的
//参数 4 为加密的次数
Object obj = new SimpleHash("MD5","123456","",1);
System.out.println("123456使用MD5加密1次---- "+obj);
Object obj2 = new SimpleHash("MD5","123456","",2);
System.out.println("123456使用MD5加密2次---- "+obj2);
Object obj3 = new SimpleHash("MD5","e10adc3949ba59abbe56e057f20f883e","",1);
System.out.println("123456使用MD5加密1次后在对这个数据加密1次---- "+obj3);
Object obj4 = new SimpleHash("MD5","123456","admin",1);
System.out.println("123456使用MD5 加盐admin 加密1次---- "+obj4);
Object obj5 = new SimpleHash("MD5","123456","admin1",1);
System.out.println("123456使用MD5 加盐admin1 加密1次---- "+obj5);
}
}
2、前端,后端加密
1、修改MyRealm类
/**
* 自定义Realm,用来实现用户的认证和授权
* AuthenticatingRealm 只负责认证(登录)的Realm父类
* AuthorizingRealm 负责认证(登录)和授权 的Realm父类
*/
public class MyAuthRealm extends AuthenticatingRealm/*AuthorizingRealm*/ {
/**
* AuthenticatingRealm
* Shiro的认证方法我们需要在这个方法中来获取用户的信息(从数据库中)
* @param authenticationToken 用户登录时的Token(令牌),这个对象中将存放着我们用户在浏览器中存放的账号和密码
* @return 返回一个AuthenticationInfo 对象,这个返回以后Shiro会调用这个对象中的一些方法来完成对密码的验证 密码是由Shiro进行验证是否合法
* @throws AuthenticationException 如果认证失败,Shiro就会抛出AuthenticationException 也可以手动抛出这个AuthenticationException
* 以及它的任意子异常类,不同的异常类型对应认证过程中的不同错误情况,我们需要根据异常类型来为用户返回特定的响应数据
* AuthenticationException 异常的子类 可以自己抛出
* AccountException 账号异常 可以自己抛出
* UnknownAccountException 账号不存在的异常 可以自己抛出
* LockedAccountException 账号异常锁定异常 可以自己抛出
* IncorrectCredentialsException 密码错误异常(这个异常会在Shiro进行密码验证时抛出)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//将AuthenticationToken强转成UsernamePasswordToken 这样获取账号和密码更加的方便
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取用户在浏览器中输入的账号
String userName = token.getUsername();
String password = new String(token.getPassword());//获取页面中的用户密码实际工作中基本不需要获取
System.out.println("账号:" + userName + " ----- 密码:" + password);
//认证账号,正常情况我们需要这里从数据库中获取账号的信息,以及其他关键数据,例如账号是否被冻结等等
String dbUserName = userName;
if(!"admin".equals(dbUserName) && !"zhangsan".equals(dbUserName)){//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if("zhangsan".equals(userName)){
throw new LockedAccountException("账号被锁定");
}
//定义一个密码(这个密码应该来自数据库)
//String dbpassword = "123456";
/**
* 数据密码加密主要是防止数据在浏览器访问后台服务器之间进行数据传递时被篡改或被截获,因此应该在前端到后台的过程中
* 进行加密,而这里的加密方式是将浏览器中获取后台的明码加密和对数据库中的数据进行加密
* 这就丢失了数据加密的意义 因此不建议在这里进行加密,应该在页面传递传递时进行加密
* 注:
* 建议浏览器传递数据时就加密数据,数据库中存在的数据也是加密数据,必须保证前端传递的数据
* 和数据主库中存放的数据加密次数以及盐规则都是完全相同的,否则认证失败
*/
//设置让当前登录用户中的密码数据进行加密
/*HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
credentialsMatcher.setHashAlgorithmName("MD5");
credentialsMatcher.setHashIterations(1);
this.setCredentialsMatcher(credentialsMatcher);*/
/**
* 密码加密
* 参数 1 为加密算法 我们选择MD5加密
* 参数 2 为被加密的数据的数据
* 参数 3 为加密时的盐值 ,用于改变加密后数据结果
* 通常这个盐值需要选择一个表中唯一的数据,如表中的账号
* 参数 4 为需要对数据使用指定的算法加密多少次
*/
//对数据库中的密码进行加密
//Object obj = new SimpleHash("MD5",dbpassword,"",1);
//数据库中 MD5 加密之后密码
String dbMD5Password = "e10adc3949ba59abbe56e057f20f883e";
/**
* 创建密码认证对象,由Shiro自动认证密码
* 参数1 数据库中的账号(页面账号也可)
* 参数2 数据库中的密码
* 参数3 当前Relam的名字
* 如果密码认证成功,会返回一个用户身份对象;如果密码验证失败则抛出异常
*/
//认证密码是否正确
return new SimpleAuthenticationInfo(dbUserName,dbMD5Password/*obj.toString()*//*dbpassword*/,getName());
}
public static void main(String[] args) {
//使用Shiro提供的工具类对数据进行加密
//参数 1 为加密算法名 我们使用MD5这是一个不可逆的加密算法
//参数 2 为需要加密的数据
//参数 3 加密的盐值 用于改变加密结果的 不同的盐加密的数据是不一致的
//参数 4 为加密的次数
Object obj = new SimpleHash("MD5","123456","",1);
System.out.println("123456使用MD5加密1次---- "+obj);
Object obj2 = new SimpleHash("MD5","123456","",2);
System.out.println("123456使用MD5加密2次---- "+obj2);
Object obj3 = new SimpleHash("MD5","e10adc3949ba59abbe56e057f20f883e","",1);
System.out.println("123456使用MD5加密1次后在对这个数据加密1次---- "+obj3);
Object obj4 = new SimpleHash("MD5","123456","admin",1);
System.out.println("123456使用MD5 加盐admin 加密1次---- "+obj4);
Object obj5 = new SimpleHash("MD5","123456","admin1",1);
System.out.println("123456使用MD5 加盐admin1 加密1次---- "+obj5);
}
}
2、修改login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script th:src="@{|/js/jquery-1.11.3.min.js|}"></script>
<script th:src="@{|/js/jQuery.md5.js|}"></script>
<script>
$(function(){
$("#loginBut").bind("click",function(){
var v_md5password=$.md5($("#password").val());
alert(v_md5password)
$("#md5Password").val(v_md5password)
})
})
</script>
</head>
<body>
<form action="login" method="post">
账号<input type="text" name="username" id="username"><br>
<!-- 密码<input type="text"name="password" id="password"><br>-->
密码<input type="text" id="password"><br>
<input type="hidden" name="password" id="md5Password">
<input type="submit" value="登录" id="loginBut">
</form>
<span style="color: red" th:text="${errorMessage}"></span>
</body>
</html>
注:
1、通常数据库中存放的密码数据不应该是明码123456,而是加密后的数据,
如e10adc3949ba59abbe56e057f20f883e,这是使用MD5加密后的123456,如果数据库中的密码已经是加密后的那么这里可以不选择进行加密
2、如果数据库中的密码已经加密,那么页面中传递数据前必须要对密码进行加密才能传递,否则无法可能会登录失败
3、如果选择加密传递,那么页面和数据库中的密码加密次数以及盐必须相同,否则登录一定失败
(六)权限分配
判断用户哪个权限是否可以使用,我们就必须要先为用户分配一个权限才可以分配需要在MyRealm类中配置并且修改继承的父类
1、修改MyRealm类继承的父类为 AuthorizingRealm类,并实现抽象方法doGetAuthorizationInfo
/**
* 自定义Realm,用来实现用户的认证和授权
* AuthenticatingRealm 只负责认证(登录)的Realm父类
* AuthorizingRealm 负责认证(登录)和授权 的Realm父类
*/
public class MyAuthRealm extends AuthorizingRealm {
/**
* AuthorizingRealm
* Shiro用户授权的回调方法
* @param principalCollection
* @return
*/
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//从Shiro中获取用户名
Object username = principalCollection.getPrimaryPrincipal();
//创建一个SimpleAuthorizationInfo类的对象,利用这个对象需要设置当前用户的权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//创建角色信息的集合
Set<String> roles = new HashSet<String>();
//根据账号到数据库中获取用户所对应的所有角色信息,并初始化到roles集合中
if("admin".equals(username)){
roles.add("admin");
roles.add("user");
}else if ("zhangsan".equals(username)){
roles.add("user");
}
Set<String>psermission = new HashSet<String>();
if("admin".equals(username)){
psermission.add("admin:add");
}
//设置角色信息
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(psermission);
return simpleAuthorizationInfo;
}
}
2、修改ShiroConfig
/**
* 配置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
* 如什么样的请求可以访问,什么样的请求不可以访问等等
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
//创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
shiroFilter.setSecurityManager(securityManager);
//用于设置一个登录的请求地址,这个地址可以是一个html或jsp的访问路径,也可以是一个控制器的路径
//作用是用于通知Shiro我们可以使用这里路径转向到登录页面,但Shiro判断到我们当前的用户没有登录时就会自动转换到这个路径
//要求用户完成成功
shiroFilter.setLoginUrl("/");
//登录成功后转向页面,由于用户的登录后期需要交给Shiro完成,因此就需要通知Shiro登录成功之后返回到那个位置
shiroFilter.setSuccessUrl("/success");
//用于指定没有权限的页面,当用户访问某个功能是如果Shiro判断这个用户没有对应的操作权限,那么Shiro就会将请求
//转向到这个位置,用于提示用户没有操作权限
shiroFilter.setUnauthorizedUrl("/noPermission");
//定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问,什么样的请求不可以访问
Map<String,String> filterChainMap = new LinkedHashMap<String,String>();
// /login 表示某个请求的名字;anon 表示可以使用游客进行登录(这个请求不需要登录)
filterChainMap.put("/login","anon");
//我们可以在这里配置所有的权限规则,这列数据需要从数据库中读取出来
//或者在控制器中添加Shiro的注解
/**
1、 /admin/** 表示一个请求名字的通配, 以admin开头的任意子路径下的所有请求
authc 表示这个请求需要进行认证(登录),只有认证(登录)通过才能访问
注:
** 表示任意子路径
* 表示任意的一个路径
? 表示 任意的一个字符
2、roles[admin] 表示 以/admin/**开头的请求需要拥有admin角色才可以访问否 则返回没有权限的页面
perms[admin:add] 表示 /admin/test的请求需要拥有 admin:add权限才可访问
注:admin:add仅仅是一个普通的字符串用于标记某个权限功能
*/
filterChainMap.put("/admin/test","authc,perms[admin:add]");
filterChainMap.put("/admin/**","authc,roles[admin]");
filterChainMap.put("/user/**","authc,roles[user]");
//表示所有的请求路径全部都需要被拦截登录,这个必须必须写在Map集合的最后面,这个选项是可选的
//如果没有指定/** 那么如果某个请求不符合上面的拦截规则Shiro将方行这个请求
//filterChainMap.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(filterChainMap);
return shiroFilter;
}
}
用户拥有角色和权限以后需要配置Shiro的权限规则
(七)基于注解的权限控制
1、修改ShiroConfig,添加
/**
* 开启Shiro注解支持(例如@RequiresRoles()和@RequiresPermissions())
* shiro的注解需要借助Spring的AOP来实现
* @return
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启AOP的支持
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
注:
启动注解的权限控制以后需要删除在Shiro配置类中的权限拦截的配置规则
filterChainMap.put("/admin/test","authc,perms[admin:add]");
filterChainMap.put("/admin/**","authc,roles[admin]");
filterChainMap.put("/user/**","authc,roles[user]");
2、修改UserController
@RequiresRoles(value = {"user"})
@RequestMapping("/user/test")
public @ResponseBody String userTest(){
return "这个userTest请求";
}
//RequiresRoles Shiro的注解 表示访问这功能必须要拥有 admin角色
//注:如果需要支持多个角色,就直接填写多个角色名称即可;如 "admin","user"
//RequiresRoles 属性 logical 用于在拥有多个角色时使用 取值为Logical.AND 表示并且的意思必须同时拥有多个角色 或
// Logical.OR 或者的意思,只要拥有多个角色中的其中一个即可
//注:使用了注解以后需要配置Spring声明式异常捕获,否则将在浏览器中直接看到Shiro的错误信息而不是友好的信息提示
@RequiresRoles(value = {"admin"})
@RequestMapping("/admin/test")
public @ResponseBody String adminTest(){
return "这个adminTest请求";
}
//@RequiresPermissions 注解用于指定当前请求必须要拥有指定的权限名字为 admin:add才能访问
//admin:add 只是一个普通的权限名称字符串,表示admin下的add功能
@RequiresPermissions(value = {"admin:add"})
@RequestMapping("/admin/add")
public @ResponseBody String adminAdd(){
Subject subject = SecurityUtils.getSubject();
//验证当前用户是否拥有这个权限
//subject.checkPermission();
//验证当前用户是否拥有这个角色
//subject.checkRole();
return "这个adminAdd请求";
}
//配置一个Spring的异常监控,当工程抛出了value所指定的所以异常类型以后将直接进入到当前方法中
@ExceptionHandler(value = {Exception.class})
public String myError(Throwable throwable){
//获取异常的类型,应该根据不同的异常类型进入到不通的页面显示不同提示信息
System.out.println(throwable.getClass());
System.out.println("---------------------------------");
return "noPermission";
}
注:
Shiro验证失败以后会抛出异常,因此这时必须要配置一个Spring的异常监控方法myError否则当前Shiro权限认证失败以后将无法转向到错误页面
(八)Shiro标签
使用Thymeleaf整合Shiro标签
1、添加 maven依赖
<!-- 使用Thymeleaf整合Shiro标签 -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
2、ShiroConfig配置Bean,Shiro标签与Thymeleaf的集成
/**
* 配置Shiro标签与Thymeleaf的集成
* @return
*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
3、success引入命名空间
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"
Shiro标签语法
作为属性控制
<****button ********type********="button" ********shiro********:authenticated********="true"****>
权限控制
</****button****>
作为标签
<****shiro********:hasRole ********name********="admin"****>
<****button ********type********="button"****>
权限控制
</****button****>
</****shiro********:hasRole****>
常用标签说明
guest标签
<****shiro********:guest****>
</****shiro********:guest****>
用户没有身份验证时显示相应信息,即游客访问信息。
user标签
<****shiro********:user****>
</****shiro********:user****>
用户已经身份验证/记住我登录后显示相应的信息。
authenticated标签
<****shiro********:authenticated****>
</****shiro********:authenticated****>
用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的。
notAuthenticated标签
<****shiro********:notAuthenticated****>
</****shiro********:notAuthenticated****>
用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证。
principal标签
<****shiro********:principal ********property********="username"****>
<****shiro********:principal********/****>
相当于((User)Subject.getPrincipals()).getUsername()。
lacksPermission标签
<****shiro********:lacksPermission ********name********="org:create"****>
</****shiro********:lacksPermission****>
如果当前Subject没有权限将显示body体内容。
hasRole标签
<****shiro********:hasRole ********name********="admin"****>
</****shiro********:hasRole****>
如果当前Subject有角色将显示body体内容。
hasAnyRoles标签
<****shiro********:hasAnyRoles ********name********="admin,user"****>
</****shiro********:hasAnyRoles****>
如果当前Subject有任意一个角色(或的关系)将显示body体内容。
lacksRole标签
<****shiro********:lacksRole ********name********="abc"****>
</****shiro********:lacksRole****>
如果当前Subject没有角色将显示body体内容。
hasPermission标签
<****shiro********:hasPermission ********name********="user:create"****>
</****shiro********:hasPermission****>
如果当前Subject有权限将显示body体内容
<****shiro********:hasAnyPermissions ********name********="admin:add,admin:update"****>
</****shiro********:hasAnyPermissions****>
如果当前Subject有任意一个权限(或的关系)将显示body体内容。
<****shiro********:hasAllRoles ********name********=""****></****shiro********:hasAllRoles****>
必须拥有指定的全选全部角色
<****shiro********:hasAllPermissions ********name********=""****></****shiro********:hasAllRoles****>
必须拥有指定的全选全部权限
版权归原作者 MinggeQingchun 所有, 如有侵权,请联系我们删除。