一.shiro安全框架(安全认证+授权+加密等)
更多详细介绍参考->大神篇 1.目前市面主流的安全框架
- shiro:轻量级的,使用很方便,灵活,是apache提供的,在任何框架的
- SpringSecurity:是Spring家族的一部分,很多项目中会使用spring全家桶,相对与shiro来说,springSecurity更重量级,必须要求spring环境;相对shiro而言,功能更强大
2.什么是shiro Shiro是apache旗下一个开源安全框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证、权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
Shiro的功能模块如下图所示:

- Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
- SessionManagement:对用户的会话信息进行管理,使Session不再仅限于JavaEE应用,同时扩展了Session数据的存储途径及缓存方式,更易于实现Session数据的集群共享。
- Cryptography:加密,保护数据的安全性,以简洁的API提供常用的加密算法和数据摘要算法。
3. 为什么使用Shiro
- 使用shiro可以非常快速的完成认证、授权等功能的开发,降低系统成本。
- 较之SpringSecurity,Shiro在保持强大功能的同时,在简单性和灵活性方面拥有较为明显的优势。
4. Shiro架构设计
Shiro架构设计如下图所示:  通过Shiro框架进行权限管理时,要涉及到的一些核心对象,主要包括: 认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象, 以及Realm管理对象(领域对象:负责处理认证和授权领域的数据访问)
- Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
- SecurityManager(安全管理器):Shiro的核心,用来协调管理组件工作。
- Authenticator(认证管理器):负责执行认证操作。 Authorizer(授权管理器):负责授权检测。
- SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的Session 体验。
- SessionDAO:代表SessionManager执行Session持久(CRUD)操作,它允许任何存储的数据挂接到session管理器上。
- CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
- Cryptography(加密管理器):提供了加密方式的设计及管理。
- Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。
二.spring中的cache缓存
更多详细介绍请参考->大神篇
参考第二篇章
1.为什么要使用缓存
-
提升性能 绝大多数情况下,关系型数据库select查询是出现性能问题最大的地方。一方面,select会有很多像join、 group、order、like等这样丰富的语义,而这些语义是非常耗性能的;另一方面,大多数应用都是读多写少,所以加剧了慢查询的问题。 分布式系统中远程调用也会耗很多性能,因为有网络开销,会导致整体的响应时间下降。为了挽救这样的性能开销,在业务允许的情况(不需要太实时的数据)下,使用缓存是非常必要的事情。,使用缓存,减少查询数据库次数,提升性能 -
缓解数据库压力: 当用户请求增多时,数据库的压力将大大增加,通过缓存能够大大降低数据库的压力。
2.Spring Cache缓存
Spring Cache 是作用在方法上的,其核心思想是,当我们在调用一个缓存方法时会把该方法参数和返回结果作为一个键值对存在缓存中。
使用缓存最关键的一点就是保证:缓存与数据库的数据一致性
- 更新写数据:先把数据存到数据库中,然后再让缓存失效或更新。缓存操作失败,数据库事务回滚。
- 删除写数据: 先从数据库里面删掉数据,再从缓存里面删掉。缓存操作失败,数据库事务回滚。 查询读数据
- 缓存命中:先去缓存 cache中取数据,取到后返回结果。
- 缓存失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,在将数据放到缓存中。
如果上面的这些更新、删除、查询操作流程全都由程序员通过编码来完成的话 - 因为加入缓存层,程序员的编码量大大增多 缓存层代码和业务代码耦合,造成难以维护的问题。
3.运行流程
- 方法运行之前,先去查询Cache(缓存组件),按照cacheNames指定的名字获取;CacheManager先获取相应的缓存),第一次获取缓存如果没有Cache组件会自动创建。
- 去Cache中查找缓存的内容,使用一个key,默认就是方法的参数;key是按照某种策略生成的;默认是使用keyGenerator生成的,默认使用SimpleKeyGenerator生成key;SimpleKeyGenerator生成key的默认策略;如果没有参数;key=new SimpleKey();如果有一个参数:key=参数的值;如果有多个参数:key=new SimpleKey(params);
- 没有查到缓存就调用目标方法; 将目标方法返回的结果,放进缓存中
4.Spring缓存注解@Cache,@CachePut , @CacheEvict,@CacheConfig使用
@Cacheable、@CacheEvict、@CachePut 三个注解非常灵活,满足了我们对数据缓存的绝大多数使用场景,并且使用起来非常的简单而又强大,在实际工作中我们可以灵活搭配使用。
Spring 提供了基于注释驱动的 Spring Cache,它是一个对缓存使用的抽象,将我们常用的缓存策略都进行了高度抽象,让我们在项目中使用时只需要添加几个注解,即可完成大多数缓存策略的实现。Spring Boot Starter Cache 是 Spring Boot 提供给我们在 Spring Boot 中使用 Spring Cache 的 Starter 包,集成后方便在 Spring Boot 体系中使用缓存。
(1)@Cache(“something")
- 这个相当于save()操作, save()保存当前状态
(2)@Cacheable 的作用
主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,其参数如下(一般用于查询时缓存数据)
主要参数 | 解释 | 举例 |
---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 如 @Cacheable(value=“mycache”) 或者 @Cacheable(value={“cache1”,“cache2”} | key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 如 @Cacheable(value=“testcache”,key=“#userName”) | condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | 如 @Cacheable(value=“testcache”,condition=“#userName.length()>2”) |
- 当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,
- 如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。 “accountCache”缓存是在 spring*.xml 中定义的名称。
(3) @CachePut 的作用->更新缓存(需与DB数据库保存一致性)
是主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用,@cachePut相当于Update()操作,只要他标示的方法被调用,那么都会缓存起来,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
主要参数 | 解释 | 举例 |
---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 如 @Cacheable(value=“mycache”) 或者 @Cacheable(value={“cache1”,“cache2”} | key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 如 @Cacheable(value=“testcache”,key=“#userName”) | condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | 如 @Cacheable(value=“testcache”,condition=“#userName.length()>2”) |
(4)@CacheEvict的作用->清除缓存(正常清空->异常清空)
主要针对方法配置,能够根据一定的条件对缓存进行清空。
主要参数 | 解释 | 举例 |
---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 如 @Cacheable(value=“mycache”) 或者 @Cacheable(value={“cache1”,“cache2”} | key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | 如 @Cacheable(value=“testcache”,key=“#userName”) | condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才清空缓存 | 如 如 @CachEvict(value=“testcache”,condition=“#userName.length()>2”) | allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | 如 @CachEvict(value=“testcache”,allEntries=true) | beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | 如 @CachEvict(value=“testcache”,beforeInvocation=true) |
- 标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。 注意其中一个 @CacheEvict(value=”accountCache”,key=”#account.getName()”), 其中的 Key 是用来指定缓存的 key 的,这里因为我们保存的时候用的是 account 对象的 name 字段,所以这里还需要从参数 account 对象中获取 name 的值来作为 key,前面的 # 号代表这是一个 SpEL 表达式, 此表达式可以遍历方法的参数对象,具体语法可以参考 Spring 的相关文档手册。
(5)@CacheConfig 配置类的方式,对特殊缓存条件进行缓存
三.代码实现-完整版
1.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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<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.tjetc</groupId>
<artifactId>springboot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-shiro</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--thymeleaf 模板依赖-->
<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.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!--引入shrio-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.5.3</version>
</dependency>
<!--引入shiro和ehcache,缓存ehcache操作数据-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.5.3</version>
</dependency>
<!--有默认配置。所以需要屏蔽shiro,SpringBoot自动加载机制
在SpringBoot启动类中输入 ShiroAutoConfiguration 把出现的三个匹配的项目排除
-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources</directory>
<includes>
<include>**/*.*</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
2.application.yml
server:
port: 8088
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://123.123.58.230:3306/springboot?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&autoReconnect=true
username: root
password:
application:
name: springboot-shiro
main:
allow-circular-references: true
thymeleaf:
cache: false
prefix: classpath:/templates/
suffix: .html
check-template-location: true
encoding: utf-8
mode: HTML
servlet:
content-type: text/html
mybatis:
type-aliases-package: com.tjetc.domain
logging:
level:
com.tjetc.mapper: debug
3.启动类SpringbootShiroApplication
package com.tjetc;
import org.apache.shiro.spring.boot.autoconfigure.ShiroAnnotationProcessorAutoConfiguration;
import org.apache.shiro.spring.boot.autoconfigure.ShiroAutoConfiguration;
import org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.ComponentScan;
/**在很多SpringBoot项目中,common或者parent
* 做了shiro依赖,这样其他模块项目
* 总是提示 No bean of type 'org.apache.shiro.realm.Realm' found*/
@SpringBootApplication(exclude = {
ShiroAnnotationProcessorAutoConfiguration.class,
ShiroAutoConfiguration.class,
ShiroBeanAutoConfiguration.class})
@MapperScan("com.tjetc.mapper")
//开启基于注解的缓存
@EnableCaching
public class SpringbootShiroApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootShiroApplication.class, args);
}
}
4.controller控制层
LoginController .java
package com.tjetc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @belongProject:hibernate
* @belongPackage:com.tjetc.controller
* @author:xujirong
* @dscription:TODO
* @date:2022-09-18 19:41
* @version:1.0
*/
@Controller
@RequestMapping("/login")
public class LoginController {
/**
*@descriprion: 跳转至登录页面
*@author: xujirong
*@date: 2022/9/24 17:35
*@return: java.lang.String
*/
@GetMapping("/initLogin")
public String initLogin(){
return "login";
}
/**
*@descriprion: 跳转至首页
*@author: xujirong
*@date: 2022/9/24 17:36
*@return: java.lang.String
*/
@GetMapping("/initIndex")
public String initIndex(){
return "index";
}
/**
*@descriprion: 跳转至注册页面
*@author: xujirong
*@date: 2022/10/1 22:13
*@return:
*/
@GetMapping("/initRegister")
public String initRegister(){
return "register";
}
}
UserController .java
package com.tjetc.controller;
import com.tjetc.domain.User;
import com.tjetc.service.UserService;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
/**
*@descriprion: @Cacheable
* @Cacheable(value=”accountCache”),这个注释的意思是,当调用这个方法的时候,会从一个名叫 accountCache 的缓存中查询,
* 如果没有,则执行实际的方法(即查询数据库),并将执行的结果存入缓存中,否则返回缓存中的对象。
* 这里的缓存中的 key 就是参数 userName,value 就是 Account 对象。
* “accountCache”缓存是在 spring*.xml 中定义的名称。
*
* @CacheEvict
* @CacheEvict 注释来标记要清空缓存的方法,当这个方法被调用后,即会清空缓存。
* 注意其中一个 @CacheEvict(value=”accountCache”,key=”
* 其中的 Key 是用来指定缓存的 key 的,这里因为我们保存的时候用的是 account 对象的 name 字段,
* 所以这里还需要从参数 account 对象中获取 name 的值来作为 key,前面的
* 此表达式可以遍历方法的参数对象,具体语法可以参考 Spring 的相关文档手册。
*
* @CachePut
* @CachePut 注释,这个注释可以确保方法被执行,同时方法的返回值也被记录到缓存中,实现缓存与数据库的同步更新。
*@author: xujirong
*@date: 2022/10/1 23:50
*@return:
*/
@Slf4j
@Controller
@RequestMapping("/user")
public class UserController {
//以下用到的cache缓存中-value和cacheNames作用是一样的
//指定一个缓存对象
@Autowired
private UserService userService;
/**
*@descriprion: 用户注册
*@author: xujirong
*@date: 2022/9/24 17:35
*@return: java.lang.String
*/
@PostMapping("/register")
public String register(User user) {
if (user==null || "".equals(user.getUsername()) || "".equals(user.getPassword())){
log.info("用户名:{},密码:{}",user.getUsername(),user.getPassword()+",其中一个为空");
return "register";
}
User dbUser = userService.findByUsername(user.getUsername());
if (dbUser!=null){
log.info("用户名:{},已存在,请重新输入.....",user.getUsername());
return "register";
}
try {
userService.register(user);
return "login";
} catch (Exception e) {
e.printStackTrace();
return "register";
}
}
/**
*@descriprion: 用户退出
*@author: xujirong
*@date: 2022/9/24 17:35
*@return: java.lang.String
*/
@RequestMapping("/logout")
public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
/**
*@descriprion: 前端提交登录时-调用shiro的验证方法 login
* 获取身份验证信息 Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的
*@author: xujirong
*@date: 2022/9/24 17:16
*@return: java.lang.String
* new UsernamePasswordToken(username,password):用户身份信息
*/
@RequestMapping("/login")
public String login(String username, String password, Model model) {
if (username==null || "".equals(username)){
return "login";
}
//获取主题对象
Subject subject = SecurityUtils.getSubject();
try {
model.addAttribute("username",username);
subject.login(new UsernamePasswordToken(username,password));
System.out.println("登录成功!!!");
return "index";
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在!!!");
} catch (IncorrectCredentialsException e) {
System.out.println("密码错误!!!");
} catch (Exception e) {
e.printStackTrace();
System.out.println("此用户不是通过shiro注册验证的用户,不具备shiro验证条件");
}
//若抛出异常,则重新跳转至登录页面
return "login";
}
/**
*@descriprion: 查询所有数据,并以listUser为名称缓存数据
* 第二次查询时,从一个名叫 listUser 的缓存中查询
*@author: xujirong
*@date: 2022/10/1 19:29
*@return: java.util.List<com.tjetc.domain.User>
*/
@RequestMapping("/queryUsers")
@Cacheable(value = "listUser")
@ResponseBody
public List<User> queryUsers(){
List<User> userList = userService.queryUsers();
System.out.println("userList = " + userList);
log.info("搜索所有用户记录:{}",userList);
return userList;
}
/**
*@descriprion: 根据id查询数据,并以id为标识,将数据缓存到cache中至users组中
*@author: xujirong
*@date: 2022/10/1 19:25
*@return: com.tjetc.domain.User
*/
@RequestMapping("/findUserById")
@Cacheable(value = "user",key = "#id")
@ResponseBody
public User findUserById(String id){
User user = userService.findUserById(id);
log.info("搜索记录id:{},添加数据缓存",user.getId());
return user;
}
/**
*@descriprion: 数据删除时,将数据从缓存中清除
*@author: xujirong
*@date: 2022/10/1 19:33
*@return:
*/
@RequestMapping("/deleteById")
@CacheEvict(key = "#id",cacheNames = "user")
@ResponseBody
public void deleteById(String id){
log.info("删除记录id:{},移除数据缓存",id);
}
/**
* CachePut缓存新增的或更新的数据到缓存,其中缓存名称为users,数据的key是参数的id。
* 且key获取到的id不能为空,变更之后,同时更新cache缓存中的数据
* @param user
*/
@RequestMapping("/updateUserById")
@CachePut(value = "user",key = "#user.id")
@ResponseBody
public User updateUserById(User user){
//userService.updateUserById(user);
log.info("变更数据user:{},cache数据缓存",user);
return user;
}
}
OrderController .java
package com.tjetc.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @belongProject:hibernate
* @belongPackage:com.tjetc.controller
* @author:xujirong
* @dscription:订单控制类
* @date:2022-09-25 12:03
* @version:1.0
*/
@Controller
@RequestMapping("/order")
public class OrderController {
@RequiresRoles(value={"admin","user"})//用来判断角色 同时具有 admin user
@RequiresPermissions("user:update:01") //用来判断权限字符串
@RequestMapping("/save")
public String save() {
//基于角色
//获取主体对象
Subject subject = SecurityUtils.getSubject();
//代码方式
if (subject.hasRole("admin")) {
System.out.println("保存订单!");
}else{
System.out.println("无权访问!");
}
System.out.println("进入save方法============");
return "index";
}
}
5.service业务层
package com.tjetc.service;
import com.tjetc.domain.Perms;
import com.tjetc.domain.User;
import java.util.List;
public interface UserService {
//注册用户方法
void register(User user);
//根据用户名查找用户
User findByUsername(String username);
//根据用户名查询所有角色
User findRoleByUserName(String username);
//根据角色id查询权限集合
List<Perms> findPermsByRoleId2(String id);
List<User> queryUsers();
User findUserById(String id);
}
package com.tjetc.service.impl;
import com.tjetc.domain.Perms;
import com.tjetc.domain.User;
import com.tjetc.mapper.UserMapper;
import com.tjetc.service.UserService;
import com.tjetc.utils.SaltUtils;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service("userService")
public class UserServiceImpl implements UserService {
@Resource
private UserMapper userMapper;
@Override
public void register(User user) {
try {
//1.获取随机盐
String salt = SaltUtils.getSalt(8);
//2.将随机盐保存到数据
user.setSalt(salt);
//3.明文密码进行md5 + salt + hash散列
Md5Hash MD5 = new Md5Hash(user.getPassword(),salt,1024);
user.setPassword(MD5.toHex());
userMapper.save(user);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public User findByUsername(String username) {
return userMapper.findByUsername(username);
}
@Override
public User findRoleByUserName(String username) {
return userMapper.findRoleByUserName(username);
}
@Override
public List<Perms> findPermsByRoleId2(String id) {
return userMapper.findPermsByRoleId2(id);
}
@Override
public List<User> queryUsers() {
return userMapper.queryUsers();
}
@Override
public User findUserById(String id) {
return userMapper.findUserById(id);
}
}
6.Mapper数据层
package com.tjetc.mapper;
import com.tjetc.domain.Perms;
import com.tjetc.domain.User;
import org.springframework.stereotype.Repository;
import java.util.List;
public interface UserMapper {
void save(User user);
User findByUsername(String username);
User findRoleByUserName(String username);
List<Perms> findPermsByRoleId2(String id);
List<User> queryUsers();
User findUserById(String id);
}
<?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.tjetc.mapper.UserMapper">
<select id="findByUsername" parameterType="string" resultType="user">
select * from t_user where username like concat('%',
</select>
<insert id="save" parameterType="user">
insert into t_user (username,password,salt) values (
</insert>
<resultMap id="userMap" type="User">
<id column="uid" property="id"/>
<result column="username" property="username"/>
<!--角色信息-->
<collection property="roles" javaType="list" ofType="Role">
<id column="id" property="id"/>
<result column="rname" property="name"/>
</collection>
</resultMap>
<select id="findRoleByUserName" parameterType="String" resultMap="userMap">
SELECT u.id uid,u.username,r.id,r.name rname
FROM t_user u
LEFT JOIN t_user_role ur
ON u.id=ur.userid
LEFT JOIN t_role r
ON ur.roleid=r.id
WHERE u.username=
</select>
<resultMap id="roleMap" type="Role">
<result column="name" property="name"/>
<collection property="perms" javaType="list" ofType="Perms">
<id column="id" property="id"/>
<result column="name" property="name"/>
<result column="url" property="url"/>
</collection>
</resultMap>
<select id="findPermByRoleId" parameterType="String" resultType="Role" resultMap="roleMap">
SELECT r.`name`,p.`id`,p.`name`,p.`url`
FROM t_role r
LEFT JOIN t_role_perms rp
ON r.`id` = rp.`roleid`
LEFT JOIN t_pers p
ON rp.`permsid` = p.`id`
WHERE r.`id` =
</select>
<select id="findPermsByRoleId2" parameterType="String" 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_pers p ON rp.permsid=p.id
WHERE r.id=
</select>
<select id="queryUsers" resultType="user">
select * from t_user
</select>
<select id="findUserById" parameterType="string" resultType="user">
select * from t_user where id=
</select>
</mapper>
7.config配置类,util工具类
package com.tjetc.config;
import com.tjetc.realm.CustomerRealm;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.ehcache.EhCacheManager;
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.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
*@descriprion: 如果要使用ShiroFilter过滤 ,则以下的几个创建bean缺一不可
*@author: xujirong
*@date: 2022/9/24 19:44
*/
@Configuration
public class ShiroConfig {
/**
*@descriprion: ShiroFilter-过滤所有请求 ,过滤web前端请求
*@author: xujirong
*@date: 2022/9/24 19:10
*@return: org.apache.shiro.spring.web.ShiroFilterFactoryBean
*/
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//给ShiroFilter配置安全管理器-Shiro的核心安全接口,是必须的
shiroFilter.setSecurityManager(securityManager);
// 设置认证界面路径
//身份认证失败,则跳转到登录页面的配置 没有登录的用户请求需要登录的页面时自动跳转到登录页面,
//不是必须的属性,不输入地址的话会自动寻找项目web项目的根目录下的”/login”登录页面。
shiroFilter.setLoginUrl("/login/initLogin");
//shiroFilter.setSuccessUrl("");
// 登录成功默认跳转页面,不配置则跳转至”/”。
// 如果登陆前点击的一个需要登录的页面,则在登录自动跳转到那个需要登录的页面。不跳转到此。
//shiroFilter.setUnauthorizedUrl("");
// 没有权限默认跳转的页面
//shiroFilter.setFilterChainDefinitions("");
//filterChainDefinitions的配置顺序为自上而下,以最上面的为准
//当运行一个Web应用程序时,Shiro将会创建一些有用的默认Filter实例,
// 并自动地在[main]项中将它们置为可用自动地可用的默认的Filter实例是被DefaultFilter枚举类定义的,
// 枚举的名称字段就是可供配置的名称
/*
* anon---------------org.apache.shiro.web.filter.authc.AnonymousFilter
没有参数,表示可以匿名使用。
* authc--------------org.apache.shiro.web.filter.authc.FormAuthenticationFilter
表示需要认证(登录)才能使用,没有参数
* authcBasic---------org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
没有参数表示httpBasic认证
* logout-------------org.apache.shiro.web.filter.authc.LogoutFilter
* noSessionCreation--org.apache.shiro.web.filter.session.NoSessionCreationFilter
* perms--------------org.apache.shiro.web.filter.authz.PermissionAuthorizationFilter
参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],
当有多个参数时必须每个参数都通过才通过,想当于isPermitedAll()方法。
* port---------------org.apache.shiro.web.filter.authz.PortFilter port[8081],
当请求的url的端口不是8081是跳转到schemal://serverName:8081?queryString,其中schmal是协议http或https等,serverName是你访问的host,8081是url配置里port的端口,queryString是你访问的url里的?后面的参数。
* rest---------------org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter
根据请求的方法,相当于/admins/user/**=perms[user:method] ,其中method为post,get,delete等。
* roles--------------org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
参数可以写多个,多个时必须加上引号,并且参数之间用逗号分割,当有多个参数时,例如admins/user/**=roles["admin,guest"],每个参数通过才算通过,相当于hasAllRoles()方法。
* ssl----------------org.apache.shiro.web.filter.authz.SslFilter
没有参数,表示安全的url请求,协议为https
* user---------------org.apache.shiro.web.filter.authz.UserFilter
没有参数表示必须存在用户,当登入操作时不做检查
*/
/*
* 通常可将这些过滤器分为两组
* anon,authc,authcBasic,user是第一组认证过滤器
* perms,port,rest,roles,ssl是第二组授权过滤器
* 注意user和authc不同:当应用开启了rememberMe时,用户下次访问时可以是一个user,但绝不会是authc,因为authc是需要重新认证的
* user表示用户不一定已通过认证,只要曾被Shiro记住过登录状态的用户就可以正常发起请求,比如rememberMe 说白了,以前的一个用户登录时开启了rememberMe,然后他关闭浏览器,下次再访问时他就是一个user,而不会authc
*
*
* 举几个例子
* /admin=authc,roles[admin] 表示用户必需已通过认证,并拥有admin角色才可以正常发起'/admin'请求
* /edit=authc,perms[admin:edit] 表示用户必需已通过认证,并拥有admin:edit权限才可以正常发起'/edit'请求
* /home=user 表示用户不一定需要已经通过认证,只需要曾经被Shiro记住过登录状态就可以正常发起'/home'请求
*/
/*
* 各默认过滤器常用如下(注意URL Pattern里用到的是两颗星,这样才能实现任意层次的全匹配)
* /admins/**=anon 无参,表示可匿名使用,可以理解为匿名用户或游客
* /admins/user/**=authc 无参,表示需认证才能使用
* /admins/user/**=authcBasic 无参,表示httpBasic认证
* /admins/user/**=ssl 无参,表示安全的URL请求,协议为https
* /admins/user/**=perms[user:add:*] 参数可写多个,多参时必须加上引号,
且参数之间用逗号分割,如/admins/user/**=perms["user:add:*,user:modify:*"]。
当有多个参数时必须每个参数都通过才算通过,相当于isPermitedAll()方法
* /admins/user/**=port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString。其中schmal是协议http或https等,serverName是你访问的Host,8081是Port端口,queryString是你访问的URL里的?后面的参数
* /admins/user/**=rest[user] 根据请求的方法,相当于/admins/user/**=perms[user:method],其中method为post,get,delete等
* /admins/user/**=roles[admin] 参数可写多个,多个时必须加上引号,且参数之间用逗号分割,如:/admins/user/**=roles["admin,guest"]。当有多个参数时必须每个参数都通过才算通过,相当于hasAllRoles()方法
*
*/
//Shiro验证URL时,URL匹配成功便不再继续匹配查找(所以要注意配置文件中的URL顺序,尤其在使用通配符时)
// 配置不会被拦截的链接 顺序判断
//配置系统受限资源
//配置系统公共资源
Map<String, String> filterMap = new HashMap<String, String>();
//表示这个为公共资源 一定是在受限资源上面
filterMap.put("/user/login", "anon");
//表示这个为公共资源 一定是在受限资源上面
filterMap.put("/login/initRegister", "anon");
//表示这个为公共资源 一定是在受限资源上面
filterMap.put("/user/register", "anon");
//表示这个受限资源需要认证和授权-除了以上资源以外,其它的路径必须要先验证
//也就是,需要先登录完成,通过后才能访问
filterMap.put("/**", "authc");
shiroFilter.setFilterChainDefinitionMap(filterMap);
String loginUrl = shiroFilter.getLoginUrl();
System.out.println("loginUrl = " + loginUrl);
return shiroFilter;
}
/**
*@descriprion: 创建创建安全管理器-将安全管理器对象纳入spring容器管理
*@author: xujirong
*@date: 2022/9/24 19:41
*@return: org.apache.shiro.realm.Realm
*/
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(Realm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
return securityManager;
}
/**
*@descriprion: 创建自定义Realm-将Realm纳入spring容器管理
* 设置凭证匹配器(设置凭证匹配时加密的算法,加密的次数)
*@author: xujirong
*@date: 2022/9/24 19:41
*@return: org.apache.shiro.realm.Realm
*/
@Bean
public Realm getRealm() {
CustomerRealm realm = new CustomerRealm();
HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
//设置使用MD5加密算法
credentialsMatcher.setHashAlgorithmName("md5");
//散列次数-迭代次数
credentialsMatcher.setHashIterations(1024);
realm.setCredentialsMatcher(credentialsMatcher);
//开启缓存管理器-会将第一次查询到的数据进行缓存,下次就不会去查数据库表了
realm.setCacheManager(new EhCacheManager());
realm.setCachingEnabled(true);//开启缓存
realm.setAuthenticationCachingEnabled(true);//开启认证缓存
realm.setAuthenticationCacheName("authentication");
realm.setAuthorizationCachingEnabled(true);//开启授权缓存
realm.setAuthorizationCacheName("authorization");
return realm;
}
}
package com.tjetc.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class ApplicationContextUtil implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
//根据bean名字获取工厂中指定bean 对象
public static Object getBean(String beanName) {
return context.getBean(beanName);
}
}
SaltUtils .java
package com.tjetc.utils;
import java.util.Random;
/**
* @belongProject:hibernate
* @belongPackage:com.tjetc.utils
* @author:xujirong
* @dscription:生成shiro验证随机盐salt
* @date:2022-09-18 13:25
* @version:1.0
*/
public class SaltUtils {
public static String getSalt(int n) {
char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890!@#$%^&*()".toCharArray();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
char c = chars[new Random().nextInt(chars.length)];
sb.append(c);
}
return sb.toString();
}
public static void main(String[] args) {
System.out.println(getSalt(100));
}
}
8.shiro框架中的Realm对象类
package com.tjetc.realm;
import com.tjetc.domain.Perms;
import com.tjetc.domain.User;
import com.tjetc.service.UserService;
import com.tjetc.utils.ApplicationContextUtil;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* 获取身份验证信息 Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的
*/
public class CustomerRealm extends AuthorizingRealm {
//授权实现-给用户赋予权限
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String principal = (String) principalCollection.getPrimaryPrincipal();
//获取UserService对象
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
//System.out.println(userService);
//基于角色授权
User user = userService.findRoleByUserName(principal);
System.out.println("user======="+user);
if (!CollectionUtils.isEmpty(user.getRoles())) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
user.getRoles().forEach(role ->{
info.addRole(role.getName());
/*Role role2 = userService.findPermByRoleId(role.getId());
System.out.println("role2======"+role2);
if (!CollectionUtils.isEmpty(role2.getPerms())) {
role2.getPerms().forEach(perm -> info.addStringPermission(perm.getName()));
}*/
//权限信息
List<Perms> perms = userService.findPermsByRoleId2(role.getId());
System.out.println("perms========"+perms);
if(!CollectionUtils.isEmpty(perms) && perms.get(0)!=null ){
perms.forEach(perm->{
info.addStringPermission(perm.getName());
});
}
});
System.out.println("info = " + info);
return info;
}
//用户名为xujr3,则赋予以下角色-授权
if ("xujr3".equals(principal)) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRole("admin");
info.addRole("user");
info.addStringPermission("user:find:*");
info.addStringPermission("admin:*");
return info;
}
return null;
}
/**
* 获取身份验证信息 Shiro中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。
*
* @param authenticationToken
* 用户身份信息 token
* @return 返回封装了用户信息的 AuthenticationInfo 实例
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
String username = (String) authenticationToken.getPrincipal();
//获取UserService对象
UserService userService = (UserService) ApplicationContextUtil.getBean("userService");
System.out.println(userService);
//根据用户名-到数据库中查询用户
User user = userService.findByUsername(username);
if (!ObjectUtils.isEmpty(user)) {
String name = this.getName();
// 获取盐值,即用户名- 盐值,需要注意,此处需要的盐值是ByteSource类型(是shiro中的盐值对象)
ByteSource salt = ByteSource.Util.bytes(user.getSalt());
System.out.println("name = " + name);
System.out.println("salt = " + salt);
//注意,数据库中的user的密码必须是要经过md5加密,生成一个随机盐salt,
//不然会抛出异常:java.lang.IllegalArgumentException: Odd number of characters.!!!!!
//用户注册时,会经过md5加密,并且会生成一个随机盐salt
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), salt, this.getName());
}
return null;
}
}
9.实体类对象
package com.tjetc.domain;
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)
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private String id;
private String username;
private String password;
private String salt;
private String photopath;
//定义角色集合
private List<Role> roles;
}
package com.tjetc.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.List;
/**
* @belongProject:hibernate
* @belongPackage:com.tjetc.domain
* @author:xujirong
* @dscription:角色
* @date:2022-09-25 12:09
* @version:1.0
*/
@Data
@Accessors(chain = true)
@AllArgsConstructor
@NoArgsConstructor
public class Role implements Serializable {
private String id;
private String name;
/*定义权限的集合*/
private List<Perms> perms;
}
package com.tjetc.domain;
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.experimental.Accessors;
import java.io.Serializable;
/**
- @belongProject:hibernate
- @belongPackage:com.tjetc.domain
- @author:xujirong
- @dscription:授权
- @date:2022-09-25 12:09
- @version:1.0
*/ @Data @Accessors(chain = true) @AllArgsConstructor @NoArgsConstructor public class Perms implements Serializable { private String id; private String name; private String url; }
10.接口注解Cacheable->Cacheable的详细介绍
package com.tjetc.annotion;
import org.springframework.core.annotation.AliasFor;
/**
* @belongProject:hibernate
* @belongPackage:com.tjetc.annotion
* @author:xujirong
* @dscription:Cacheable参数描述
* @date:2022-10-01 15:55
* @version:1.0
*/
public @interface Cacheable {
//value与cacheNames任选一个,但不可同时使用
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
//
// 可以指定多个缓存;
String key() default "";
//
//编写SpEL;
String keyGenerator() default "";
//
String cacheManager() default "";
//
String cacheResolver() default "";
//
String condition() default "";
//
// condition = "#id>0" condition = "#a0>1":第一个参数的值>1的时候才进行缓存
String unless() default "";
//
boolean sync() default false;
//
}
11.前端页面 index.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<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>
<shiro:hasRole name="user">
<li><a href="#">用户管理</a>
<ul>
<shiro:hasPermission name="user:save:*">
<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:hasRole>
<shiro:hasRole name="admin">
<li><a href="#">商品管理</a></li>
<li><a href="#">订单管理</a></li>
<li><a href="#">物流管理</a></li>
</shiro:hasRole>
</ul>
<a href="http://localhost:8088/user/logout">退出登录</a>
</body>
</html>
login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>登录界面</h1>
<form th:action="@{/user/login}" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="登录">
</form>
</body>
</html>
register.html
<!DOCTYPE html>
<html lang="en">
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>注册界面</h1>
<form th:action="@{/user/register}" method="post">
用户名:<input type="text" name="username" > <br/>
密码 : <input type="text" name="password"> <br>
<input type="submit" value="立即注册">
</form>
</body>
</html>
12.测试截图    注意:因为这里我使用了cache缓存,所以重复获取数据的时候,只查询了一次数据库
 
|