1.知识点梳理
1.1什么是shiro:
Shiro是apache旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
身份认证:
就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令,看其是否与系统中存储的该用户的用户名和口令一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。
授权;
即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的
认证流程:
授权流程:
1.2shiro整合springboot思路:
通过ShiroFilter拦截所以后请求,再到安全管理器处理请求(SecurityManager),然后通过Realm获取数据库数据进行认证授权校验。
2.shiro基本框架的搭建
创建一个springboot项目,引入依赖:
<dependencies>
<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.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>
<!--引入jsp解析依赖-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>
<!--jstl-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--引入shiro整合springboot依赖-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.1</version>
</dependency>
</dependencies>
因为项目使用jsp,所以要在配置文件(application.properties)上修改jsp视图
#配置端口
server.port=8090
server.servlet.context-path=/shiro
spring.application.name=shiro
#配置jsp试图
spring.mvc.view.prefix=/
spring.mvc.view.suffix=.jsp
因为jsp与idea存在一定的不兼容,因此要修改启动配置,保证视图的实现
创建自定义Realm:
package com.study.springboot_shiro_boke.shiro.realm;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
//创建自定义realm
public class CustomRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return null;
}
}
创建配置类(整合shiro框架相关的配置类,项目启动先执行配置类)
package com.study.springboot_shiro_boke.config;
import com.study.springboot_shiro_boke.shiro.realm.CustomRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Configuration;
@Configuration
public class shiroconfig {
//创建shiroFilter拦截器 负责拦截所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给拦截器(shiroFilter)设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return shiroFilterFactoryBean;
}
//创建web安全管理器 给安全管理器设置realm
@Bean
public DefaultWebSecurityManager getdefaultWebSecurityManager(Realm realm){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm);
return defaultWebSecurityManager;
}
//创建自定义realm
@Bean
public Realm getRealm(){
CustomRealm customRealm = new CustomRealm();
return customRealm;
}
}
main下创建一个webapp包,创建jsp文件
Index.jsp
<%@page contentType="text/html;utf-8" pageEncoding="utf-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<%-- 受限资源--%>
<h1>系统主页</h1>
<ul>
<li><a href="#">用户管理</a></li>
<li><a href="#">商品管理</a></li>
<li><a href="#">订单管理</a></li>
<li><a href="#">物流管理</a></li>
</ul>
</body>
</html>
login.jsp
<%@page contentType="text/html;utf-8" pageEncoding="utf-8" isELIgnored="false" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>登录页面</h1>
<form action="" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br/>
请输入验证码:<input type="text" name="code">
<input type="submit" value="登录">
</form>
</body>
</html>
启动项目简单测试 (在浏览器上输入 localhost:8090/shiro/index.jsp)
2.1配置拦截器 实现拦截
然后我们通过配置拦截器,把index.jsp 给他拦截掉,jsp默认会走login.jsp
/**
* 整合shiro相关的配置类
*/
@Configuration
public class shiroconfig {
//创建shiroFilter拦截器 负责拦截所有请求
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给拦截器(shiroFilter)设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//设置 受限资源 || 公共资源
Map<String,String> map = new HashMap<String, String>();
map.put("/index.jsp","authc"); //authc这个资源是要经过认证和授权的
//默认会走login.jsp
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
//设置过滤
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
然后再次启动项目 在浏览器上输入 localhost:8090/shiro/index.jsp 会发现他会跳转到 login.jsp
这样就说明拦截成功
常见的拦截过滤器,我们可以通过用这些过滤器来配置控制指定url的权限
配置缩写 对应的过滤器 功能
anon AnonymousFilter 指定url可以匿名访问(访问时不需要认证授权)
authc FormAuthenticationFilter [指定url需要form表单登录,默认会从请求中获取username、password,rememberMe等参数并尝试登录,如果登录不了就会跳转到loginUrl配置的路径。我们也可以用这个过滤器做默认的登录逻辑,但是一般都是我们自己在控制器写登录逻辑的,自己写的话出错返回的信息都可以定制嘛。]
perms PermissionsAuthorizationFilter 需要指定权限才能访问
roles RolesAuthorizationFilter 需要指定角色才能访问
3.实现登录认证:
添加依赖(pom.xml)
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
添加配置 配置文件(application.properties)
#配置mysql
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123
#配置mybatis
mybatis.type-aliases-package=com.study.shiro_jsp_springbooot.entity
mybatis.mapper-locations=classpath*:mappers/**/*.xml
创建 实体类User, UserService ,UserMapper 实现从数据库中获取用户信息
User
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String username;
private String password;
private String salt;
}
UserMapper
@Repository
public interface UserMapper {
User findByUserName(@Param("username") String username);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.study.springboot_shiro_boke.mapper.UserMapper">
<select id="findByUserName" resultType="user" >
select * from t_user where username = #{username}
</select>
</mapper>
UserService
public interface UserService {
User findByUserName(String username);
}
UserServiceImp
@Service("userservice")
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public User findByUserName(String username) {
return userMapper.findByUserName(username);
}
}
创建自定义工具类(ApplicationContextUtile ) 获取bean对象
配置自定义realm,进行认证校验 (CustomRealm),因为我们自定realm是不在工厂内的,所以调用不了UserService ,因此我们要创建一个 在工厂获取bean对象的工具类。
package com.study.springboot_shiro_boke.util;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* 在工厂中获取bean对象的工具类
*/
@Component
public class ApplicationContextUtile implements ApplicationContextAware {
private static ApplicationContext context;
@Override //只要springboot启动成功后 就会把创建好的工厂以参数的形式回传给方法
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
//根据名字去获取工厂中指定的对象(bean)
public static Object getBean(String beanName){
return context.getBean(beanName);
}
}
通过工具类使用service进行数据查询校验,实现认证操作
package com.study.springboot_shiro_boke.shiro.realm;
import com.study.springboot_shiro_boke.entity.User;
import com.study.springboot_shiro_boke.service.UserService;
import com.study.springboot_shiro_boke.util.ApplicationContextUtile;
import org.apache.ibatis.jdbc.Null;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.CollectionUtils;
//创建自定义realm
public class CustomRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//获取用户身份信息
String principal = (String) token.getPrincipal();
//通过工具类访问数据库
UserService userservice = (UserService) ApplicationContextUtile.getBean("userservice");
User user = userservice.findByUserName(principal);
//判断用户是否存在
if (user != null ){
//参数1:返回数据库中正确的用户名 参数2:返回数据库中正确密码参数4:提供当前realm的名字
return new SimpleAuthenticationInfo(principal,
user.getPassword(),
this.getName());
}
return null;
}
}
创建UserController,修改login.jsp 实现登录
package com.study.springboot_shiro_boke.controller;
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.RequestMapping;
@Controller
@RequestMapping("/user")
public class UserController {
//登录
@RequestMapping("/login")
public String login(String username,String password){
try {
//1. 获取主体对象 subject
Subject subject = SecurityUtils.getSubject();
//2. 创建令牌
subject.login(new UsernamePasswordToken(username,password));
return "redirect:/index.jsp";
}catch (UnknownAccountException e){
e.printStackTrace();
System.out.println("登入失败:用户名错误!");
}catch (IncorrectCredentialsException e){
e.printStackTrace();
System.out.println("登入失败:密码错误!");
}
return "redirect:/login.jsp";
}
}
在login.jsp上添加 ${pageContext.request.contextPath}/user/login
重启项目实现登录认证
在系统主页添加退出登录
<a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
在UserController 上添加 退出登录操作
//退出
@RequestMapping("/logout")
public String logout(){
//获取主体对象
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login.jsp";
}
这样退出操作就完成了,点击退出登录就跳转到登录的页面
4.shiro的MD5+Salt+Hash (就是把明文密码转成密文)
思路:用户通过注册把账号密码存入数据库,密码做加盐(salt)处理,因此数据库要添加一个salt(盐)字段 存储盐,在登录的时候,通过用户名查询到用户信息,根据用户输入的密码和数据库查询的盐(salt)做加密运算,与数据库返回的密码进行比较做认证。
数据库添加字段:
4.1创建加盐工具类(Salt)
package com.study.springboot_shiro_boke.util;
import java.util.Random;
/**
* 随机盐工具类
* 生成salt的静态方法
*/
public class SaltUtil {
public static String getSalt(int n) {
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < chars.length; i++) {
char achar =chars[new Random().nextInt(chars.length)];
sb.append(achar);
}
return sb.toString();
}
}
在UserService、UserMapper上分别添加 向数据库保存数据
UserMapper:
//存储用户信息
void Save(@Param("user") User user);
UserMapper.xml
<insert id="Save" keyProperty="id" useGeneratedKeys="true">
insert into t_user values(#{user.id},#{user.username},#{user.password},#{user.salt})
</insert>
UserService:
//存储用户信息
void Save( User user);
在UserServiceImpl上实现盐的添加操作,以及将明文密码加密(进行md5操作、加盐、hash散列1024次),再存储到数据库中:
@Override
public void Save(User user) {
//1.生成随机盐
String salt = SaltUtil.getSalt(8);
//2.将随机盐存入数据库
user.setSalt(salt);
//3.将明文密码进行 md5 + salt +hash 实现密文处理
Md5Hash md5Hash = new Md5Hash(user.getPassword(),salt,1024);
//4.将密文保存到对象中
user.setPassword(md5Hash.toHex());
//5.将对象保存到数据库中
userMapper.Save(user);
}
在UserController上添加 register接口实现注册:
//注册
@RequestMapping("/register")
public String register(User user){
try {
userService.Save(user);
return "redirect:/login.jsp";
} catch (Exception e) {
e.printStackTrace();
}
return "redirect:/register.jsp";
}
添加注册页面 register.jsp
<%@page contentType="text/html;utf-8" pageEncoding="utf-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <%--引入shiro标签--%>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>用户注册</h1>
<form action="${pageContext.request.contextPath}/user/register" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="点击注册">
</form>
</body>
</html>
最后一定要到ShiroConfig的配置类 去释放拦截器 ,否则访问会报错
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//给拦截器(shiroFilter)设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
//设置 受限资源 || 公共资源
Map<String,String> map = new HashMap<String, String>();
map.put("/user/login","anon");
map.put("/register.jsp","anon");
map.put("/user/register","anon");
map.put("/**.jsp","authc"); //authc这个资源是要经过认证和授权的
//默认认证路径--当认证不通过时jsp会自动跳转到login.jsp
shiroFilterFactoryBean.setLoginUrl("/login.jsp");
//设置过滤
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
然后重启项目,实现注册,注册成功后会跳转到登录页面,此时还不能登陆我们刚刚注册的账号,因为我们存入数据的密码是加密的。而登录输入的密码是明文密码,肯定不匹配,因此报密码错误。
注册后数据库的密码
登录后
因此在实现登录时我们要告诉自定义realm我们要使用加密的密码,所以我们要在ShiroConfig配置类中,设置登录时的密码加密。
ShiroConfig:
//创建自定义realm
@Bean
public RealmgetRealm(){
CustomRealm customRealm = new CustomRealm();
//1.创建Hash凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//2.设置md5加密算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//3.设置散列次数 一般都是 1024 或者 2048
hashedCredentialsMatcher.setHashIterations(1024);
//4.修改凭证校验匹配器
customRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return customRealm;
紧接着在 自定义realm(CustomRealm)中,设置认证时的盐
然后重启项目 重新输入刚才注册的账号密码,登陆成功,跳转到主界面
以上实现了认证的操作,接下来是授权操作,即用户可以访问那些资源。
5.授权实现
5.1角色授权
1.在数据库创建新的表
t_perms:
t_role:
t_role_perms:t_user_role :
2.创建实体类
perms:
package com.study.springboot_shiro_boke.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Perms implements Serializable {
private int id;
private String name;
private String url;
}
role:
package com.study.springboot_shiro_boke.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class role implements Serializable {
private int id;
private String name;
private List<Perms> perms;
}
设置页面资源授权:
<%@page contentType="text/html;utf-8" pageEncoding="utf-8" isELIgnored="false" %>
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <%--引入shiro标签--%>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<h1>系统主页</h1>
<a href="${pageContext.request.contextPath}/user/logout">退出登录</a>
<ul>
<shiro:hasAnyRoles name="user,admin">
<li><a href="#">用户管理</a>
<ul>
<shiro:hasPermission name="user:add:*">
<li><a href="">添加</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:delete:*">
<li><a href="">删除</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:update:*">
<li><a href="">修改</a></li>
</shiro:hasPermission>
<shiro:hasPermission name="user:find:*">
<li><a href="">查询</a></li>
</shiro:hasPermission>
</ul>
</li>
</shiro:hasAnyRoles>
<shiro:hasRole name="admin">
<li><a href="#">商品管理</a></li>
<li><a href="#">订单管理</a></li>
<li><a href="#">物流管理</a></li>
</shiro:hasRole>
</ul>
</body>
</html>
这里涉及到到资源标识符:
权限字符串的规则是:资源标识符:操作:资源实例标识符,意思是对哪个资源的那个实例具有什么操作,
":"是资源/操作/实例的分隔符,权限字符串也可以使用*通配符 。
控制的是资源类型 后面就写* ; 控制的是某一个资源实例 后面写实例的id/或者实例的唯一标识
*:*:* 表示该用户对所有资源具有所有操作
例子:
用户创建权限:user:create, 或user:create:* --->资源类型 (最后一个资源*,代表所有可以省略)
用户修改实例001权限:user:update:001 --->资源实例
用户实例001的所有权限:user:*:001
3.向数据库中添加数据:
4.在UserService 和 UserMapp 上创建方法 实现通过用户名获取角色信息
UserMapper:
//通过用户名查找角色(role)
User findRoleByUserName(@Param("username") String username);
UserMapper.xml:
因为一个用户 可能会有多个角色,因此用户和角色是一对多的关系,所以处理一对多关系应当用<collection />标签。
<resultMap id="findRoleMap" type="user">
<id property="id" column="uid"></id>
<result property="username" column="username"></result>
<collection property="roles" javaType="list" ofType="role">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
</collection>
</resultMap>
<select id="findRoleByUserName" resultMap="findRoleMap">
select u.id uid, u.username , r.id , r.name from t_user u
left join t_role_perms tp on u.id = tp.id
left join t_role r on r.id = tp.id
where username = #{username}
</select>
UserService:
//通过用户名查找角色
User findRoleByUserName( String username);
UserServiceImpl:
@Override
public User findRoleByUserName(String username) {
return userMapper.findRoleByUserName(username);
}
5.在自定义realm(CustomRealm)上实现授权操作
//创建自定义realm
public class CustomRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取主身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
UserService userservice = (UserService) ApplicationContextUtile.getBean("userservice");
//根据主身份信息 获取 角色
User user = userservice.findRoleByUserName(primaryPrincipal);
//判断角色是否为空 若不为空则添加角色
if (!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
//添加角色信息
simpleAuthorizationInfo.addRole(role.getName());
});
return simpleAuthorizationInfo;
}
return null;
}
6.重启项目 进行登录 (localhost:8090/shiro/login.jsp)
因为用户kangkang 是admin角色 所以 它可以显示四个
然后我们用xiaoming进行登录 会发现他只有一个用户管理 因为 他的角色是 user
5.2资源授权
资源授权我们就会用到资源表示符,一个角色他也有可能拥有多个资源,因此我们可以通过角色id去获取。
1.创建方法 通过角色id查找资源
UserMapper:
//通过角色id 查找资源perms
List<Perms> findPermsByRoleId(@Param("Roleid") int Roleid);
UserMapper.xml:
<select id="findPermsByRoleId" resultType="perms">
select p.id,p.`name`,p.url,r.`name` from t_role r
left join t_role_perms rp on r.id = rp.roleid
left join t_perms p on p.id = rp.permsid
where r.id = #{Roleid}
</select>
UserService:
//通过角色id查找资源perms
List<Perms> findPermsByRoleId(int Roleid);
UserServiceImpl:
@Override
public List<Perms> findPermsByRoleId(int Roleid) {
return userMapper.findPermsByRoleId(Roleid);
}
2.在自定义realm(CustomRealm)配置授权资源
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取主身份信息
String primaryPrincipal = (String) principals.getPrimaryPrincipal();
UserService userservice = (UserService) ApplicationContextUtile.getBean("userservice");
//根据主身份信息 获取 角色
User user = userservice.findRoleByUserName(primaryPrincipal);
//判断角色是否为空 若不为空则添加角色
if (!CollectionUtils.isEmpty(user.getRoles())){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
user.getRoles().forEach(role -> {
//添加角色信息
simpleAuthorizationInfo.addRole(role.getName());
List<Perms> perms = userservice.findPermsByRoleId(role.getId());
if (!CollectionUtils.isEmpty(perms)){
perms.forEach(perms1 -> {
simpleAuthorizationInfo.addStringPermission(perms1.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
重启项目 同样会发现 kangkang、xiaoming这两个用户的区别:
以上就是shiro整合sprinboot的授权操作。
6.springboot+shiro 实现缓存
使用的缓存的作用:用来减轻数据库的访问压力,从而提高系统的查询效率
6.1Shiro默认的缓存 Ehcache
1.1引入依赖
<!--shiro与ehcache的整合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.7.1</version>
</dependency>
1.2在自定义Realm中(CustomRealm) 开启缓存
//创建自定义realm
@Bean
public Realm getRealm(){
CustomRealm customRealm = new CustomRealm();
//1.创建Hash凭证匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
//2.设置md5加密算法
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//3.设置散列次数 一般都是 1024 或者 2048
hashedCredentialsMatcher.setHashIterations(1024);
//4.修改凭证校验匹配器
customRealm.setCredentialsMatcher(hashedCredentialsMatcher);
//开启缓存管理
customRealm.setCacheManager(new EhCacheManager());
//全局的开启缓存管理
customRealm.setCachingEnabled(true);
//开启认证的缓存
customRealm.setAuthenticationCachingEnabled(true);
//给认证缓存在内存中取一个名字
customRealm.setAuthenticationCacheName("authenticationCache");
//开启授权的缓存
customRealm.setAuthorizationCachingEnabled(true);
//给授权缓存在内存中取一个名字
customRealm.setAuthorizationCacheName("authorizationCache");
return customRealm;
}
6.2使用Redis作为缓存实现
2.1引入依赖
<!-- redis 整合 springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.1</version>
</dependency>
2.2在配置文件(application.properties)中配置Redis连接在(这里是连接本地的redis)
#springboot 连接 redis 的配置
spring.redis.port=6379
spring.redis.host=localhost
spring.redis.database=0
2.3创建自定义shiro缓存管理器RedisCacheManager
package com.study.springboot_shiro_boke.shiro.cache;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
//自定义缓存管理器
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String cacheName) throws CacheException {
System.out.println();
return new RedisCache<K,V>(cacheName);
}
}
2.4创建自定义redis缓存的实现
package com.study.springboot_shiro_boke.shiro.cache;
import com.study.springboot_shiro_boke.util.ApplicationContextUtile;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collection;
import java.util.Set;
//自定义redis缓存的实现
public class RedisCache<k,v> implements Cache<k,v> {
private Object cacheName;
public RedisCache() {
}
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
//从缓存中获取数据
@Override
public v get(k k) throws CacheException {
System.out.println("get key:" + k);
return (v) getRedisTemplate().opsForHash().get(this.cacheName, k.toString());
}
//查询数据库 把数据放入缓存中
@Override
public v put(k k, v v) throws CacheException {
System.out.println("put key:" + k);
System.out.println("put value:" + v);
getRedisTemplate().opsForHash().put(this.cacheName, k.toString(), v);
return null;
}
//删除指定的key
@Override
public v remove(k k) throws CacheException {
return (v) getRedisTemplate().opsForHash().delete(this.cacheName, k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(this.cacheName); //把整个map都给删了
}
//计算认证和授权的数量
@Override
public int size() {
return getRedisTemplate().opsForHash().size(this.cacheName).intValue();
}
//获取keys
@Override
public Set<k> keys() {
return getRedisTemplate().opsForHash().keys(this.cacheName);
}
@Override
public Collection<v> values() {
return getRedisTemplate().opsForHash().values(this.cacheName);
}
public RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtile.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
因为shiro中提供的simpleByteSource实现没有实现序列化,所有在认证时会出现错误信息
因此我们要自动给salt设置序列化,所以要创建一个实现salt的类 MyByteSouce
package com.study.springboot_shiro_boke.shiro.salt;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.codec.CodecSupport;
import org.apache.shiro.codec.Hex;
import org.apache.shiro.util.ByteSource;
import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Arrays;
public class MyByteSource implements ByteSource, Serializable {
private byte[] bytes;
private String cachedHex;
private String cachedBase64;
//加入无参构造方法实现序列化和反序列化
public MyByteSource(){
}
public MyByteSource(byte[] bytes) {
this.bytes = bytes;
}
public MyByteSource(char[] chars) {
this.bytes = CodecSupport.toBytes(chars);
}
public MyByteSource(String string) {
this.bytes = CodecSupport.toBytes(string);
}
public MyByteSource(ByteSource source) {
this.bytes = source.getBytes();
}
public MyByteSource(File file) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(file);
}
public MyByteSource(InputStream stream) {
this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream);
}
public static boolean isCompatible(Object o) {
return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
}
public byte[] getBytes() {
return this.bytes;
}
public boolean isEmpty() {
return this.bytes == null || this.bytes.length == 0;
}
public String toHex() {
if (this.cachedHex == null) {
this.cachedHex = Hex.encodeToString(this.getBytes());
}
return this.cachedHex;
}
public String toBase64() {
if (this.cachedBase64 == null) {
this.cachedBase64 = Base64.encodeToString(this.getBytes());
}
return this.cachedBase64;
}
public String toString() {
return this.toBase64();
}
public int hashCode() {
return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
}
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o instanceof ByteSource) {
ByteSource bs = (ByteSource)o;
return Arrays.equals(this.getBytes(), bs.getBytes());
} else {
return false;
}
}
private static final class BytesHelper extends CodecSupport {
private BytesHelper() {
}
public byte[] getBytes(File file) {
return this.toBytes(file);
}
public byte[] getBytes(InputStream stream) {
return this.toBytes(stream);
}
}
}
创建完实现序列化接口类后,我们要在自定义realm(CustomRealm)中修改,修改成我们自己创建的序列化盐。
修改配置类中开启的默认缓存EhCache,他他改成我们的自定义缓存管理器
然后在控制台打开我们的redis服务器,客户端
重启项目 执行登录 然后查询会看到数据存入redis缓存当中
然后刷新登录界面 会发现 数据不再存数据库中查询,而是从缓存中获取