1.shiro环境搭建
创建一个普通 SpringBoot 的 Web 工程 01-Shiro 添加依赖包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
2.配置shiro
2.1创建配置类
//标记当前类是一个 Spring 的配置类, 用于模拟 Spring 的配置文件
//在这里我们将要配置 Shiro
@Configuration
public class ShiroConfig {
//配置 Shiro 的安全管理器
@Bean
public SecurityManager securityManager(Realm myRealm){
DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();
//设置一个 Realm, 这个 Realm 是最终用于完成我们的认证号和授权操作的具体对象
securityManager.setRealm(myRealm);
return securityManager;
}
/*
配置一个自定义的 Realm 的 bean, 最终将使用这个 bean 返回的对象来完成我们的认证和授权,realm中写的就是登陆验证的规则,这里暂时没有编写
*/
@Bean
public Realm myRealm(){
MyRealm realm=new MyRealm();
return realm;
}
//配置一个 Shiro 的过滤器 bean, 这个 bean 将配置 Shiro 相关的一个规则的拦截
//例如什么样的请求可以访问什么样的请求不可以访问等等
@Bean
public ShiroFilterFactoryBean shiroFilter(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> map = new LinkedHashMap<String, String>();
// /login 表示某个请求的名字 anon 表示可以使用游客什么进行登录(这个请求不需要登录)
map.put("/login", "anon");
//roles[admin] 表示 以/admin/**开头的请求需要拥有 admin 角色才可以访问否则返回没有权限的页面
//perms[admin:add] 表示 /admin/test 的请求需要拥有 admin:add 权限才可访问
//注意: admin:add 仅仅是一个普通的字符串用于标记某个权限功能
map.put("/admin/test", "authc,perms[admin:add]");
map.put("/admin/**", "authc,roles[admin]");
map.put("/user/**", "authc,roles[user]");
//表示所有的请求路径全部都需要被拦截登录, 这个必须必须写在 Map 集合的最后面,这个选项是可选的
//如果没有指定/** 那么如果某个请求不符合上面的拦截规则 Shiro 将方行这个请求
// map.put("/**","authc");
shiroFilter.setFilterChainDefinitionMap(map);
return shiroFilter;
}
}
2.2自定义MyRealm类,用于配置shiro对账号的认证和授权
package com.example.shiro.realm;
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.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.HashSet;
import java.util.Set;
/*
自定义的realm,用来实现用户的认证和授权
*/
public class MyRealm extends AuthorizingRealm {
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
//获取用户在浏览器中输入的账号
String username = token.getUsername();
//认证账号,正常情况我们需要这里从数据库中获取账号的信息, 以及其他关键数据,例如账号是否被冻结等等
String dbusername = username;
if (!"admin".equals(dbusername) && !"zhangsan".equals(dbusername)&&!"user".equals(dbusername)) {//判断用户账号是否正确
throw new UnknownAccountException("账号错误");
}
if ("zhangsan".equals(username)) {
throw new LockedAccountException("账号被锁定");
}
//定义一个密码这个密码应该来自数据库中
String dbpassword = "123456";
Object obj=new SimpleHash("MD5",dbpassword,"",1);
//认证密码是否正确
return new SimpleAuthenticationInfo(dbusername,obj.toString(), getName());
}
//授权;用户认证通过后,每次访问需要授权的请求时都需要执行这段代码来完成授权操作
//这里应该查询数据库来获取当前用户的所有角色和权限,并设置到shiro中
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//从 Shiro 中获取用户名
Object username=principalCollection.getPrimaryPrincipal();
//创建一个 SimpleAuthorizationInfo 类的对象,利用这个对象需要设置当前用户的权限信息
SimpleAuthorizationInfo simpleAuthorizationInfo=new SimpleAuthorizationInfo();
//创建角色信息的集合
//注意:由于每次访问需要授权的请求时,shiro都会执行这个方法,因此如果这里的数据来自于数据库中
//那么一定要控制,不能每次都从数据库中读取,这样效率太低了
Set<String> roles=new HashSet<String>();
//这里应该根据账号到数据库中获取用户的所对应的所有角色信息并初始化到 roles集合中
if("admin".equals(username)){
roles.add("admin");
roles.add("user");
}else if ("user".equals(username)){
roles.add("user");
}
Set<String>permission=new HashSet<String>();
if("admin".equals(username)){
permission.add("admin:add");
}
//设置角色信息
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permission);
return simpleAuthorizationInfo;
}
}
2.3 创建Controller
@RequestMapping("/login")
public String login(String username, String password, Model model) {
//创建一个 shiro 的 Subject 对象, 利用这个对象来完成用户的登录认证
Subject subject= SecurityUtils.getSubject();
//登出方法调用, 用于清空登录时的缓存信息, 否则无法重复登录
subject.logout();
//判断当前用户是否已经认证过,如果已经认证过着不需要认证如果没有认证过则进入if完成认证
if (!subject.isAuthenticated()) {
//创建一个用户账号和密码的 Token 对象, 并设置用户输入的账号和密码
//这个对象将在 Shiro 中被获取
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
//例如账号不存在或密码错误等等, 我们需要根据不同的异常类型来判断用户的登录状态并给与友好的信息提示
//调用 login 后 Shiro 就会自动执行我们自定义的 Realm 中的认证方法
subject.login(token);
} catch (UnknownAccountException e) {
//进入 catch 表示用户的账号错误, 这个异常是我们在后台抛出的
System.out.println("---------------账号不存在");
model.addAttribute("errorMessage", "账号不存在");
return "login";
} catch (LockedAccountException e) {
//进入 catch 表示用户的账号被锁定, 这个异常是我们在后台抛出的
System.out.println("===============账号被锁定");
model.addAttribute("errorMessage", "账号被冻结");
return "login";
} catch (IncorrectCredentialsException e) {
//进入 catch 表示用户的密码, 这个异常是 shiro 在认证密码时抛出
System.out.println("***************密码不匹配");
model.addAttribute("errorMessage", "密码错误");
return "login";
}
}
return "redirect:/success";
}
认证缓存: 当登录成功过一次以后我可以点击后退, 然后输入任意账号和密码这时无论输入什么信息Shiro 都会认为认证成功, 这是因为 Shiro 在登录成功以后会将数据写入 Shiro 的缓存导致,因此应该在登录请求的控制器中在判断是否认证过之前添加一个登出操作, 已清空缓存这样就可以重复测试登录
3.密码加密
//定义一个密码, 这个密码是数据库中的密码我们应该从数据库中获取
String dbpassword="123456";
//密码加密码
//参数 1 为加密算法 我们选择 MD5 加密
//参数 2 为被加密的数据的数据
//参数 3 为加密时的盐值 , 用于改变加密后数据结果
// 通常这个盐值需要选择一个表中唯一的数据例如表中的账号
//参数 4 为需要对数据使用指定的算法加密多少次
Object obj=new SimpleHash("MD5",dbpassword," ",1);
//认证密码是否正确 使用加密后的密码登录
return new SimpleAuthenticationInfo(dbusername,obj.toString(),getName());
注意:
1、 通 常 数 据 库 中 存 放 的 数 据 不 应 该 是 明 码 123456 而 是 加 密 后 的 数 据 例 如e10adc3949ba59abbe56e057f20f883e, 这是使用 MD5 加密后的 123456, 如果数据库中的密码已经是加密后的那么这里可以不选择进行加密。
2、 如果数据库中的密码已经加密那么页面中传递数据前必须要对密码进行加密才能传递, 否则无法可能会登录失败。
3、 如果选择加密传递那么页面和数据库中的密码加密次数以及盐必须相同, 否则登录一定失败。
4.基于注解的权限控制
4.1 修改配置类
如果需要使用Shiro 的注解权限必须要在配置类中启动 Shiro 注解支持
/**
* 开启 Shiro 的注解例如( @RequiresRoles @RequiresUser @RequiresPermissions)
* 需要借助 SpringAOP 来扫描这些注解
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator=new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
/
**
* 开启 AOP 的注解支持
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor=new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
注意:
启动注解的权限控制以后需要删除在 Shiro 配置类中的权限拦截的配置规则
map.put("/admin/test","authc,perms[admin:add]");
map.put("/admin/**","authc,roles[admin]");
map.put("/user/**","authc,roles[user]");
4.2 修改Controller
@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("---------------------------------");
//当出现异常时会跳转到noPermission.html页面
return "noPermission";
}
5.使用 Thymeleaf 整合 Shiro 标签
添加 Maven 依赖
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
添加配置 Bean
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
引入命名空间
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro"