十、Shiro
1、Shiro简介
- Apache Shiro 是一个Java的安全(权限)框架
- Shiro可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以在JavaEE环境。
- Shiro可以完成:认证,授权,加密,会话管理,Web集成,缓存等。
- 下载地址:http://shiro.apache.org/
- GitHub下载地址:https://github.com/apache/shiro.git
Shiro三大对象
- Subject 用户
- SecurityManager 安全管理所有用户
- Realm 连接数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CBDJ5BYh-1607428903125)(F:\Y2课程\Y2课程体系\SpringBootNote\img\image-20201208134023544.png)]
2、快速搭建
文件路径shiro-master\samples\quickstart
-
导入依赖
<dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.1</version> </dependency> <!-- configure logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> </dependencies>
-
配置文件(log4j.properties、shiro.ini)
-
Java类(Quickstart)
默认用的日志是commons-logging
代码分析
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
// 使用了日志框架输出
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// 创建工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 1 获取当前的用户对象
Subject currentUser = SecurityUtils.getSubject();
// 通过当前用户拿到Session
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// 2 测试当前的用户是否被认证
if (!currentUser.isAuthenticated()) {
// 通过账号的用户名和密码生成一个令牌
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 (AuthenticationException ae) { // 认证异常
//unexpected condition? error?
}
}
// 获取当前用户的一个认证
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
// 3 测试角色
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
// 4 监测此用户有什么权限(粗粒度)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
// 5 监测此用户有什么权限(细粒度)
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
// 6 注销
currentUser.logout();
// 7 结束
System.exit(0);
}
}
3、Shiro整合SpringBoot
1、步骤
-
创建SpringBoot项目
-
导入thymeleaf依赖
<!-- Thymeleaf模板依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency>
-
导入整合包依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.1</version> </dependency>
-
自定义Realm
public class UserRealm extends AuthorizingRealm { /** * 授权 * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("授权"); return null; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("认证"); return null; } }
-
创建ShiroConfig配置类
@Configuration public class ShiroConfig{ // ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; } // DafaultWebSecurityManager @Bean public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); // 关联Realm securityManager.setRealm(userRealm); return securityManager; } // 创建realm对象 @Bean public UserRealm userRealm(){ return new UserRealm(); } }
-
在Shiro配置类中
ShiroFilterFactoryBean
中设置过滤器-
设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
-
添加Shiro的内置过滤器
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); filterChainDefinitionMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
-
设置拦截后的登录页面
bean.setLoginUrl("/toLogin");
-
2、Shiro内置过滤器:
参考源码类DefaultFilter
/admin/**=anon
:无参,表示可匿名访问/admin/user/**=authc
:无参,表示需要认证才能访问/admin/user/**=authcBasic
:无参,表示需要httpBasic认证才能访问/admin/user/**=ssl
:无参,表示需要安全的URL请求,协议为https/home=user
:表示用户不一定需要通过认证,只要曾被 Shiro 记住过登录状态就可以正常发起 /home 请求/edit=authc,perms[admin:edit]
:表示用户必需已通过认证,并拥有 admin:edit 权限才可以正常发起 /edit 请求/admin=authc,roles[admin]
:表示用户必需已通过认证,并拥有 admin 角色才可以正常发起 /admin 请求/admin/user/**=port[8081]
:当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString/admin/user/**=rest[user]
:根据请求方式来识别,相当于/admins/user/**=perms[user:get]或perms[user:post]
等等/admin**=roles["admin,guest"]
:允许多个参数(逗号分隔),此时要全部通过才算通过,相当于hasAllRoles()/admin**=perms["user:add:*,user:del:*"]
:允许多个参数(逗号分隔),此时要全部通过才算通过,相当于isPermitedAll()
3、认证
-
在控制器处理前端的登录请求中进行认证
-
获取当前用户
Subject subject = SecurityUtils.getSubject();
-
封装用户的用户名和密码成令牌(用户名通过name和接口传参得到)
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
-
执行登录方法,
subject.login(token);
并捕捉异常
try { subject.login(token); // 执行登录方法,如果没有异常就说明OK了 return "index"; } catch (UnknownAccountException uae) { // 用户名不存在 model.addAttribute("massage","用户名不存在"); return "login"; } catch (IncorrectCredentialsException ice) { // 密码不对 model.addAttribute("massage","密码不对"); return "login"; } catch (LockedAccountException lae) { // 用户名被锁定 model.addAttribute("massage","用户名被锁定"); return "login"; } catch (AuthenticationException ae) { // 认证异常 model.addAttribute("massage","认证异常"); return "login"; }
-
-
每执行一次登录操作(
subject.login(token);
)都会进行一次认证(doGetAuthenticationInfo
)String name = "root"; String password = "2019"; UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token; if (!usernamePasswordToken.getUsername().equals(name)){ return null; // 抛出异常UnknownAccountException } // 密码认证 return new SimpleAuthenticationInfo("",password,"");
4、Shiro整合Mybatis
1、步骤
-
导入依赖
<!-- Mysql驱动--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- Druid数据源--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.3</version> </dependency> <!-- Log4j--> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> </dependency>
-
创建yaml文件配置数据源
spring: datasource: username: root password: 2019 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的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
-
建立mybatis的映射
mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.aaron.pojo
-
创建实体类(pojo)
@Data @AllArgsConstructor @NoArgsConstructor public class Userguang { private int id; private String name; private String pwd; }
-
创建数据访问层(Mapper)
@Mapper @Repository public interface UserMapper { /** * 通过用户名登录 * @return */ Userguang queryUserByName(String name); }
-
在配置(
resources
)中创建mapper的xml文件<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.aaron.mapper.UserMapper"> <select id="queryUserByName" resultType="com.aaron.pojo.Userguang"> select * from userguang where name = #{name} </select> </mapper>
-
创建业务层(Service)
public interface UserService { /** * 通过用户名登录 * @return */ Userguang queryUserByName(String name); }
-
实现业务层
@Service public class UserServiceimpl implements UserService { private UserMapper userMapper; @Autowired public void setUserMapper(UserMapper userMapper) { this.userMapper = userMapper; } @Override public Userguang queryUserByName(String name) { return userMapper.queryUserByName(name); } }
-
在通过业务
queryUserByName
判断用户输入的账户是否存在@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("认证"); UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken)token; Userguang userguang = userService.queryUserByName(usernamePasswordToken.getUsername()); if (userguang.getName() == null){ return null; // 抛出异常UnknownAccountException } // 密码认证 可以 MD5加密 MD5盐值加密 return new SimpleAuthenticationInfo("",userguang.getPwd(),""); }
密码加密
所有的加密方式都在CredentialsMatcher
接口 下实现的
默认为SimpleCredentialsMatcher
类,其他的方法也都继承此类。
2、授权
-
定义权限(有此权限才能访问这个请求)
在配置类
ShiroConfig
中getShiroFilterFactoryBean
方法中设置filterChainDefinitionMap.put("/user/add","perms[user:add]"); filterChainDefinitionMap.put("/user/update","perms[user:update]");
-
给予权限(给当前请求赋予权限)
在
UserRealm
类中的授权方法AuthorizationInfo
设置授权方法想拿到当前账户信息,需要让认证方法拿给授权方法
- 通过返回的
SimpleAuthenticationInfo
对象(“对象”,,“密码”,“xxx”)
在授权方法里拿认证给的对象信息
- 获取当前登录的对象
Subject subject = SecurityUtils.getSubject();
- 拿认证方法给的对象信息
Userguang principal = (Userguang)subject.getPrincipal();
- 设置当前请求的权限
info.addStringPermission(principal.getPerms());
- 通过返回的
这样就实现了真正的业务,数据库表中字段存储权限,通过addStringPermission设置权限,设置的权限就是数据库表中字段的内容。内容就是此用户的权限
5、Shiro整合Thymeleaf
1、步骤
-
导入依赖
<dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency>
-
配置
ShiroDialect
用来整合Shiro
和thymeleaf
@Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); }
-
设置标签页头
<html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
-
判断当前登录的用户的权限
hasPermission
<div shiro:hasPermission="user:add"> <a th:href="@{/user/add}">add</a> </div>
2、动态登录与注销
实现思路:判断是否登录,如果未登录显示登录按钮,如果登录显示注销按钮。
**1、登录:**判断用户是否登录需要看Session中是否有用户(在登录成功后,把用户信息放到Session中)
// 登录成功后,把用户的信息方法Session中
// 1、获取当前对象
Subject currentSubject = SecurityUtils.getSubject();
// 2、获取当前对象的Session
Session session = currentSubject.getSession();
// 3、设置把登录成功的信息放到Session中
session.setAttribute("loginUser",userguang);
<div th:if="${session.loginUser == null}">
<p><a th:href="@{/toLogin}">登录</a></p>
</div>
2、注销
在ShiroFilterFactoryBean
方法中对注销进行约定
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/logout","logout");
在控制器中写处理请求
@RequestMapping("/logout")
public void logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
}
在前端判断用户是否已经登录,如果登录发送注销请求。
<div th:if="${session.loginUser != null}">
<p><a th:href="@{/logout}">注销</a></p>
</div>