Shiro学习笔记
Shiro
shiro外部来看:
内部来看:
认证登录基本流程:
- 收集用户身份/凭证,如用户名密码
- 调用Subject.login(),进行登录,如果错误返回异常
- 创建自定义的Realm类,继承org.apache.shiro.realm.AuthorizingRealm类,实现doGetAuthenticationInfo()方法
授权流程:
- 首先调用Subject.isPermitted/hasRole接口,其会委托给SecurityManager,然后SecurityManager会委托给Authorizer
- Authorizer是真正的授权者,如果调用isPermitted()会首先通过PermissionResolver把字符串转化成相应的Permission实例;
- 在进行角色授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限
- Authorizer会判断Relam的角色/权限是否和传入的匹配,如果有多个Relam,会委托给ModularRelamAuthorizor进行循环判断,如果匹配isPermitted/hasRole,会返回true,反之返回false
- Subject.checkPermission方法与isPermitted/hasRole异同:isPermitted/hasRole返回true/false,checkPermission方法如果不存在权限抛出异常,没有返回值
登录认证实例:
ini:
[users]
zhangsan=z3,role1,role2
lisi=l4
[roles]
role1=user:insert
java:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.pam.UnsupportedTokenException;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
/**
* @author HanJiacheng
* @date 2024/1/13 15:50
*/
public class ShiroMain {
public static void main(String[] args) {
// 初始化Shiro
IniSecurityManagerFactory iniSecurityManagerFactory=new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = iniSecurityManagerFactory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 获取Subject对象
Subject subject = SecurityUtils.getSubject();
// 创建Token对象
AuthenticationToken token=new UsernamePasswordToken("zhangsan","z3");
// 完成登录
try {
subject.login(token);
System.out.println("登录成功");
// 判断是否有角色
boolean hasRole = subject.hasRole("user:insert");
System.out.println("是否有角色:"+hasRole);
// 判断是否有角色
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否有权限:"+permitted);
// 判断是否有角色
subject.checkPermission("user:insert");
}catch (UnknownAccountException e){
System.out.println("用户不存在");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误");
}
}
}
Shiro加密:
shiro自带了多种加密方法,包含普通加密,加盐加密,迭代加密,下列代码为MD5类型加密
import org.apache.shiro.crypto.hash.Md5Hash;
/**
* @author HanJiacheng
* @date 2024/1/13 16:58
*/
public class ShiroMD5 {
public static void main(String[] args) {
String password="password";
// MD5加密
Md5Hash md5Hash=new Md5Hash(password);
System.out.println("md5Hash = " + md5Hash);
// MD5加盐加密
Md5Hash md5HashSalt=new Md5Hash(password,"salt");
System.out.println("md5Hash2 = " + md5HashSalt);
// MD5迭代加密三次
Md5Hash md5HashIter=new Md5Hash(password,"salt",3);
System.out.println("md5HashIter = " + md5HashIter);
}
}
Shiro自定义认证
shiro的默认认证不带加密,想要加密就要自定义认证方法
创建自定义的Realm类,继承org.apache.shiro.realm.AuthorizingRealm类,实现doGetAuthenticationInfo()方法
ini文件
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
#md5加密迭代次数
md5CredentialsMatcher.hashIterations=3
#自定义Relam类地址
myrealm=com.hanjiacheng.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
#上面的代码为新添加的代码
[users]
zhangsan=z3,role1,role2
lisi=l4
[roles]
role1=user:insert
java
package com.hanjiacheng;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.util.ByteSource;
/**
* @author HanJiacheng
* @date 2024/1/13 17:12
*/
public class MyRealm extends AuthenticatingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1.获取身份信息
String princip=authenticationToken.getPrincipal().toString();
// 2.获取凭证信息
String password=new String((char[])authenticationToken.getCredentials());
System.out.println("认证用户信息:"+princip+"---"+password);
// 3.获取数据库中存储的用户信息
if(princip.equals("zhangsan")){
// 数据存储加盐迭代三次密码
String pwdInfo ="dda5359be921db4f73a69223ec264c11";
// 封装校验逻辑对象,将要比较的数据给对象
AuthenticationInfo info =new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),
pwdInfo,
ByteSource.Util.bytes("salt"),
authenticationToken.getPrincipal().toString()
);
return info;
}
return null;
}
}
Shiro整合SpringBoot
准备工作
pom:
<parent>
<artifactId>spring-boot-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
User类:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Integer id;
private String name;
private String pwd;
private Integer rid;
}
UserService接口:
public interface UserService {
//用户登录
User getUserInfoByName(String name);
}
接口实现类和Mapper类按照MyBatis-Plus来写
准备工作到此完成
编写自己的Realm类
继承AuthorizingRealm类
AuthorizingRealm类继承了AuthenticatingRealm类,需要重写两个方法:doGetAuthorizationInfo 自定义授权,doGetAuthenticationInfo 自定义认证
@Component
public class MyRealm extends AuthorizingRealm {
@Autowired
UserService userService;
// 自定义授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 自定义认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户身份信息
String name = authenticationToken.getPrincipal().toString();
// 调用业务层获取用户信息
User user = userService.getUserInfoByName(name);
// 非空校验,不为空就封装
if(user !=null){
AuthenticationInfo authenticationInfo= new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),
user.getPwd(),
ByteSource.Util.bytes("salt"),
name
);
}
return null;
}
}
配置Shiro相关信息,这里用类进行配置
@Configuration
public class RealmConfig {
@Autowired
private MyRealm myRealm;
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
// 1.创建DefaultSecurityManager对象
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 2.创建加密对象
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
// 2.1设置加密类型
matcher.setHashAlgorithmName("md5");
// 2.2设置加密迭代次数
matcher.setHashIterations(3);
// 3.将加密对象存储到MyRealm中
myRealm.setCredentialsMatcher(matcher);
// 4.将MyRealm对象封装到DefaultSecurityManager对象中
manager.setRealm(myRealm);
// 5.返回DefaultSecurityManager对象
return manager;
}
}
编写相关Controller类
@Controller
@RequestMapping("myController")
public class MyController {
@GetMapping("userLogin")
public String userLogin(String username, String password){
// 1.获取Subject对象
Subject subject= SecurityUtils.getSubject();
// 2.封装请求到Token中
AuthenticationToken token=new UsernamePasswordToken(username,password);
// 3.调用subject.login()进行登录
try {
subject.login(token);
return "登录成功";
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录异常");
return "登录异常";
}
}
}
在ShiroConfig类中配置过滤拦截范围
@Configuration
public class RealmConfig {
@Autowired
private MyRealm myRealm;
@Bean
public DefaultWebSecurityManager defaultSecurityManager(){
// 1.创建DefaultSecurityManager对象
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 2.创建加密对象
HashedCredentialsMatcher matcher=new HashedCredentialsMatcher();
// 2.1设置加密类型
matcher.setHashAlgorithmName("md5");
// 2.2设置加密迭代次数
matcher.setHashIterations(3);
// 3.将加密对象存储到MyRealm中
myRealm.setCredentialsMatcher(matcher);
// 4.将MyRealm对象封装到DefaultSecurityManager对象中
manager.setRealm(myRealm);
// 5.返回DefaultSecurityManager对象
return manager;
}
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/myController/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
return definition;
}
}
多个Realm校验
多个realm实现原理
当应用程序配置多个 Realm 时,例如:用户名密码校验、手机号验证码校验等等。
Shiro 的 ModularRealmAuthenticator 会使用内部的 AuthenticationStrategy 组件判断认
证是成功还是失败。
AuthenticationStrategy 是一个无状态的组件,它在身份验证尝试中被询问 4 次(这
4 次交互所需的任何必要的状态将被作为方法参数):
(1) 在所有 Realm 被调用之前
(2) 在调用 Realm 的 getAuthenticationInfo 方法之前
(3) 在调用 Realm 的 getAuthenticationInfo 方法之后
(4) 在所有 Realm 被调用之后
认证策略的另外一项工作就是聚合所有 Realm 的结果信息封装至一个
AuthenticationInfo 实例中,并将此信息返回,以此作为 Subject 的身份信息。
Shiro 中定义了 3 种认证策略的实现:
AuthenticationStrategy class | 描述 |
---|---|
AtLeastOneSuccessfulStrategy | 只要有一个(或更多)的Realm验证成功,那么认证将视为成功 |
FirstSuccessfulStrategy | 第一个Realm验证成功,整体认证将视为成功,且后续Realm将被忽略 |
AllSuccessfulStrategy | 所有Realm成功,认证才视为成功 |
ModularRealmAuthenticator 内置的认证策略默认实现是
AtLeastOneSuccessfulStrategy 方式。可以通过配置修改策略
代码实现:
//配置 SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(){
//1 创建 defaultWebSecurityManager 对象
DefaultWebSecurityManager defaultWebSecurityManager = new
DefaultWebSecurityManager();
//2 创建认证对象,并设置认证策略
ModularRealmAuthenticator modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(newAllSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
//3 封装 myRealm 集合List<Realm> list = new ArrayList<>();
list.add(myRealm);
list.add(myRealm2);
//4 将 myRealm 存入 defaultWebSecurityManager 对象
defaultWebSecurityManager.setRealms(list);
//5 返回
return defaultWebSecurityManager;
}
RememberMe 实现
配置类添加代码:
//cookie 属性设置
public SimpleCookie rememberMeCookie(){
SimpleCookie cookie = new SimpleCookie("rememberMe");
//设置跨域
//cookie.setDomain(domain);
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
//创建 Shiro 的 cookie 管理对象
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
cookieRememberMeManager.setCipherKey("1234567890987654".getBytes());
return cookieRememberMeManager;
}
配置类DefaultSecurityManager方法添加RememberMe
// rememberMe
manager.setRememberMeManager(rememberMeManager());
过滤器添加过滤条目:
// 设置存在user的过滤器(rememberMe)
definition.addPathDefinition("/**","user");
controller类中userLogin方法添加接收是否记住我的布尔类参数和直接登录的方法
@GetMapping("userLogin")
public String userLogin(String username, String pwd, HttpSession session,
@RequestParam(defaultValue = "false") boolean rememberMe){
System.out.println("username:"+username+",pwd:"+pwd);
// 1.获取Subject对象
Subject subject= SecurityUtils.getSubject();
// 2.封装请求到Token中
AuthenticationToken token=new UsernamePasswordToken(username,pwd,rememberMe);
// 3.调用subject.login()进行登录
try {
subject.login(token);
session.setAttribute("user",username);
return "main";
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录异常");
return "登录异常";
}
}
@GetMapping("toMain")
public String toMain(HttpSession httpSession){
httpSession.setAttribute("user","rememberMe");
return "main";
}
登出功能
修改main.html
<body>
<h1>Shiro 登录认证后主页面</h1>
<br>
登录用户为: <span th:text="${session.user}"></span>
<a href="/logout">登出</a>
</body>
在过滤器中添加登出过滤器
public RealmConfig{
//配置 Shiro 内置过滤器拦截范围
@Bean
public DefaultShiroFilterChainDefinition shiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
//设置不认证可以访问的资源
definition.addPathDefinition("/myController/userLogin","anon");
definition.addPathDefinition("/myController/login","anon");
//设置需要进行登录认证的拦截范围
definition.addPathDefinition("/**","authc");
// 设置存在user的过滤器(rememberMe)
definition.addPathDefinition("/**","user");
// 登出
definition.addPathDefinition("/logout","logout");
return definition;
}
...
//过滤器方法省略
}
获取用户角色进行认证
role表
role_user表
为什么要单独建一个role_user表,而不是在user表里添加:因为一个用户会有多个角色,所以要单独建立一个表将二者对应起来。
验证用户是否有对应的角色
这里从数据库获取用户角色信息,并且输出来进行模拟
前端添加代码:
<a href="/myController/userLoginRoles">测试授权</a>
Mapper类
@Repository
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT NAMEFROM role WHERE idIN (SELECT ridFROM role_user WHERE uid=(SELECT idFROM USER WHERE NAME=#{principal}))")
List<String> getUserRoleInfoMapper(@Param("principal") String principal);
}
Service实现类:
@Override
public List<String> getUserRoleInfo(String principal) {
List<String> list=userMapper.getUserRoleInfoMapper(principal);
return list;
}
MyRealm添加授权代码在授权方法中
// 自定义授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1.获取用户身份信息
String name=principalCollection.getPrimaryPrincipal().toString();
// 2.获取当前用户的角色信息
List<String> roles=userService.getUserRoleInfo(name);
System.out.println("当前用户角色信息:"+roles);
// 创建对象,存储当前登录的用户的权限和角色
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
info.addRoles(roles);
return info;
}
controller
// 验证是否有对应角色,回进入到MyRealm中doGetAuthorizationInfo方法进行自定义授权,括号里填写的是用户角色,验证是否有这个用户角色。
@RequiresRoles("admin")
@GetMapping("userLoginRoles")
@ResponseBody
public String userLoginRoles(){
return "验证角色成功";
}
测试
成功显示当前用户角色信息
如果修改了Controller类中注解:
@RequiresRoles("admin")
括号中的角色名,改为用户所不具有的角色,那么会报错
验证用户是否有对应权限
权限表:
角色与权限对应表
前端添加代码
<a href="/myController/userLoginPermissions">测试权限</a>
UserMapper类新增加查询语句
@Select({
"<script>",
"select infoFROM permissions WHERE idIN ",
"(SELECT pidFROM role_ps WHERE ridIN (",
"SELECT idFROM role WHERE NAMEIN ",
"<foreach collection='roles' item='name' open='(' separator=',' close=')'>",
"#{name}",
"</foreach>",
"))",
"</script>"
})
List<String> getUserPermissionInfoMapper(@Param("roles")List<String> roles);
UserServiceImpl增加方法
@Override
public List<String> getUserPermissionInfo(List<String> roles) {
List<String> list=userMapper.getUserPermissionInfoMapper(roles);
return list;
}
MyRealm授权方法:
// 自定义授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 1.获取用户身份信息
String name=principalCollection.getPrimaryPrincipal().toString();
// 2.获取当前用户的角色信息
List<String> roles=userService.getUserRoleInfo(name);
System.out.println("当前用户角色信息:"+roles);
// 2.5获取用户权限信息
List<String> permissions=userService.getUserPermissionInfo(roles);
System.out.println("当前用户权限信息"+permissions);
//创建对象,存储当前登录的用户的权限和角色
SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();
// 添加用户角色信息
info.addRoles(roles);
// 添加用户权限信息
info.addStringPermissions(permissions);
return info;
}
通过UserService获取用户权限:``List`<String>` permissions=userService.getUserPermissionInfo(roles);``,将用户权限信息添加到AuthorizationInfo类中 ``info.addStringPermissions(permissions);``
Controller类
// 验证是否有对应权限,回进入到MyRealm中doGetAuthorizationInfo方法进行自定义授权
@RequiresPermissions("user:delete")
@GetMapping("userLoginPermissions")
@ResponseBody
public String userLoginPermissions(){
return "验证权限成功";
}
测试
权限信息显示成功
如果修改了Controller类中注解:
@RequiresPermissions("user:delete")
括号中的角色名,改为用户所不具有的权限,那么会报错
异常处理
在我们验证用户是否有权限或者角色等操作中,如果没有权限或者角色会抛出异常,我们要对这些异常进行统一的处理(有些异常时注解抛出的),新建一个类,专门用于处理全局异常,在类的前方添加 @ControllerAdvice
注解,表示这是一个全局性类。(@ControllerAdvice
是一个增强型的Controller注解,也可实现页面跳转或者返回数据,同时也有@RestControllerAdvice)
全局异常处理
配合@ExceptionHandler()注解用于全局异常的处理,括号内是异常的处理类型。在方法前面加上表示这个方法捕获注解中的类。
@ControllerAdvice
public class PermissionsException {
@ResponseBody
@ExceptionHandler(UnauthorizedException.class)
public String unauthorizedException(Exception ex){
return "无权限";
}
@ResponseBody
@ExceptionHandler(AuthorizationException.class)
public String authorizationException(Exception ex){
return "权限认证失败";
}
}
由于权限认证失败和角色认证失败都是抛出
UnauthorizedException
所以在这个项目中角色认证异常和权限认证异常都是进入同一个方法。
前端界面授权认证
我们希望不同的角色显示的条目不同(或者页面内容不同)我们可以使用Thymeleaf与Shrio的整合依赖来实现这个功能
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
配置类中添加如下代码
// 用于解析 thymeleaf 中的 shiro:相关属性
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}
Thymeleaf 中常用的 shiro:属性
guest 标签
<shiro:guest></shiro:guest>
用户没有身份验证时显示相应信息,即游客访问信息。
user 标签
<shiro:user></shiro:user>
用户已经身份验证/记住我登录后显示相应的信息。
authenticated 标签
<shiro:authenticated></shiro:authenticated>
用户已经身份验证通过,即 Subject.login 登录成功,不是记住我登录的。
notAuthenticated 标签
<shiro:notAuthenticated></shiro:notAuthenticated>
用户已经身份验证通过,即没有调用 Subject.login 进行登录,包括记住我自动登录的也属于未进行身份验证。
principal 标签
<shiro: principal/><shiro:principal property="username"/>
相当于((User)Subject.getPrincipals()).getUsername()。
lacksPermission 标签
<shiro:lacksPermission name="org:create"></shiro:lacksPermission>
如果当前 Subject 没有权限将显示 body 体内容。
hasRole 标签
<shiro:hasRole name="admin"></shiro:hasRole>
如果当前 Subject 有角色将显示 body 体内容。
hasAnyRoles 标签
<shiro:hasAnyRoles name="admin,user"></shiro:hasAnyRoles>
如果当前 Subject 有任意一个角色(或的关系)将显示 body 体内容。
lacksRole 标签
<shiro:lacksRole name="abc"></shiro:lacksRole>
如果当前 Subject 没有角色将显示 body 体内容。
hasPermission 标签
<shiro:hasPermission name="user:create"></shiro:hasPermission>
如果当前 Subject 有权限将显示 body 体内容