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, adminguest = 123456, guest, goodguy# roleName = perm1, perm2, ..., permN[roles]admin = *common = dept:*goodguy = winnebago:drive:eagle5
2、自定义认证授权。
public class ShiroTest {@Testpublic 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")); // falseSystem.out.println(subject.isPermitted("winnebago:drive:eagle5")); // true}}
2.3 自定义配置类
核心有三个 Bean 配置
@Configurationpublic class ShiroConfig {/*** 定义Realm* @return*/@Beanpublic Realm realm() {}/*** 定义SecurityManager* @return*/@Beanpublic DefaultWebSecurityManager securityManager() {}/*** 定义ShiroFilter,实现对请求拦截* @return*/@Beanpublic 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、rolesrealm.addAccount("admin", "123456", "ADMIN");realm.addAccount("normal", "123456", "NORMAL");return realm;}
2.3.2 SecurityManager
@Beanpublic DefaultWebSecurityManager securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 设置其使用的RealmsecurityManager.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、登录校验。
@Controllerpublic 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")@RestControllerpublic 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")@RestControllerpublic 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());}
