简介
Shiro是一个功能强大和易于使用的Java安全框架,为开发人员提供一个直观而全面的解决方案的认证,授权,加密,会话管理。
Shiro 四个主要的功能:
Authentication:身份认证/登录,验证用户是不是拥有相应的身份
Authorization:授权,即权限验证,判断某个已经认证过的用户是否拥有某些权限访问某些资源,一般授权会有角色授权和权限授权
Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的,web 环境中作用是和 HttpSession 是一样的
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
Shiro 的其它几个特点:
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro 的架构有 3 个主要概念:Subject, SecurityManager和Realms
Subject:主体,相当于是请求过来的“用户”
SecurityManager: 管理着所有 Subject,负责进行认证和授权、及会话、缓存的管理,是 Shiro 的心脏,所有具体的交互都通过 SecurityManager 进行拦截并控制
Realm:一般我们都需要去实现自己的Realm ,可以有1个或多个 Realm,即当我们进行登录认证时所获取的安全数据来源(帐号/密码)
环境搭建
创建SpringBoot项目springboot-shiro-first,导入Shiro依赖包
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
编写简单的前端代码
index.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}"></p>
<a href="/user/add">add</a> |
<a href="/user/update">update</a>
</body>
</html>
add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>
update.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>
编写controller
@RestController
public class UserController {
@RequestMapping({"/","/index"})
public String toIndex(Model model) {
model.addAttribute("msg","hello, Shiro!");
return "index";
}
@RequestMapping("/user/add")
public String add() {
return "user/add";
}
@RequestMapping("/user/update")
public String update() {
return "user/update";
}
}
编写Shiro配置类ShiroConfig
package com.tomato.jackson.config;
import com.tomato.jackson.entity.UserRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
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;
/**
* @author gf
* @date 2023/1/30
*/
@Configuration
public class ShiroConfig {
// ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
// DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 关联 UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
// 创建 realm 对象, 需要自定义类
// @Bean(name = "userRealm")
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
}
注意:
以上代码为Shiro配置类的固定写法,需要创建ShiroFilterFactoryBean过滤器对象、DefaultWebSecurityManager对象、自定义Realm对象
通过@Qualifier标签拿到userRealm()方法创建的Bean对象,绑定到getDefaultWebSecurityManager()方法中的参数userRealm上
编写自定义的Realm类,需要继承AuthorizingRealm抽象类
package com.tomato.jackson.entity;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @author gf
* @date 2023/1/30
*/
public class UserRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
return null;
}
}
测试页面跳转
首页:
新增(add):
更新(update):
登录拦截
果点击add、update时,要设置拦截功能:在进行页面跳转过程中首先会跳转到一个登录界面。Shiro可以通过拦截器链实现该功能,常见的拦截器有:
anon:任何人都可以访问
authc:只有认证后才可以访问
logout:只有登录后才可以访问
roles[角色名]:只有拥有特定角色才能访问
perms["行为"]:只有拥有某种行为的才能访问
代码实现步骤如下:
手动创建拦截跳转页面
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>登录</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
</head>
<body>
<!--主容器-->
<div class="ui container">
<div class="ui segment">
<div style="text-align: center">
<h1 class="header">登录</h1>
</div>
<div class="ui placeholder segment">
<div class="ui column very relaxed stackable grid">
<div class="column">
<div class="ui form">
<!--<form th:action="@{/login}" method="post">-->
<form action="/login" method="post">
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" placeholder="username" name="username">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" name="password">
<i class="lock icon"></i>
</div>
</div>
<input type="submit" class="ui blue submit button"/>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/js/semantic.min.js}"></script>
</body>
</html>
并编写相应controller
@RequestMapping("/toLogin")
public String toLogin() {
return "去登陆";
}
编写登录拦截功能
登录拦截功能代码的实现需要在ShiroConfig类中getShiroFilterFactoryBean(...)添加以下代码:
// (1)设置拦截器链 : anon authc logout roles perms
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/**","authc");
bean.setFilterChainDefinitionMap(filterMap);
// (2)设置拦截登录的请求(拦截之后跳转的页面)
bean.setLoginUrl("/toLogin");
filterMap.put("/user/**","authc");表示路径user下的所有请求必须通过认证才会跳转
bean.setLoginUrl("/toLogin");表示路径user下的所有请求会跳转到URL为/toLogin的登录页面进行认证
测试拦截功能
点击add/update提示去登录
如果删除代码:
// (2)设置拦截登录的请求(拦截之后跳转的页面)
bean.setLoginUrl("/toLogin");
点击add、update时,跳转失败:
用户认证
用户在跳转页面时需要经过验证,否则不能跳转到界面里,登录拦截实现了拦截功能,此时需要在登录界面输入相应的用户名和密码进行验证,来判断输入的用户是否可以通过验证并进行界面跳转。步骤如下:
前端登录界面输入用户名和密码跳转到相应的controller进行处理。编写一个对应的controller
@PostMapping("/login")
public String login(@RequestParam("username") String username, @RequestParam("password")String password) {
// 获取当前用户
Subject subject = SecurityUtils.getSubject();
// 封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 执行登录方法
try {
subject.login(token);
return "index";
} catch(UnknownAccountException e) { //用户名不存在
return "login";
} catch (IncorrectCredentialsException e) { // 密码不存在
return "login";
}
}
此时进行登录测试会发现自动执行了UserRealm类中的doGetAuthenticationInfo(...)认证方法
2023-01-31 14:13:00.225 INFO 16612 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-01-31 14:13:00.226 INFO 16612 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-01-31 14:13:00.226 INFO 16612 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 0 ms
执行了=>认证doGetAuthenticationInfo
在UserRealm类补全的doGetAuthenticationInfo(...)认证方法代码
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
// 模拟获取数据库中的用户名和密码
String uname = "root";
String pwd = "123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
System.out.println(userToken.getUsername());
// 验证用户名是否输入正确
if (!userToken.getUsername().equals(uname)) {
// 用户名输入错误, return null 表示抛出异常 UnknownAccountException
return null;
}
// 验证密码是否输入正确, 由 Shiro 自动完成
return new SimpleAuthenticationInfo("", pwd, "");
}
测试登陆效果
根据登录逻辑,如果用户名和密码正确返回index,否则返回login
注意:SimpleAuthenticationInfo()中传入的密码需要String类型,传入int类型认证失败
用户授权
如果需要实现部分用户可以访问add页面,部分页面可以访问update页面,需要用到用户授权功能,实现步骤如下:
在ShiroConfig配置类getShiroFilterFactoryBean(...)方法中添加如下代码:
// 授权// 携带 user:add 字符串的用户才能有权限访问 user 文件夹下的 add 页面
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
表示如果想访问/user文件夹下的add资源,需要用户具有user:add权限
之后登录认证成功之后输入正确的用户名和密码发现报错未授权,错误401
此时执行了UserRealm类中的授权方法
需要在UserRealm类中的授权方法编写授权代码
用户未授权可以跳转到自定义未授权页面,而不是401页面
ShiroConfig类getShiroFilterFactoryBean(...)方法添加:
// 设置未授权跳转页面
bean.setUnauthorizedUrl("/unauth");
类MyController添加:
@RequestMapping("/unauth")@ResponseBodypublicStringunauth(){return"用户未授权访问权限!";}
测试如果用户未授权跳转到以下界面:
在UserRealm类中的授权方法编写授权代码
// 授权@OverrideprotectedAuthorizationInfodoGetAuthorizationInfo(PrincipalCollection principalCollection){System.out.println("执行了=>授权doGetAuthorizationInfo");SimpleAuthorizationInfo info =newSimpleAuthorizationInfo();// 为所有用户增加 user:add 权限//info.addStringPermission("user:add");// 获取当前登录用户Subject subject =SecurityUtils.getSubject();// 获取认证方法中查到的当前登录用户信息 : User 对象User currentUser =(User) subject.getPrincipal();// 获取当前用户在数据库中查询到的拥有的权限, 并为当前用户设置该权限
info.addStringPermission(currentUser.getPerms());return info;}
对象subject通过getPrincipal()方法获得当前User用户,User用户的信息是在认证代码中数据库查询到的,修改认证代码如下所示,通过return返回了一个SimpleAuthenticationInfo对象,将user对象作为SimpleAuthenticationInfo对象的第一个参数传入,之后授权代码便可以通过getPrincipal()方法获得当前User用户的信息了。
// 认证@OverrideprotectedAuthenticationInfodoGetAuthenticationInfo(AuthenticationToken token){
// ...returnnewSimpleAuthenticationInfo(user, user.getPwd(),"");}
通过addStringPermission()为当前User用户设置与数据库对应一致的权限,数据库表usetest增加权限字段perms,对应的User实体类也增加perms属性
@Data//Lombok标签@AllArgsConstructor@NoArgsConstructorpublicclassUser{//属性名称要与数据库对应表字段名称一致(不区分大小写)privateint id;privateString username;privateString pwd;privateString perms;}