一、Shiro
概念
Apache Shiro 是 Java 的一个安全(权限)框架,可以非常容易的开发出足够好的应用,其不仅可以在Java SE环境,也可以用在Java EE环境
Shiro可以完成,认证、授权、加密、会话管理,Web集成,缓存等
源码下载地址:http://shiro.apache.org/
1.1 功能介绍
-
Authentication:身份认证、登录,验证用户是不是拥有相应的身份
-
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么提作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限
-
SessionManager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中会话可以是普通的JavaSE环境,也可以是Web环境
-
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储
-
WebSupport:Web支持,可以非常容易的集成到Web环境
-
Caching:缓存,比如用户登录后,其用户信息,拥有的角色,权限不必每次去查,这样可以提高效率
-
Concurrency:Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
-
Testing:提供测试支持
-
Run As:允许一个用户假装为另一个用户(如要他们允许)的身份进行访问
-
Rememner Me:记任我,这个是非能常见的功能,即一次登录后,下次再来的话不用登录了
1.2 Shiro 架构
外部
-
subject:应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外API核心就是 Subject,Subject 代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给 SecurityManager;Subject 其实是一个门面,SecurityManager 才是实际的执行者
-
SecurityManager:安全管理器,即所有与安全有关的操作都会与 SercurityManager 交互,并且它管理着所有的 Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC 的 DispatcherServlet 的角色
-
Realm:Shiro 从 Realm 获取安全数据(如用户角色权限),就是说 SercurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从 Realm 得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把 Realm 看成 DataSource
内部
-
Subject:任何可以与应用交互的用户
-
SecurityManager:相当于SpringMVC中的DispatcherServiet;是Shiro的心脏,所有具体的交互都通过 SecurityManager进行控制,它管理者所有的Subject,且负责进行认证,授权,会话,及缓存的管理。
-
Authenticator:负责Subject认证,是一个扩展点,可以自定义实现:可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了:
-
Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作:即控制着用户能访问应用中的那些功能:
-
Realm:可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用jDBC实现,也可以是内存实现等等,由用户提供;所以一般在应用中都需要实现自己的realm
-
SessionManager:管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的 JavaSE环境中
-
CacheManager:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能:
-
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于密码加密,解密等
1.3 10分钟快速了解
官方版本地址:https://shiro.apache.org/release-archive.html#180
# 克隆到本地
git clone https://github.com/apache/shiro.git
- 打开该项目工程
- 打开Quickstart.class
// 获取当前的用户对象 Subjcet
Subject currentUser = SecurityUtils.getSubject();
// 通过当前用户拿到Session,不是HttpSession
Session session = currentUser.getSession();
// 判断当前的用户是否被认证
if (!currentUser.isAuthenticated()) {
// Token: 令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true); // 设置记住我
try {
currentUser.login(token); // 执行登录操作
} catch (UnknownAccountException uae) { // 用户名不存在
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) { // 密码不正确
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) { // 该账户已被冻结
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
// 获取当前用户的认证
currentUser.getPrincipal()
// 判断用户有什么角色
currentUser.hasRole("schwartz")
// 获得角色的权限
currentUser.isPermitted("lightsaber:wield")
// 注销
currentUser.logout();
了解了快速上手之后,我们来使用SpringBoot整合Shiro
二、SpringBoot整合Shiro(Mybatis-Plus版)
一、基础配置
后端配置
- 导入依赖pom.xml
<!--springboot使用2.3.7.RELEASE版本-->
<dependencies>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.8.0</version>
</dependency>
<!--thymeleaf与shiro整合-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<!--Swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!--自动生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.2</version>
</dependency>
<!--velocity模板引擎-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<!--druid数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</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>
</dependencies>
- spring配置文件application.yml
# 端口号
server:
port: 3035
# 应用名称
spring:
application:
name: shiro
# 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
url: jdbc:mysql://localhost:3306/test?useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
# 初始化创建initialSize个连接
initialSize: 10
# 最小连接池数量,如果空闲的连接数大于该值,则关闭多余的连接,反之则创建更多的连接以满足最小连接数要求
min-idle: 10
# 如果当前池中正在使用的连接池数等于maxActive,则会等待一段时间,等待其他操作释放掉某一个连接
max-active: 100
# 如果这个等待时间超过了maxWait,则会报错(单位: 毫秒)
max-wait: 60000
# 如果当前连接池中某个连接在空闲了timeBetweenEvictionRunsMillis时间后仍然没有使用,则被物理性的关闭掉(单位: 毫秒)
time-between-eviction-runs-millis: 60000
# 连接的最小生存时间,连接保持空闲而不被驱逐的最小时间(单位: 毫秒)
minEvictableIdleTimeMillis: 300000
# 验证数据库服务可用性的sql,用来检测连接是否有效的sql,因数据库方言而差,例如 oracle 应该写成 SELECT 1 FROM DUAL
validationQuery: SELECT 1 FROM DUAL
# 申请连接时检测空闲时间,根据空闲时间再检测连接是否有效,建议配置为true,不影响性能,并且保证安全性. 申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRun,就测试连接是否有效,不会直接剔除;如果空闲时间超过了minEvictableIdleTimeMillis则会直接剔除
testWhileIdle: true
# 申请连接时直接检测连接是否有效.申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnBorrow: false
# 归还连接时检测连接是否有效.归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnReturn: false
# 开启PSCache
# poolPreparedStatements: true
# 连接出错后再尝试连接三次
# connectionErrorRetryAttempts: 3
# 数据库服务宕机自动重连机制
# breakAfterAcquireFailure: true
# 连接出错后重试时间间隔
# timeBetweenConnectErrorMillis: 300000
# 异步初始化策略
# asyncInit: true
# 是否自动回收超时连接
# remove-abandoned: true
# 超时时间(以秒数为单位) 超过此值后,druid将强制回收该连接
# remove-abandoned-timeout: 1800
# 事务超时时间
# transaction-query-timeout: 6000
# 配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
# 如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
# 则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# redis
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
timeout: 0
# 配置mybatis-plus
mybatis-plus:
type-aliases-package: com.vinjcent.pojo
mapper-locations: classpath*:com/vinjcent/mapper/**/*.xml
configuration:
# 开启缓存
cache-enabled: true
# 开启驼峰命名
map-underscore-to-camel-case: true
# 开启日志监控
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
- 实体类、持久层、业务逻辑层
1)实体类
User.class
package com.vinjcent.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
*
* </p>
*
* @author vinjcent
* @since 2022-07-24 00:22:56
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
@TableName("user")
@ApiModel(value = "User对象", description = "用户表")
public class User implements Serializable {
@ApiModelProperty("用户id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty("姓名")
@TableField("`name`")
private String name;
@ApiModelProperty("用户名")
@TableField("username")
private String username;
@ApiModelProperty("密码")
@TableField("`password`")
private String password;
@ApiModelProperty("盐")
@TableField("salt")
private String salt;
@ApiModelProperty("状态,0为冻结,1为正常")
@TableField("state")
private Integer state;
}
Role.class
package com.vinjcent.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
*
* </p>
*
* @author vinjcent
* @since 2022-07-24 00:22:56
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
@TableName("role")
@ApiModel(value = "Role对象", description = "角色表")
public class Role implements Serializable {
@ApiModelProperty("id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty("角色名")
@TableField("`name`")
private String name;
}
Permission.class
package com.vinjcent.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
/**
* <p>
*
* </p>
*
* @author vinjcent
* @since 2022-07-24 00:22:56
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
@Accessors(chain = true)
@TableName("permission")
@ApiModel(value = "Permission对象", description = "权限表")
public class Permission implements Serializable {
@ApiModelProperty("id")
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@ApiModelProperty("权限名")
@TableField("`name`")
private String name;
}
【注】还有另外两个实体类无需用到,但可以作为参考去修改某一条记录!
2)持久层
对应映射文件(XML)…
3)业务逻辑层
实现接口类…
由于设计的数据库关系表有点多,所以所有与数据库操作的类都上传到gitee上,这里就不再为大家详细描述,大多数代码都有注释,大家可以参考一下《springboot整合shiro安全框架》
- 编写自定义 Realm 以及 ShiroConfiguration 配置类
1)UserRealm.class
package com.vinjcent.config;
import com.vinjcent.pojo.Permission;
import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.UserService;
import org.apache.shiro.SecurityUtils;
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.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.util.List;
public class UserRealm extends AuthorizingRealm {
/*
解决 xxx is not eligible for getting processed by all BeanPostProcessors
(for example: not eligible for auto-proxying 问题
*/
@Lazy // 由于某一个service实现了BeanPostProcessor接口,同时这个Service又依赖其他的Service导致的
@Autowired
UserService userService;
// 执行授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("执行了授权doGetAuthorizationInfo");
// 获得当前subject
Subject subject = SecurityUtils.getSubject();
// 获得当前的principal,也就是认证完后放入当前对象的信息
User currentUser = (User) subject.getPrincipal();
// 认证之后,如果前端shiro标签中有出现需要权限的标签,或者过滤器中某个链接需要权限,就会进行认证
// 根据主身份信息获取"角色"和"权限"信息
List<Role> roles = userService.queryRolesByUserId(currentUser.getId());
if (!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(viewRole -> {
simpleAuthorizationInfo.addRole(viewRole.getName());
List<Permission> perms = userService.queryPermsByRoleId(viewRole.getId());
if (!CollectionUtils.isEmpty(perms)){
/*
添加权限
注意!如果数据库没有为该用户设置权限,该字段为null,会报错,需要手动添加一个
*/
perms.forEach(viewPerm -> simpleAuthorizationInfo.addStringPermission(viewPerm.getName()));
}
});
return simpleAuthorizationInfo;
}
return null;
}
// 执行认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证doGetAuthenticationInfo");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库中查询该用户
User user = userService.queryOneByUsername(token.getUsername());
if (user != null){
// 如果身份认证验证成功,返回一个AuthenticationInfo实现
// 第一个参数为principal;
// 第二个参数为从数据库中查出的用于验证的密码,shiro中密码验证不需要我们自己去做;
// 第三个参数为realm的名称
return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName());
}
// 如果不存在该用户,返回null,会抛出UnknownAccountException异常
return null;
}
}
ShiroConfiguration.class
package com.vinjcent.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
// 1.创建shiroFilter 负责拦截请求
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(
@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 让管理者所管理的用户过滤生效
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 配置shiro的内置过滤器
/*
* anon: 无需认证即可访问
* authc: 必须认证才能用
* user: 必须拥有 “记住我” 功能才能用
* perms: 拥有对某个资源的权限才能用
* role: 拥有某个角色权限才能访问
*/
// 由于过滤器链是一个链结构,使用linkedHashMap比较合适
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/login","anon");
filterMap.put("/","anon");
// 登陆后授权,正常情况下没有授权会跳转到未授权页面
// 登陆后授权,正常情况下没有授权会跳转到未授权页面
filterMap.put("/toAdd","perms[addPerm]");
filterMap.put("/toDelete","perms[deletePerm]");
filterMap.put("/toQuery","perms[queryPerm]");
filterMap.put("/toUpdate","perms[updatePerm]");
// 设置注销过滤器
filterMap.put("/logout","logout");
// 如果没有认证,拦截所有请求
filterMap.put("/**", "authc");
/*
* /** 匹配所有的路径
* 通过Map集合组成了一个拦截器链,自顶向下过滤,一旦匹配,则不再执行下面的过滤
* 如果下面的定义与上面冲突,那按照了谁先定义谁说了算
* 所以/** 一定要配置在最后
* 这里是否要对所有路径进行认证视情况而定,因为一些路由跳转可能在没登陆出现导致出错
*/
// 设置登录的请求
shiroFilterFactoryBean.setLoginUrl("/toLogin");
// 未授权跳转的页面
shiroFilterFactoryBean.setUnauthorizedUrl("/noauth");
// 设置shiro过滤器链
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
// 2.web安全管理者(这里的使用DefaultWebSecurityManager,该类继承了DefaultSecurityManager)
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(
@Qualifier("userRealm") UserRealm userRealm,
@Qualifier("webSessionManager") DefaultWebSessionManager webSessionManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 让管理者管理userRealm
securityManager.setRealm(userRealm);
// 配置webSessionManager
securityManager.setSessionManager(webSessionManager);
return securityManager;
}
// 3.配置Realm对象
@Bean(name = "userRealm")
public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher credentialsMatcher){
UserRealm userRealm = new UserRealm();
// 给realm配置凭证校验匹配器
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
// ShiroDialect: 用来整合前端标签 shiro-thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
// 配置webSessionManager
@Bean(name="webSessionManager")
public DefaultWebSessionManager webSessionManager(){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 将sessionIdUrlRewritingEnabled属性设置成false,防止第一次登出出现sessionId问题,登出失败(如下图)
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
}
- 控制层
ShiroController.class
package com.vinjcent.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ShiroController {
// 跳到登录页
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@RequestMapping("/toAdd")
public String toAdd(){
return "add";
}
@RequestMapping("/toDelete")
public String toDelete(){
return "delete";
}
@RequestMapping("/toQuery")
public String toQuery(){
return "query";
}
@RequestMapping("/toUpdate")
public String toUpdate(){
return "update";
}
// 跳到未授权页面
@RequestMapping("/noauth")
public String tonoauth(){
return "noauth";
}
// 跳到首页
@RequestMapping({"/", "/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello shiro");
return "index";
}
// 用于测试记住我和认证的区别
@RequestMapping("/buy")
public String buy(){
Subject subject = SecurityUtils.getSubject();
// 只有认证后才能访问,如果只是记住我则需要先登录
if(!subject.isAuthenticated()){
return "redirect:/toLogin";
}
return "add";
}
// 登录认证
@RequestMapping("/login")
public String login(String username,String password,Integer rememberMe,Model model){
Subject subject = SecurityUtils.getSubject();
// 令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
if(rememberMe != null && rememberMe == 1){
// 设置记住我功能
token.setRememberMe(true);
}
try {
//登录认证
subject.login(token);
}catch (UnknownAccountException e){
model.addAttribute("msg","用户名不存在!");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误!");
return "login";
}catch (LockedAccountException e) {
model.addAttribute("msg","账户已锁定!");
return "login";
} catch (ExcessiveAttemptsException e) {
model.addAttribute("msg","用户名或密码错误次数过多!");
return "login";
} catch (AuthenticationException e) {
model.addAttribute("msg","用户名或密码不正确!");
return "login";
}
if(subject.isAuthenticated()){
System.out.println("认证成功");
Session session = subject.getSession();
// 将用户存入shiro里的session中
session.setAttribute("currentUser", subject.getPrincipal());
return "index";
}else{
token.clear();
return "login";
}
}
// 注销
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "index";
}
}
- 配置日志输出
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache=WARN
# Spring
log4j.logger.org.springframework=WARN
# Default Shiro logging
log4j.logger.org.apache.shiro=INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
前端配置
- index.html页面
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<h2 th:text="${msg}"></h2>
<div shiro:guest="">
<a th:href="@{/toLogin}">我是游客,需要登录</a>
</div>
<span shiro:notAuthenticated=""><hr>
没有认证时显示
</span>
<div shiro:hasPermission="addPerm">
<a th:href="@{/toAdd}">具有add权限才会显示</a>
</div>
<div shiro:hasPermission="deletePerm">
<a th:href="@{/toDelete}">具有delete权限才会显示</a>
</div>
<div shiro:hasPermission="queryPerm">
<a th:href="@{/toQuery}">具有query权限才会显示</a>
</div>
<div shiro:hasPermission="updatePerm">
<a th:href="@{/toUpdate}">具有update权限才会显示</a>
</div>
<div shiro:hasRole="user">
<p>是user角色才会显示</p>
</div>
<div shiro:hasRole="admin">
<p>是admin角色才会显示</p>
</div>
<div shiro:user>
<a th:href="@{/buy}">记住我能看到,但前提是认证了才能用</a>
</div>
<div shiro:user="true">
记住我或认证都能看到
</div>
<div shiro:user>
<a th:href="@{/logout}">注销</a>
</div>
</body>
</html>
- login.html页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<form action="/login" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="checkbox" name="rememberMe" value="1">记住我<br>
<input type="submit">
</form>
</body>
</html>
- 其它页面,add.html、delete.html、query.html、noauth.html、update.html页面随意添加内容,能够确认两者之间内容不同即可
Shiro与Thymeleaf整合标签拓展
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/>
<shiro:principal property="username"/>
相当于((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体内容
运行结果
二、记住我功能
记住我功能是要在用户登录成功以后,假如关闭浏览器,下次再访问系统资源时,无需再执行登录操作
- 在ShiroConfiguration.class添加CookieRememberMeManager这个bean
// 记住我配置
@Bean(name = "cookieRememberMeManager")
public CookieRememberMeManager rememberMeManager(){
// cookie管理器
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
// cookie的名字
SimpleCookie simpleCookie = new SimpleCookie("shiro-rememberMe");
// 设置有效期时间30天(单位秒)
simpleCookie.setMaxAge(2592000);
cookieRememberMeManager.setCookie(simpleCookie);
// rememberMe cookie加密的密钥,建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
// cookieRememberMeManager.setCipherKey用来设置加密的Key,参数类型byte[],字节数组长度要求16
cookieRememberMeManager.setCipherKey("vinjcent30_ovo35".getBytes());
// 也可以使用cookieRememberMeManager.setCipherKey(Base64.decode("16位"));
return cookieRememberMeManager;
}
这里设置cookie加密的密钥是为了给它指定一种加密方式,同时也为了防止关闭浏览器后再打开,记住我功能失效的问题,因为如果不设置的话,两次加密后数据可能出现不一致,就像拿密钥b去匹配上次的密钥a,会导致cookie失效
当然设置也有一部分是为了加强的cookie的复杂性,不易破解
- 在安全管理者DefaultWebSecurityManager这个bean配置记住我CookieRememberMeManager
// web安全管理者
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(
@Qualifier("userRealm") UserRealm userRealm,
@Qualifier("webSessionManager") DefaultWebSessionManager webSessionManager,
@Qualifier("cookieRememberMeManager") CookieRememberMeManager rememberMeManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 让管理者管理userRealm
securityManager.setRealm(userRealm);
// 配置webSessionManager
securityManager.setSessionManager(webSessionManager);
// 关联rememberMe
securityManager.setRememberMeManager(rememberMeManager);
return securityManager;
}
- 重启应用,进行登录
登陆之前
在登陆之前,可以在浏览器按F12
对应的Cookies查看
登录之后
能够看到,在我们的浏览器中,已经保存了我们的用户信息,通过后端设置加密之后值的显示,个人隐私数据进一步提高了安全性
【注】检验关闭浏览器,重新访问之后是否已经进入主页面
三、加盐加密
3.1 加密概述
在一个系统中我们通常会将用户名和密码存在数据库中,如果我们直接将原密码存入数据库,会存在很大的安全隐患,比如:数据库被盗,传输过程中被黑客拦截,这都是很常见的问题,数据库所在的服务并不是绝对的安全,传输过程中也有可能被黑客拦截得到你传输的数据获得一些隐私的数据,并篡改后发往服务器
我们要如何解决这类问题呢?
这里我们就可以通过Shiro去实现我们的加密
当我们调用 Subject 的 login 方法时,shiro 会调用 SecurityManager 安全管理者,尝试对比密码并登陆,而我们要做的就是自定义 Realm,我们的角色、用户、权限都是从Realm得到的数据,也就是说 SecurityManager 认证就一定会走 Realm 取用户的一些信息,可以把它理解为数据源,如下图调试过程
3.2 认证过程(debug)
在调用login方法时,会进入以下这个方法
而上面这个方法又获取了我们自定义的Realm对象
最终又来到了我们自定义UserRealm.class重写的认证方法
接着就是断言密码匹配
进去之后就是对比数据库存储的密码与我们输入原密码加密之后的匹配操作
可以从上面看到,对原密码的加密操作其实就是,通过 shiro 包下的SimpleHash(加密名称,原密码,盐值,hash迭代次数)这个函数加密原密码得到的密文,这个密文将与存储在数据库中用户密码字段的值是一致的!
接下来就是配置我们的盐值加密
3.3 配置加密
- 在ShiroConfiguration.class添加HashedCredentialsMatcher.class这个bean
// 密码匹配凭证管理器
@Bean(name = "hashedCredentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置加密算法为md5
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 设置散列次数
hashedCredentialsMatcher.setHashIterations(1024);
// storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码: false时用Base64编码
hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
return hashedCredentialsMatcher;
}
- 在Realm.class这个bean当中配置HashedCredentialsMatcher匹配凭证管理器
// 3.配置Realm对象
@Bean(name = "userRealm")
public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher credentialsMatcher){
UserRealm userRealm = new UserRealm();
// 给realm配置凭证校验匹配器
userRealm.setCredentialsMatcher(credentialsMatcher);
return userRealm;
}
- 在Realm.class类修改认证方法
package com.vinjcent.config;
import com.vinjcent.pojo.Permission;
import com.vinjcent.pojo.Role;
import com.vinjcent.pojo.User;
import com.vinjcent.service.UserService;
import org.apache.shiro.SecurityUtils;
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.subject.Subject;
import org.apache.shiro.util.ByteSource;
import org.apache.shiro.util.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import java.util.List;
public class UserRealm extends AuthorizingRealm {
/*
解决 xxx is not eligible for getting processed by all BeanPostProcessors
(for example: not eligible for auto-proxying 问题
*/
@Lazy // 由于某一个service实现了BeanPostProcessor接口,同时这个Service又依赖其他的Service导致的
@Autowired
UserService userService;
// 执行授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("执行了授权doGetAuthorizationInfo");
// 获得当前subject
Subject subject = SecurityUtils.getSubject();
// 获得当前的principal,也就是认证完后放入当前对象的信息
User currentUser = (User) subject.getPrincipal();
// 认证之后,如果前端shiro标签中有出现需要权限的标签,或者过滤器中某个链接需要权限,就会进行认证
// 根据主身份信息获取"角色"和"权限"信息
List<Role> roles = userService.queryRolesByUserId(currentUser.getId());
if (!CollectionUtils.isEmpty(roles)){
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
roles.forEach(viewRole -> {
simpleAuthorizationInfo.addRole(viewRole.getName());
List<Permission> perms = userService.queryPermsByRoleId(viewRole.getId());
if (!CollectionUtils.isEmpty(perms)){
/*
添加权限
注意!如果数据库没有为该用户设置权限,该字段为null,会报错,需要手动添加一个
*/
perms.forEach(viewPerm -> {
simpleAuthorizationInfo.addStringPermission(viewPerm.getName());
});
}
});
return simpleAuthorizationInfo;
}
return null;
}
// 执行认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证doGetAuthenticationInfo");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库中查询该用户
User user = userService.queryOneByUsername(token.getUsername());
if (user != null){
// 如果身份认证验证成功,返回一个AuthenticationInfo实现
// 第一个参数为principal;
// 第二个参数为从数据库中查出的用于验证的密码,shiro中密码验证不需要我们自己去做;
// 第三个参数为盐值,根据盐值与输入的原密码进行加密之后,再与当前user.getPassword()进行匹配
// 第四个参数为realm的名称
return new SimpleAuthenticationInfo(user,user.getPassword(), ByteSource.Util.bytes(user.getUsername()+user.getSalt()), this.getName());
}
// 如果不存在该用户,返回null,会抛出UnknownAccountException异常
return null;
}
}
问题:如何注册一个用户并且与检验密码?
上面的登录过程,也就是认证过程的调试,可以很清晰的知道,密码的校验是如何进行的,下面就来展示一个用户注册的测试用例
- 先查看数据库的数据
user表
没有任何的数据
role表
有两个身份记录,一个是user(普通用户),一个是admin(管理员)
permission表
有着增删查改的权限记录
viewperm表
,记住!什么样的身份,就有什么样的权限,用来记录身份不同—权限不同
- 注册一个用户,在springboot测试类中运行
先了解SimpleHash()这个函数有什么参数
测试用例
@Test
void test01(){
/*
测试用例
加密名称"md5" 大写小写都没有关系,但必须是加密规定的名称,不可自定义!
username = vinjcent
password = 123456
salt = username + time
散列次数1024
md5Pwd为加密之后的密码,也就是存入数据库的密码
*/
String username = "vinjcent"; // 原用户名
String password = "123456"; // 原密码
String currentTime = String.valueOf(System.currentTimeMillis()); // 将当前时间作为盐值一部分,真正的salt是username+currentTime,这里可以自定义
// 根据SimpleHash()得到加密字符后,转为Hex编码字符串,也可以使用Base64字符串
String md5Pwd = new SimpleHash("MD5", password,
ByteSource.Util.bytes(username + currentTime), 1024).toHex();
//String md5Pwd = new SimpleHash("MD5", password, username + currentTime, 1024).toBase64();
userService.save(new User(null, "test", username, md5Pwd, currentTime,null));
}
【注】以下这两个地方,对于加密后不同形式的编码格式(Hex或Base64)需要一一对应,以及散列次数要一致
查看数据库user表
由于该用户还没有身份,需要在数据库手动添加一个身份给他,这里我让这个用户的身份为管理员(admin)
启动项目,测试登录
登陆成功!
四、开启缓存
缓存是提供性能的重要手段。缓存适合那些经常不变动的数据,比如系统中用户的信息和权限不会经常改变,特别适合缓存起来供下次使用。这样减少了系统查询数据库的次数,提升了性能。shiro自身不实现缓存,而是提供缓存接口,让其他第三方实现,经常使用ehcache、redis缓存
在没有配置缓存的时候,会存在这样的问题。每发起一个请求,就会调用一次授权方法。用户基数大请求多的时候,会对数据库造成很大的压力。所以我们需要配置缓存,将用户信息放在缓存里,从而减小数据库压力
开启缓存前
每次访问需要授权的页面都会执行一次授权,因为授权的信息是从数据库查询得到,这就说明在访问的同时也给数据库也造成了一定时间的开销
开启缓存后
用户认证完之后,每次访问授权的页面,不再进行授权操作;说明开启了缓存,只授权一次之后用户无需再次授权
实现缓存有三种方式
- shiro自带的 MemoryConstrainedCacheManager,适用于本机,不适用于集群
- ehcache(推荐)
- redis
1)ehcache缓存
- 导入依赖pom.xml
<!--shiro缓存,添加ehcache-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.8.0</version>
</dependency>
<!--log4j-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- 在
resources
下面新建config文件夹,并创建ehcache-shiro.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
<!--
缓存对象存放路径
java.io.tmpdir: 默认的临时文件存放路径
user.home: 用户的主目录
user.dir: 用户的当前工作目录,即当前程序所对应的工作路径
其它通过命令行指定的系统属性,如“java –DdiskStore.path=D:\\abc ……”
-->
<diskStore path="java.io.tmpdir"/>
<!--
name:缓存名称。
maxElementsOnDisk: 硬盘最大缓存个数。0表示不限制
maxEntriesLocalHeap: 指定允许在内存中存放元素的最大数量,0表示不限制
maxBytesLocalDisk: 指定当前缓存能够使用的硬盘的最大字节数,其值可以是数字加单位,单位可以是K、M或者G,不区分大小写,
如: 30G。当在CacheManager级别指定了该属性后,Cache级别也可以用百分比来表示,
如: 60%,表示最多使用CacheManager级别指定硬盘容量的60%。该属性也可以在运行期指定。当指定了该属性后会隐式的使当前Cache的overflowToDisk为true。
maxEntriesInCache: 指定缓存中允许存放元素的最大数量。这个属性也可以在运行期动态修改。但是这个属性只对Terracotta分布式缓存有用
maxBytesLocalHeap: 指定当前缓存能够使用的堆内存的最大字节数,其值的设置规则跟maxBytesLocalDisk是一样的
maxBytesLocalOffHeap: 指定当前Cache允许使用的非堆内存的最大字节数。当指定了该属性后,会使当前Cache的overflowToOffHeap的值变为true,
如果我们需要关闭overflowToOffHeap,那么我们需要显示的指定overflowToOffHeap的值为false
overflowToDisk:boolean类型,默认为false。当内存里面的缓存已经达到预设的上限时是否允许将按驱除策略驱除的元素保存在硬盘上,默认是LRU(最近最少使用)
当指定为false的时候表示缓存信息不会保存到磁盘上,只会保存在内存中
该属性现在已经废弃,推荐使用cache元素的子元素persistence来代替,如: <persistence strategy=”localTempSwap”/>
diskSpoolBufferSizeMB: 当往磁盘上写入缓存信息时缓冲区的大小,单位是MB,默认是30
overflowToOffHeap: boolean类型,默认为false。表示是否允许Cache使用非堆内存进行存储,非堆内存是不受Java GC影响的。该属性只对企业版Ehcache有用
copyOnRead: 当指定该属性为true时,我们在从Cache中读数据时取到的是Cache中对应元素的一个copy副本,而不是对应的一个引用。默认为false
copyOnWrite: 当指定该属性为true时,我们在往Cache中写入数据时用的是原对象的一个copy副本,而不是对应的一个引用。默认为false
timeToIdleSeconds: 单位是秒,表示一个元素所允许闲置的最大时间,也就是说一个元素在不被请求的情况下允许在缓存中待的最大时间。默认是0,表示不限制
timeToLiveSeconds: 单位是秒,表示无论一个元素闲置与否,其允许在Cache中存在的最大时间。默认是0,表示不限制
eternal: boolean类型,表示是否永恒,默认为false。如果设为true,将忽略timeToIdleSeconds和timeToLiveSeconds,Cache内的元素永远都不会过期,也就不会因为元素的过期而被清除了
diskExpiryThreadIntervalSeconds: 单位是秒,表示多久检查元素是否过期的线程多久运行一次,默认是120秒
clearOnFlush: boolean类型。表示在调用Cache的flush方法时是否要清空MemoryStore。默认为true
diskPersistent: 是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
maxElementsInMemory: 缓存最大数目
memoryStoreEvictionPolicy: 当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)
memoryStoreEvictionPolicy:
Ehcache的三种清空策略;
FIFO,first in first out: 先进先出
LFU, Less Frequently Used: 就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存
LRU,Least Recently Used: 最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存
-->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!-- 授权缓存 -->
<cache name="authorizationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
<!-- 认证缓存 -->
<cache name="authenticationCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="0"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
- 在ShiroConfiguration.class中添加如下bean,并绑定配置文件
// shiro自带的MemoryConstrainedCacheManager作缓存
// 但是只能用于本机,在集群时就无法使用,如果集群需要使用ehcache
@Bean(name = "cacheManager")
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager(); // 使用内存缓存
}
// 配置ehcache,推荐使用
@Bean(name = "ehCacheManager")
public EhCacheManager ehCacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
// 绑定缓存配置文件
ehCacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return ehCacheManager;
}
- 在DefaultWebSecurityManager bean下添加ehCacheManager缓存管理
// 2.web安全管理者
@Bean(name = "defaultWebSecurityManager")
public DefaultWebSecurityManager defaultWebSecurityManager(
@Qualifier("userRealm") UserRealm userRealm,
@Qualifier("webSessionManager") DefaultWebSessionManager webSessionManager,
@Qualifier("cookieRememberMeManager") CookieRememberMeManager rememberMeManager,
@Qualifier("ehCacheManager") EhCacheManager ehCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 让管理者管理userRealm
securityManager.setRealm(userRealm);
// 配置webSessionManager
securityManager.setSessionManager(webSessionManager);
// 关联rememberMe
securityManager.setRememberMeManager(rememberMeManager);
// shiro自带的缓存(不推荐使用)
// securityManager.setCacheManager(cacheManager);
// ehcache缓存(推荐)
securityManager.setCacheManager(ehCacheManager);
return securityManager;
}
- 在UserRealm bean下设置开启缓存,
// 3.配置Realm对象
@Bean(name = "userRealm")
public UserRealm userRealm(@Qualifier("hashedCredentialsMatcher") HashedCredentialsMatcher credentialsMatcher){
UserRealm userRealm = new UserRealm();
// 给realm配置凭证校验匹配器
userRealm.setCredentialsMatcher(credentialsMatcher);
// 开启缓存
userRealm.setCachingEnabled(true);
// 启用身份认证缓存,即缓存AuthenticationInfo信息,默认false
userRealm.setAuthenticationCachingEnabled(true);
// 设置AuthenticationInfo信息的缓存名称,在ehcache-shiro.xml文件中有对应缓存的配置
userRealm.setAuthenticationCacheName("authenticationCache");
// 启用身份授权缓存,即缓存AuthorizationInfo信息,默认false
userRealm.setAuthorizationCachingEnabled(true);
// 设置AuthorizationInfo信息的缓存名称,在ehcache-shiro.xml文件中有对应缓存的配置
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
有了缓存,如果需要清除缓存怎么办?
- 在UserRealm.class类下重写和定义缓存方法,如下所示
package com.vinjcent.config;
import com.vinjcent.pojo.User;
import com.vinjcent.service.UserService;
import org.apache.shiro.SecurityUtils;
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.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
public class UserRealm extends AuthorizingRealm {
@Lazy // 由于某一个service实现了BeanPostProcessor接口,同时这个Service又依赖其他的Service导致的
@Autowired
UserService userService;
// 执行授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("执行了授权doGetAuthorizationInfo");
//......
return simpleAuthorizationInfo;
}
// 执行认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证doGetAuthenticationInfo");
//......
return new SimpleAuthenticationInfo(user,user.getPassword(),"");
}
// ======================================重写和添加以下的方法======================================
/**
* 重写方法,清除当前用户的 "授权" 缓存
* @param principals 当前用户
*/
@Override
public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
super.clearCachedAuthorizationInfo(principals);
}
/**
* 重写方法,清除当前用户的 "认证" 缓存
* @param principals 当前用户
*/
@Override
public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
super.clearCachedAuthenticationInfo(principals);
}
/**
* 重写方法,清除当前用户的缓存
* @param principals 当前用户
*/
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
/**
* 自定义方法: 清除所有 "授权" 缓存
* 需要手动调用!
*/
public void clearAllCachedAuthorizationInfo() {
getAuthorizationCache().clear();
}
/**
* 自定义方法:清除所有 "认证" 缓存
* 需要手动调用!
*/
public void clearAllCachedAuthenticationInfo() {
getAuthenticationCache().clear();
}
/**
* 自定义方法:清除所有的 "认证" 缓存和 "授权" 缓存
* 需要手动调用!
*/
public void clearAllCache() {
clearAllCachedAuthenticationInfo();
clearAllCachedAuthorizationInfo();
}
}
2)redis缓存
- 导入依赖pom.xml
<!-- shiro + redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
- 修改application.yml配置文件
### 添加以下即可
spring:
# redis
redis:
host: localhost
port: 6379
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1
timeout: 0
- 修改ShiroConfiguration.class配置类,添加如下bean即可,无需重写
// 配置Redis缓存管理者
@Bean(name = "redisCacheManager")
public RedisCacheManager redisCacheManager(@Qualifier("redisManager") RedisManager redisManager) {
RedisCacheManager redisCacheManager = new RedisCacheManager();
// 设置redisManager属性
redisCacheManager.setRedisManager(redisManager);
// 用户权限信息缓存时间(单位:秒)
redisCacheManager.setExpire(200000);
return redisCacheManager;
}
// 配置redisSessionDAO
@Bean(name = "redisSessionDAO")
public RedisSessionDAO redisSessionDAO(@Qualifier("redisManager") RedisManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
// 设置redisManager属性
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
// 配置redis管理者
@Bean(name = "redisManager")
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
return redisManager;
}
- 修改DefaultWebSessionManager这个bean,配置SessionManager
// 配置webSessionManager
@Bean(name="webSessionManager")
public DefaultWebSessionManager webSessionManager(@Qualifier("redisSessionDAO") RedisSessionDAO redisSessionDAO){
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 将sessionIdUrlRewritingEnabled属性设置成false
sessionManager.setSessionIdUrlRewritingEnabled(false);
// 配置redisSessionDao(添加地方)
sessionManager.setSessionDAO(redisSessionDAO);
return sessionManager;
}
- 确保UserRealm bean已开启缓存
// realm对象
@Bean(name="userRealm")
public UserRealm userRealm(){
UserRealm userRealm = new UserRealm();
// 开启缓存(这里!)
userRealm.setCachingEnabled(true);
// 启用身份验证缓存,即缓存AuthenticationInfo信息,默认false
userRealm.setAuthenticationCachingEnabled(true);
// 设置认证缓存名
userRealm.setAuthenticationCacheName("authenticationCache");
// 启用授权缓存,即缓存AuthorizationInfo信息,默认false
userRealm.setAuthorizationCachingEnabled(true);
// 设置授权缓存名
userRealm.setAuthorizationCacheName("authorizationCache");
return userRealm;
}
- 在DefaultWebSecurityManager bean当中设置redis缓存管理以及session缓存管理
// web安全管理者
@Bean(name="webSecurityManager")
public DefaultWebSecurityManager webSecurityManager(
@Qualifier("userRealm") UserRealm userRealm,
@Qualifier("rememberMeManager") CookieRememberMeManager rememberMeManager,
@Qualifier("redisCacheManager") RedisCacheManager redisCacheManager,
@Qualifier("webSessionManager") DefaultWebSessionManager webSessionManager){
// @Qualifier("ehCacheManager") EhCacheManager ehCacheManager){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联UserRealm
securityManager.setRealm(userRealm);
// 关联rememberMe
securityManager.setRememberMeManager(rememberMeManager);
// 关联webSession
securityManager.setSessionManager(webSessionManager);
// redis缓存
securityManager.setCacheManager(redisCacheManager);
return securityManager;
}
- 启动该工程
报错一
java.lang.IllegalStateException: Error processing condition on org.apache.shiro.spring.boot.autoconfigure.ShiroBeanAutoConfiguration.eventBus
Caused by: java.lang.ClassNotFoundException: org.apache.shiro.event.EventBus
原因:maven工程中已经依赖了shiro-core1.8.0的版本,由于引进了shiro-redis3.1.0的整合jar包内部也依赖了一个shiro-core,但是版本是1.2.6的,与之前引入的shiro-core1.8.0的jar包忽略掉了
解决
排除掉shiro-redis包里的shiro-core依赖
<!-- shiro + redis缓存插件 -->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
</exclusion>
</exclusions>
</dependency>
报错二
org.apache.shiro.cache.CacheException: org.crazycake.shiro.exception.SerializationException: serialize error, object=User(id=18, name=test, username=vinjcent, password=60e2402e2056efaf6aa9919bf6461980, salt=1659159996673, state=1)
Caused by: java.io.NotSerializableException: org.apache.shiro.util.SimpleByteSource
描述我进行认证时,加密过程序列化失败
原因:之前使用的是简单凭证匹配器 SimpleCredentialsMathcer 只是简单的字符串比较,现在数据库密码进行加密后,就使用 HashedCredentialsMarcher(散列凭证匹配器) 用于密码验证,一系列配置完进行测试时于是就发生了这个错误
解决
- 检查自己的实体类是否序列化,如没有则加上(implements Serializable)如果还是出现相同问题,就说明实体类没有问题
- 需要自己创建一个加密源类并序列化
步骤:
在工程utils包下,新建MyByteSource.class
package com.vinjcent.utils;
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);
}
// 使用ByteSource接口进行构造函数
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);
}
}
}
修改UserRealm.class的认证函数doGetAuthenticationInfo()
// 执行认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证doGetAuthenticationInfo");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库中查询该用户
User user = userService.queryOneByUsername(token.getUsername());
if (user != null){
// 如果身份认证验证成功,返回一个AuthenticationInfo实现
// 第一个参数为principal;
// 第二个参数为从数据库中查出的用于验证的密码,shiro中密码验证不需要我们自己去做;
// 第三个参数为盐值,根据盐值与输入的原密码进行加密之后,再与当前user.getPassword()进行匹配
// 第四个参数为realm的名称
return new SimpleAuthenticationInfo(user,
user.getPassword(),
// ByteSource.Util.bytes(user.getUsername() + user.getSalt()),
// (修改的地方!)
new MyByteSource((user.getUsername() + user.getSalt()).getBytes()),
this.getName());
}
// 如果不存在该用户,返回null,会抛出UnknownAccountException异常
return null;
}
- 启动工程成功后,查看redis-cli
【注】不需要重写我们的RedisManager、RedisSessionDao、RedisCacheManager,因为我们导入的包shiro-redis已经帮我们将key-value中的key序列化为String了
现在有了缓存,认证之后关于权限的授权就只需要走一次方法,而不需要频繁的调用,提高了业务的性能