1. 简介
1.1 了解Shiro
Shiro 是 apache 旗下一个开源框架,它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
核心架构如下:
- Subject:主体,可以理解为当前操作的用户,或者是一个浏览器请求。
- SecurityManager:安全管理器,负责对所有的 subject 进行安全管理,完成 subject 的认证和授权。实质上 SecurityManager 是通过 Authenticator 进行认证,通过 Authorizer 进行授权,通过 SessionManager 进行会话管理等。
- Authenticator:认证器,对用户身份进行认证,Authenticator 是一个接口,shiro 提供 ModularRealmAuthenticator 实现类,通过 ModularRealmAuthenticator 基本上可以满足大多数需求,也可以自定义认证器。
- Authorizer:授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
- Realm:领域,相当于datasource数据源,securityManager 进行安全认证需要通过 Realm 获取用户权限数据,比如:如果用户身份数据在数据库那么 realm 就需要从数据库获取用户身份信息。
- SessionManager:会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以 shiro 可以使用在非 web 应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
- SessionDAO:即会话 dao ,是对 session 会话操作的一套接口,比如要将 session 存储到数据库,可以通过 jdbc 将会话存储到数据库。
- CacheManager:缓存管理,将用户权限数据存储在缓存,这样可以提高性能。
- Cryptography:密码管理,shiro 提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
1.2 了解认证
判断一个用户是否为合法用户的处理过程,通过就是校验用户名和密码。在认证过程中,有三个关键对象:
- Subject:主体,访问系统的用户,主体可以是用户、程序等,进行认证的都称为主体;
- Principal:身份信息,是主体(subject)进行身份认证的标识,标识必须具有 唯一性 ,如用户名、手机号、邮箱地址等,一 个主体可以有多个身份,但是必须有一个主身份(Primary Principal)。
credential:凭证信息,只有主体自己知道的安全信息,如密码、证书等。
1.3 了解授权
即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源。在授权过程中,有三个关键对象,授权过程可以简单理解为 who 对 what 进行 how 的操作。
Who,即主体(Subject),主体需要访问系统中的资源。
- What,即资源(Resource) ,如系统菜单、页面、按钮、类方法、系统商品信息等。资源包括资源类型和资源实例,比如商品信息为资源类型,类型为t01的商品为资源实例,编号为001的商品信息也属于资源实例。
- How,权限/许可(Permission) ,规定了主体对资源的操作许可,权限离开资源没有意义,如用户查询权限、用户添加权限、某个类方法的调用权限、编号为001用户的修改权限等,通过权限可知主体对哪些资源都有哪些操作许可。
授权有两种方式,一种是基于角色授权,赋予用户角色,另一种是基于资源的授权。可参考:https://juejin.cn/post/6941734947551969288#heading-1。
2. 快速入门
2.1 引入依赖
```xml <?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">
4.0.0 <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
com.ljnt blog 0.0.1-SNAPSHOT blog Demo project for Spring Boot 1.8 org.apache.shiro shiro-spring-boot-starter 1.8.0 com.auth0 java-jwt 3.11.0 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test com.baomidou mybatis-plus-boot-starter 3.5.1 mysql mysql-connector-java 5.1.47 runtime org.springframework.boot spring-boot-maven-plugin
<a name="XnHMq"></a>
## 2.2 本地运行
1、resources 目录下自定义 shiro.ini 文件。
```properties
# username = password, role1, role2, ..., roleN
[users]
weishao = 123456, admin
guest = 123456, guest, goodguy
# roleName = perm1, perm2, ..., permN
[roles]
admin = *
common = dept:*
goodguy = winnebago:drive:eagle5
2、自定义认证授权。
public class ShiroTest {
@Test
public void testShiroIni() {
// 创建安全管理器,获取用户权限数据
DefaultSecurityManager securityManager = new DefaultSecurityManager();
securityManager.setRealm(new IniRealm("classpath:shiro.ini"));
// 在安装工具类中设置默认安全管理器
SecurityUtils.setSecurityManager(securityManager);
// 获取主体,可能是用户,也可能是一段程序
Subject subject = SecurityUtils.getSubject();
// 创建用户登录凭证,从controller传递过来的
UsernamePasswordToken token = new UsernamePasswordToken("guest", "123456");
// 登录校验,这里进行认证
subject.login(token);
// 判断用户已经认证
System.out.println(subject.isAuthenticated()); // true
// 判断权限
System.out.println(subject.hasRole("admin")); // false
System.out.println(subject.isPermitted("winnebago:drive:eagle5")); // true
}
}
2.3 自定义配置类
核心有三个 Bean 配置
@Configuration
public class ShiroConfig {
/**
* 定义Realm
* @return
*/
@Bean
public Realm realm() {}
/**
* 定义SecurityManager
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager() {}
/**
* 定义ShiroFilter,实现对请求拦截
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {}
}
2.3.1 Realm
Realm,通过和数据源是一对一的关系,主要进行用户认证和授权。看一下 Realm 的类图:
具体来看下 AuthorizingRealm 源码,我们自定义 Realm 一般都继承这个类,重写两个方法:
// 认证方法,Realm接口自定义
AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException;
// 授权方法,AuthorizingRealm 新增
protected abstract AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals);
这里采用了 SimpleAccountRealm,是使用内存作为数据源,代码如下:
public Realm realm() {
// 创建 SimpleAccountRealm 对象(使用内存作为数据源)
SimpleAccountRealm realm = new SimpleAccountRealm();
// 添加两个用户,参数是username、password、roles
realm.addAccount("admin", "123456", "ADMIN");
realm.addAccount("normal", "123456", "NORMAL");
return realm;
}
2.3.2 SecurityManager
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 设置其使用的Realm
securityManager.setRealm(this.realm());
return securityManager;
}
2.3.3 ShiroFilter
通过 AbstractShiroFilter 过滤器,实现对请求的拦截,从而实现 Shiro 的功能。比较常用的过滤器有:
- anon :AnonymousFilter :允许匿名访问,即无需登陆。
- authc :FormAuthenticationFilter :需要经过认证的用户,才可以访问。如果是匿名用户,则根据 URL 不同,会有不同的处理:
- 如果拦截的 URL 是 GET loginUrl 登陆页面,则进行该请求,跳转到登陆页面。
- 如果拦截的 URL 是 POST loginUrl 登陆请求,则基于请求表单的 username、password 进行认证。认证通过后,默认重定向到 GET loginSuccessUrl 地址。
- 如果拦截的 URL 是其它 URL 时,则记录该 URL 到 Session 中。在用户登陆成功后,重定向到该 URL 上。
- logout :LogoutFilter :拦截的 URL ,执行退出操作。退出完成后,重定向到 GET loginUrl 登陆页面。
- roles :RolesAuthorizationFilter :拥有指定角色的用户可访问。
- perms :PermissionsAuthorizationFilter :拥有指定权限的用户可以访问。
```java
/**
- 定义ShiroFilter,实现对请求拦截
- @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { // 1、创建ShiroFilterFactoryBean对象,用于创建ShiroFilter过滤器 ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean(); // 2、设置SecurityManager filterFactoryBean.setSecurityManager(this.securityManager()); // 3、设置URL filterFactoryBean.setLoginUrl(“/login”); // 登录URL filterFactoryBean.setSuccessUrl(“/login_success”); // 登录成功URL filterFactoryBean.setUnauthorizedUrl(“/unauthorized”); // 无权限URL // 4、设置URL的权限配置,过滤器执行链 filterFactoryBean.setFilterChainDefinitionMap(this.filterChainDefinitionMap()); return filterFactoryBean; }
/**
- 设置URL权限配置
- @return
/
private Map
filterChainDefinitionMap() { Map “, “authc”); // 默认剩余的URL,需要经过认证 return filterMap; } ```filterMap = new LinkedHashMap<>(); // 这里要使用有序的LinkedHashMap filterMap.put(“/test/echo”, “anon”); // 允许匿名访问 filterMap.put(“/test/admin”, “roles[ADMIN]”); // 需要ADMIN角色 filterMap.put(“/test/normal”, “roles[NORMAL]”); // 需要NORMAL角色 filterMap.put(“/logout”, “logout”); // 退出 filterMap.put(“/* 2.4 常用Shiro注解
在 Shiro 中,提供了如下五个注解,可以直接添加在 SpringMVC 的 URL 对应的方法上,实现权限配置。
- @RequiresGuest 注解,和 anon 等价。
- @RequiresAuthentication 注解,和 authc 等价。
- @RequiresUser 注解,和 user 等价,要求必须登陆。
- @RequiresRoles 注解,和 roles 等价。如果验证权限不通过,则会抛出 AuthorizationException 异常。此时,我们可以基于 Spring MVC 提供的 @RestControllerAdvice + @ExceptionHandler 注解,实现全局异常的处理。
- @RequiresPermissions 注解,和 perms 等价。如果验证权限不通过,则会抛出 AuthorizationException 异常。
举例说明:
// 属于 NORMAL 角色
@RequiresRoles("NORMAL")
// 要同时拥有 ADMIN 和 NORMAL 角色
@RequiresRoles({"ADMIN", "NORMAL"})
// 拥有 ADMIN 或 NORMAL 任一角色即可
@RequiresRoles(value = {"ADMIN", "NORMAL"}, logical = Logical.OR)
// 拥有 user:add 权限
@RequiresPermissions("user:add")
// 要同时拥有 user:add 和 user:update 权限
@RequiresPermissions({"user:add", "user:update"})
// 拥有 user:add 和 user:update 任一权限即可
@RequiresPermissions(value = {"user:add", "user:update"}, logical = Logical.OR)
2.5 定义Controller
1、登录校验。
@Controller
public class LoginController {
@GetMapping("/login")
public String loginPage() {
return "login.html";
}
@ResponseBody
@PostMapping("/login")
public String login(HttpServletRequest request) {
// 1、判断是否已经登录
Subject subject = SecurityUtils.getSubject();
if (subject.getPrincipal() != null) {
return "你已经登录账户:" + subject.getPrincipal();
}
// 2、获得登录失败的原因
String shiroLoginFailure = (String) request.getAttribute(FormAuthenticationFilter.DEFAULT_ERROR_KEY_ATTRIBUTE_NAME);
String msg = "";
if (UnknownAccountException.class.getName().equals(shiroLoginFailure)) {
msg = "账号不存在";
} else if (IncorrectCredentialsException.class.getName().equals(shiroLoginFailure)) {
msg = "密码不正确";
} else if (LockedAccountException.class.getName().equals(shiroLoginFailure)) {
msg = "账号被锁定";
} else if (ExpiredCredentialsException.class.getName().equals(shiroLoginFailure)) {
msg = "账号已过期";
} else {
msg = "未知";
}
return "登陆失败,原因:" + msg;
}
@ResponseBody
@GetMapping("/login_success")
public String loginSuccess() {
return "登录成功";
}
@ResponseBody
@GetMapping("/unauthorized")
public String unauthorized() {
return "你没有权限";
}
}
2、测试请求是否有权限访问。
@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
}
3、使用注解指定访问权限。
@RequestMapping("/demo")
@RestController
public class RequiresController {
@RequiresGuest
@GetMapping("/echo")
public String demo() {
return "示例返回";
}
@GetMapping("/home")
public String home() {
return "我是首页";
}
@RequiresRoles("ADMIN")
@GetMapping("/admin")
public String admin() {
return "我是管理员";
}
@RequiresRoles("NORMAL")
@GetMapping("/normal")
public String normal() {
return "我是普通用户";
}
}
3. 加密解密
shiro 框架中使用 SimpleHash 加盐进行密码加密,这个过程不可逆,使用的是其构造函数加密。
- algorithmName 代表进行加密的算法名称。
- source 代表需要加密的元数据,如密码。
- salt 代表盐,需要加进一起加密的数据。
hashIterations 代表hash迭代次数,可以不写,默认为 1。
public SimpleHash(String algorithmName, Object source, Object salt, int hashIterations) throws CodecException, UnknownAlgorithmException {
this.hexEncoded = null;
this.base64Encoded = null;
if (!StringUtils.hasText(algorithmName)) {
throw new NullPointerException("algorithmName argument cannot be null or empty.");
} else {
this.algorithmName = algorithmName;
this.iterations = Math.max(1, hashIterations);
ByteSource saltBytes = null;
if (salt != null) {
saltBytes = this.convertSaltToBytes(salt);
this.salt = saltBytes;
}
ByteSource sourceBytes = this.convertSourceToBytes(source);
this.hash(sourceBytes, saltBytes, hashIterations);
}
}
举例:使用构造函数对密码为 123456 进行加密。
public static void main(String[] args) {
String password = "123456";
String Shpassword = "c7acd09454c858e257aa5cecb1cdb904d6f6b0ac"; // 加密结果
String salt = "f182eba3b28945afc50c19e1ececb80e"; // 盐
SimpleHash hash = new SimpleHash("sha-1", password, salt);
System.out.println(hash.toHex());
}