目录
(1)方式一:在resource中新建shiro-test-1.ini文件(注意后缀:ini)
(2)方式二:使用realm自定义的方式,通过代码从数据库中获取认证的用户数据 User user = (User)principalCollection.getPrimaryPrincipal();
(1)第一种是在ShiroConfiguration中使用filterMap.put(过滤器的方式配置目标地址的请求权限 )
5. 异常进行统一处理(handler.BaseExceptionHandler )
(3)session.CustomSessionManager
一、Shiro入门与基本配置
1. Shiro概述
Apache Shiro 是一个开源的安全框架,它提供了身份验证、授权、密码学和会话管理等功能。Shiro 的设计目标是简单易用,同时又不失灵活性和强大。它适用于各种类型的Java应用程序,包括传统的Java EE应用程序和基于Spring的应用程序,以及Spring Boot应用程序。
核心概念
Shiro 的核心概念包括:
- Subject:Shiro中的主体概念,代表当前进行认证或授权的用户。Subject 可以与用户一一对应,也可以一对多。
- SecurityManager:Shiro的核心接口,负责协调Shiro组件的工作。它管理所有Subject,并负责处理认证、授权、会话管理等。
- Realm:Shiro与数据源(例如数据库、LDAP或内存)交互的接口,负责获取和验证用户的认证信息和权限信息。
- Authentication:身份验证过程,用于确定Subject是否为“谁”。
- Authorization:授权过程,确定Subject是否有权限做“什么”。
2. Shiro依赖
记得导入Shiro的依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
3. 设置用户的角色和权限
(1)方式一:在resource中新建shiro-test-1.ini文件(注意后缀:ini)
用于模拟从数据库中查询用户
数据的格式:用户名=密码
[users]
zhangsan=123456
lisi=1234
给每个用户赋予角色,以及每个角色的权限
数据的格式:用户名=密码,角色名
角色=权限列表
[users]
zhangsan=123456,role1,role2
lisi=1234,role2
[roles]
role1=user:save,user:update
role2=user:find
(2)方式二:使用realm自定义的方式,通过代码从数据库中获取认证的用户数据
User user = (User)principalCollection.getPrimaryPrincipal();
二、Shiro入门使用
1. 基于ini的用户认证
这里的IniSecurityManagerFactory("classpath:shiro-test-1.ini")中的shiro-test-1.ini对应source中的shiro-test-1.ini
@Test
void testLogin() {
// 创建SecurityManager工厂,用于生成SecurityManager实例
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-1.ini");
// 通过工厂获取SecurityManager实例
SecurityManager securityManager = factory.getInstance();
// 设置当前线程的SecurityManager
SecurityUtils.setSecurityManager(securityManager);
// 获取Subject实例,Subject代表了用户的安全认证状态
Subject subject = SecurityUtils.getSubject();
// 定义用户名和密码
String username = "zhangsan";
String password = "123456";
// 创建UsernamePasswordToken,用于封装用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行登录方法,会触发Realm.doGetAuthenticationInfo的执行
subject.login(token);
// 打印用户是否认证
System.out.println(subject.isAuthenticated());
// 打印认证后的主体信息,通常为用户名
System.out.println(subject.getPrincipal());
}
2. 基于ini用户授予
@Test
void testLogin() {
// 创建SecurityManager工厂,用于生成SecurityManager实例,这里使用了Ini配置文件
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-1.ini");
// 通过工厂获取SecurityManager实例
SecurityManager securityManager = factory.getInstance();
// 设置当前线程的SecurityManager
SecurityUtils.setSecurityManager(securityManager);
// 获取Subject实例,Subject代表了用户的安全认证状态
Subject subject = SecurityUtils.getSubject();
// 定义用户名和密码
String username = "zhangsan";
String password = "123456";
// 创建UsernamePasswordToken,用于封装用户名和密码
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行登录方法,会触发Realm.doGetAuthenticationInfo的执行
subject.login(token);
// 检查用户是否拥有角色"role1"
System.out.println(subject.hasRole("role1"));
// 检查用户是否拥有权限"user:save"
System.out.println(subject.isPermitted("user:save"));
}
3. 自定义realm
在source中创建shiro-test-2.ini
[main]
#声明realm,路径记得修改
permRealm=com.example.shirodemo.shiro.PermissionRealm
#注册realm到securityManager中
securityManager.realm=$permRealm
realm.PermissionRealm文件信息:
public class PermissionRealm extends AuthorizingRealm {
// 设置当前realm的名称
public void setName(String name) {
super.setName("permissionRealm");
}
// 授权方法,用于定义用户的角色和权限
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 从PrincipalCollection中获取登录的用户名
String username = (String) principalCollection.getPrimaryPrincipal();
// 模拟用户的权限和角色信息,实际应用中这些数据通常来自于数据库
List<String> perms = new ArrayList<>(); // 用户权限列表
perms.add("user:save"); // 添加权限
perms.add("user:update"); // 添加权限
List<String> roles = new ArrayList<>(); // 用户角色列表
roles.add("role1"); // 添加角色
roles.add("role2"); // 添加角色
// 创建SimpleAuthorizationInfo实例,用于封装授权信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 为用户添加权限
info.addStringPermissions(perms);
// 为用户添加角色
info.addRoles(roles);
// 返回授权信息
return info;
}
// 认证方法,用于验证用户的身份
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 转换AuthenticationToken为UsernamePasswordToken,用于获取用户名和密码
UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
String username = upToken.getUsername(); // 获取用户名
String password = new String(upToken.getPassword()); // 获取密码
// 简单的认证逻辑,实际应用中通常需要查询数据库验证用户名和密码
if ("123456".equals(password)) {
// 创建SimpleAuthenticationInfo实例,用于封装认证信息
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, password, getName());
// 返回认证信息
return info;
}
// 如果认证失败,抛出异常
throw new RuntimeException("用户名或密码错误");
}
}
总结:
ini是通过在文件中表名用户和角色的信息;
而realm自定义的方式,是通过代码的方式表名用户和角色的信息
三、Shiro整合SpringBoot单体架构示例:
1. 引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
2. UserController:进行登录验证
@PostMapping("/login")
public String login(String username,String password) {
try{
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken uptoken = new
UsernamePasswordToken(username,password);
subject.login(uptoken);
return "登录成功";
}catch (Exception e) {
return "用户名或密码错误";
}
}
3. realm.CustomRealm
public class CustomRealm extends AuthorizingRealm {
@Override
public void setName(String name) {
super.setName("customRealm");
}
@Autowired
private UserService userService;
/**
* 构造授权方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection
principalCollection) {
//1.获取认证的用户数据
User user = (User)principalCollection.getPrimaryPrincipal();
//2.构造认证数据
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Set<Role> roles = user.getRoles();
for (Role role : roles) {
//添加角色信息
info.addRole(role.getName());
for (Permission permission:role.getPermissions()) {
//添加权限信息
info.addStringPermission(permission.getCode());
}
}
return info;
}
/**
* 认证方法
*/
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken
authenticationToken) throws AuthenticationException {
//1.获取登录的upToken
UsernamePasswordToken upToken = (UsernamePasswordToken)authenticationToken;
//2.获取输入的用户名密码
String username = upToken.getUsername();
String password = new String(upToken.getPassword());
//3.数据库查询用户
User user = userService.findByName(username);
//4.用户存在并且密码匹配存储用户数据
if(user != null && user.getPassword().equals(password)) {
return new
SimpleAuthenticationInfo(user,user.getPassword(),this.getName());
}else {
//返回null会抛出异常,表明用户不存在或密码不匹配
return null;
}
}
}
4. config.ShiroConfiguration
@Configuration
public class ShiroConfiguration {
//配置自定义的Realm
@Bean
public CustomRealm getRealm() {
return new CustomRealm();
}
//配置安全管理器
@Bean
public SecurityManager securityManager(CustomRealm realm) {
//使用默认的安全管理器
DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager(realm);
//将自定义的realm交给安全管理器统一调度管理
securityManager.setRealm(realm);
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
//1.创建shiro过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//3.通用配置(配置登录页面,登录成功页面,验证未成功页面)
filterFactory.setLoginUrl("/autherror?code=1"); //设置登录页面
filterFactory.setUnauthorizedUrl("/autherror?code=2"); //授权失败跳转页面
//4.配置过滤器集合
/**
* key :访问连接
* 支持通配符的形式
* value:过滤器类型
* shiro常用过滤器
* anno :匿名访问(表明此链接所有人可以访问)
* authc :认证后访问(表明此链接需登录认证成功之后可以访问)
*/
Map<String,String> filterMap = new LinkedHashMap<String,String>();
// 配置不会被拦截的链接 顺序判断
filterMap.put("/user/home", "anon");
filterMap.put("/user/**", "authc");
//5.设置过滤器
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
//配置shiro注解支持
@Bean
public AuthorizationAttributeSourceAdvisor
authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new
AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
有两种授权的方式:
(1)第一种是在ShiroConfiguration中使用filterMap.put(过滤器的方式配置目标地址的请求权限 )
示例:
//匿名访问(所有人员可以使用)
filterMap.put("/user/home", "anon");
//具有指定权限访问
filterMap.put("/user/find", "perms[user-find]");
//认证之后访问(登录之后可以访问)
filterMap.put("/user/**", "authc");
//具有指定角色可以访问
filterMap.put("/user/**", "roles[系统管理员]");
(2)第二种是在controller中使用注解的方式
@RequiresPermissions //配置到方法上,表明执行此方法必须具有指定的权限
@RequiresRoles //配置到方法上,表明执行此方法必须具有指定的角色
可以同时使用,也可以使用其中一个注解,示例:
@RequiresRoles(value = "系统管理员")
@RequiresPermissions(value = "user-find")
public String find() {
return "查询用户成功";
}
5. 异常进行统一处理(handler.BaseExceptionHandler )
@ControllerAdvice
public class BaseExceptionHandler {
@ExceptionHandler(value = AuthorizationException.class)
@ResponseBody
public String error(HttpServletRequest request, HttpServletResponse response,AuthorizationException e) {
return "未授权";
}
}
四、Shiro会话管理
1. 自定义会话管理
(1)引入依赖
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
(2)在application.yml中配置redis信息
redis:
host: 127.0.0.1
port: 6379
(3)session.CustomSessionManager
package com.example.shirodemo.session;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
public class CustomSessionManager extends DefaultWebSessionManager {
protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String id = httpServletRequest.getHeader("Authorization");
if (StringUtils.isEmpty(id)) {
// 如果找不到会话ID,则调用父类的 getSessionId 方法
return super.getSessionId(request, response);
} else {
// 设置会话ID来源为头部信息
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "header");
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
// 返回会话ID
return id;
}
}
}
2. 配置会话管理ShiroConfiguration
session.ShiroConfiguration:
@Configuration
public class ShiroConfiguration {
//1.创建realm
@Bean
public CustomRealm getRealm() {
return new CustomRealm();
}
//2.创建安全管理器
@Bean
public SecurityManager getSecurityManager(CustomRealm realm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(realm);
//将自定义的会话管理器注册到安全管理器中
securityManager.setSessionManager(sessionManager());
//将自定义的redis缓存管理器注册到安全管理器中
securityManager.setCacheManager(cacheManager());
return securityManager;
}
//3.配置shiro的过滤器工厂
/**
* 再web程序中,shiro进行权限控制全部是通过一组过滤器集合进行控制
*
*/
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
//1.创建过滤器工厂
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
//2.设置安全管理器
filterFactory.setSecurityManager(securityManager);
//3.通用配置(跳转登录页面,为授权跳转的页面)
filterFactory.setLoginUrl("/autherror?code=1");//跳转url地址
filterFactory.setUnauthorizedUrl("/autherror?code=2");//未授权的url
//4.设置过滤器集合
/**
* 设置所有的过滤器:有顺序map
* key = 拦截的url地址
* value = 过滤器类型
*
*/
Map<String,String> filterMap = new LinkedHashMap<>();
//filterMap.put("/user/home","anon");//当前请求地址可以匿名访问
//具有某中权限才能访问
//使用过滤器的形式配置请求地址的依赖权限
//filterMap.put("/user/home","perms[user-home]"); //不具备指定的权限,跳转到setUnauthorizedUrl地址
//使用过滤器的形式配置请求地址的依赖角色
//filterMap.put("/user/home","roles[系统管理员]");
filterMap.put("/user/**","authc");//当前请求地址必须认证之后可以访问
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
/**
* 1.redis的控制器,操作redis
*/
public RedisManager redisManager() {
RedisManager redisManager = new RedisManager();
redisManager.setHost(host);
redisManager.setPort(port);
return redisManager;
}
/**
* 2.sessionDao
*/
public RedisSessionDAO redisSessionDAO() {
RedisSessionDAO sessionDAO = new RedisSessionDAO();
sessionDAO.setRedisManager(redisManager());
return sessionDAO;
}
/**
* 3.会话管理器
*/
public DefaultWebSessionManager sessionManager() {
CustomSessionManager sessionManager = new CustomSessionManager();
sessionManager.setSessionDAO(redisSessionDAO());
return sessionManager;
}
/**
* 4.缓存管理器
*/
public RedisCacheManager cacheManager() {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager());
return redisCacheManager;
}
//开启对shior注解的支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
}
3. 在controller中进行登录校验
@PostMapping("/login")
public String login(String username,String password) {
//构造登录令牌
try {
/**
* 密码加密:
* shiro提供的md5加密
* Md5Hash:
* 参数一:加密的内容
* 111111 --- abcd
* 参数二:盐(加密的混淆字符串)(用户登录的用户名)
* 111111+混淆字符串
* 参数三:加密次数
*
*/
password = new Md5Hash(password,username,3).toString();
UsernamePasswordToken upToken = new UsernamePasswordToken(username,password);
//1.获取subject
Subject subject = SecurityUtils.getSubject();
//获取session
String sid = (String) subject.getSession().getId();
//2.调用subject进行登录
subject.login(upToken);
return "登录成功";
}catch (Exception e) {
return "用户名或密码错误";
}
}