springboot整合shiro
1. 什么是Shiro
Shiro是一个基于Java的安全框架,它提供了身份验证
、授权
、加密和会话管理等
安全功能,可以帮助Java应用程序实现安全性。
2. 根据Shiro的基本使用了解其基本原理
1. 添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.9.0</version>
</dependency>
2. 创建ini文件,实现登录功能
在resources文件目录下创建 后缀为 .ini 的文件。内容如下:
[users]
zhangsan=123
lisi=123
解释:表示创建了一个2个用户 分别是shangsan,lisi。他们的密码都是123。
//////////////////////////
subject.login 认证的原理如下:
1. 首先调用 Subject.login(token) 进行登录,其会自动委托给 SecurityManager
2. SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证
3. Authenticator 才是真正的身份验证者,Shiro API 中核心的身份 认证入口点,此处可以自定义插入自己的实现
4. Authenticator 可能会委托给相应的 AuthenticationStrategy 进 行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm身份验证
5. Authenticator 会把相应的 token 传入 Realm,从 Realm 获取 身份验证信息,如果没有返回/抛出异常表示身份验证失败了。此处 可以配置多个Realm,将按照相应的顺序及策略进行访问
自己总结:就是说 subject.login 最后其实调用的是 AuthenticatingRealm 类下的 doGetAuthenticationInfo(token) 方法
//////////////////////////
public class ShiroRun {
public static void main(String[] args) {
//1 初始化获取 SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//2 获取 Subject 对象
Subject subject = SecurityUtils.getSubject();
//3 创建 token 对象, web 应用用户名密码从页面传递
AuthenticationToken token = new UsernamePasswordToken("zhangsan","123");
//4 完成登录
try {
subject.login(token);
System.out.println("登录成功");
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
}
3. 利用shiro提供加密
Shiro 内嵌了很多常用的加密算法,比如MD5加密。
常见的加密算法:
对称加密:加密,解密使用同一个秘钥进行,常见的有 DES、3DES、AES 等。
非对称加密:使用一对密钥(公钥和私钥是一对)进行加密和解密,常见的有 RSA、DSA、ECC 等。
消息摘要算法:把任意长度的输入揉和而产生长度固定的伪随机结果的算法。常见的有 MD5、SHA-1、SHA-256、SHA-512 等。
数字签名算法:数字签名算法主要是为了解决消息的真实性和完整性问题,常见的有 RSA 签名、DSA 签名、ECDSA 签名等。
MD5:
被称为信息摘要算法,所谓的信息摘要就是把明文内容按一定规则生成一段哈希(hash)值,即得到这段明文内容的信息摘要。
md5是不可逆的,也就是没有对应的算法,从生产的md5值逆向得到原始数据。但是可以通过暴力解析得到原始数据。一般不会直接使用,都会对密码进行加盐处理。
SHA-256:
也被称为信息摘要算法,对于任意长度的消息,SHA256都会产生一个256位的哈希值。[总之,是目前很安全的加密算法,推荐]
// 使用Shiro提供的MD5进行加密:
public class ShiroMD5 {
public static void main(String[] args) {
// 密码明文
String password = "z3";
// 使用 md5 加密
Md5Hash md5Hash = new Md5Hash(password);
System.out.println("md5 加密:"+md5Hash.toHex());
// 带盐的 md5 加密,盐就是在密码明文后拼接新字符串,然后再进行加密
Md5Hash md5Hash2 = new Md5Hash(password,"salt");
System.out.println("md5 带盐加密:"+md5Hash2.toHex());
// 为了保证安全,避免被破解还可以多次迭代加密,保证数据安全
Md5Hash md5Hash3 = new Md5Hash(password,"salt",3);
System.out.println("md5 带盐三次加密:"+md5Hash3.toHex());
}
}
4. 授权(授予角色,授予权限)
授权方式可以分为:
编程式:
if(subject.hasRole("root")){
// 有权限
} else{
// 没权限
}
注解式:
@RequiresRoles("admin")
public MsgResult listUser(){
}
/////////////////////////
更改.ini文件:
[users]
zhangsan=shangsan,role1,role2
lisi=lisi
[roles]
role1=user:list,user:insert
解释:
表示zhangsan用户拥有role1与role2角色
role1角色拥有 user:list 的权限, role2角色拥有 user:insert 权限
【这里的 user:list 只是一种标识,可以任意定义,表示role1具有某种权限字符串,拥有该权限字符串,就能让它有什么权限】
/////////////////////////
public class ShiroRun {
public static void main(String[] args) {
//1 初始化获取 SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
//2 获取 Subject 对象
Subject subject = SecurityUtils.getSubject();
//3 创建 token 对象, web 应用用户名密码从页面传递
AuthenticationToken token = new UsernamePasswordToken("zhangsan","123");
//4 完成登录
try {
subject.login(token);
System.out.println("登录成功");
// 判断角色
if(subject.hasRole("role1")){
System.out.println("该用户拥有角色:role1")
}
// 判断权限
if(subject.isPermitted("user:insert")){
System.out.println("该用户拥有权限:user:insert")
}
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
}
3. 使用自定义Realm更改shiro的默认登录操作
更改.ini文件:若对密码进行了加密,需要指定我们使用的加密匹配器。
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
myrealm=com.lihao.config.realm.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
[users]
zhangsan=7174f64b13022acd3c56e2781e098a5f,role1,role2
lisi=root
[roles]
role1=user:insert,user:select
/////////////////////////////
// 明白下面几句话//
// subject.login(token) 实现登录,它的本质调用的是 AuthenticatingRealm 类下的 doGetAuthenticationInfo(token) 方法。
// 我们想改造shiro的登录,就可以继承AuthenticatingRealm类,重写doGetAuthenticationInfo(token) 方法以达到我们想在登录功能里面进行的额外操作。
// 在doGetAuthenticationInfo里面,我们做的就是将获取到账号密码,与数据库打交道。真正比对密码的还是交给的shiro,通过返回SimpleAuthenticationInfo。
// 这个类是我们自己写的,需要配置自定义的 realm 生效,在 ini 文件中配置,或 Springboot 中配置
public class MyRealm extends AuthenticatingRealm {
// 方法返回值:AuthenticationInfo 是个接口,实例化他的实现类用于返回,通常用SimpleAuthenticationInfo实现类作为返回值。
// 有2个方法:Object getPrincipal(); Object getCredentials();
// 有3个子接口:
// Account(没有方法)
// MergableAuthenticationInfo(方法:void merge(AuthenticationInfo info) )
// SaltedAuthenticationInfo(方法:ByteSource getCredentialsSalt() )
// SimpleAuthenticationInfo类定义了3个属性,且同时实现了MergableAuthenticationInfo, SaltedAuthenticationInfo接口
// public class SimpleAuthenticationInfo implements MergableAuthenticationInfo, SaltedAuthenticationInfo{}
// 3个属性为:principals, credentials, credentialsSalt
// 常用3个构造函数:
// 无参构造
// public SimpleAuthenticationInfo()
// 参数1:用户的身份信息。这通常是用户名或用户对象。【不一定就非得是用户名或者用户对象,经测试我发现用户id也可以,只要能确保用户提供的身份信息和凭证信息是正确的,并且该信息是与存储在数据库或其他数据源中的信息匹配的】
// 参数2:用户的凭证信息。这通常是密码或密码哈希值。
// 参数3:身份验证信息所在的Realm的名称
// public SimpleAuthenticationInfo(Object principal, Object credentials, String realmName)
// 参数1:用户的身份信息。这通常是用户名或用户对象。
// 参数2:用户的凭证信息。这通常是密码或密码哈希值。
// 参数3:表示凭据的盐值,是一个ByteSource类型的对象
// 参数4:身份验证信息所在的Realm的名称
// public SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName)
// 方法参数:AuthenticationToken 是个接口。
// 有2个方法:Object getPrincipal(); Object getCredentials();
// 有2个子接口:
// RememberMeAuthenticationToken(方法:boolean isRememberMe();)
// HostAuthenticationToken(方法:String getHost();)
// 这2个子接口,都有一个共同的实现类:public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken{}
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//1 获取身份信息 可以authenticationToken.getPrincipal, 也可以强转 (UsernamePasswordToken) authenticationToken,强转后通过 .getUserName
String principal = (String)authenticationToken.getPrincipal();
//2 如果数据库有一个叫 zhangsan 的用户,就走下面的代码,shiro会自动去判断密码是否正确。【它底层会去调用数据库作比对】
if(principal.equals("zhangsan")){
//3.1 模拟数据库里面用户zhangsan的密码为123,且用MD5加盐加密且散列3次后,password为:7174f64b13022acd3c56e2781e098a5f
String pwdInfo = "7174f64b13022acd3c56e2781e098a5f";
//3.2 创建封装了校验逻辑的对象,将要比较的数据给该对象
//Shiro提供了JdbcRealm来支持与数据库交互,当使用JdbcRealm时,Shiro会自动连接数据库并执行相应的查询语句。
AuthenticationInfo info = new SimpleAuthenticationInfo(authenticationToken.getPrincipal(),
pwdInfo,
ByteSource.Util.bytes("salt"),
this.getName());
return info;
}
return null;
}
}
注意到网上有些博客,喜欢在自定义realm里面做判断去比较用户名和密码,其实不需要,因为shiro会去帮我们比对,只需要按规则返回 SimpleAuthenticationInfo。如果账号或密码错误,会抛出UnknownAccountException(用户名异常)或IncorrectCredentialsException(密码)异常,我们只需要在全局异常里面捕获,返回我们的统一返回JSON即可。
网上代码如下:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("————身份认证方法————");
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 从数据库获取对应用户名密码的用户
String password = userMapper.getPassword(token.getUsername());
if (null == password) {
throw new AccountException("用户名不正确");
// 这里去比对了用户输入的密码与数据库密码做比较。完全不需要这样做!
// 这样做的好处是可以提前在这里控制抛出的异常吧,谁知道呢,根据需求吧。
} else if (!password.equals(new String((char[]) token.getCredentials()))) {
throw new AccountException("密码不正确");
}
return new SimpleAuthenticationInfo(token.getPrincipal(), password, getName());
}
4. SpringBoot整合Shiro实现登录
环境搭建链接:https://pan.baidu.com/s/19xGzU9ZGjutxeJkYdSE00g
提取码:rzkc
1. 添加依赖
// todo 本文其实讲得应该叫 spring整合shiro,而不是springboot整合shiro。 springboot对于shiro的整合并不是很多。只有少数改变。
// 推荐几个链接:sptingboot整合shiro
// 网上很多博文没有分清楚,springboot整合shiro 与 spring整合shiro。 注意看他们导入的依赖,是shiro-spring依赖的话,就和本文配置应该一样,若是shiro-spring-boot-web-starter依赖,配置会和本文有所区别。
https://blog.csdn.net/weixin_44730681/article/details/110518221
http://www.movcode.com/site/content/2019-11-22/42/1
https://www.hangge.com/blog/cache/detail_2684.html
https://zhuanlan.zhihu.com/p/570866449
https://www.cnblogs.com/Naylor/p/14990965.html
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId> 还有种 shiro-spring-boot-web-starter
<version>1.9.0</version>
</dependency>
区别:如果使用starter,shiro 已经默认开启了 @RequiresRoles, @RequiresPermissions 注解支持,不需要在shiroConfig 中配置了。所以,你不需要以下配置了:
/**
* 开启Shiro注解(如@RequiresRoles,@RequiresPermissions),
* 需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)
*/
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
/*
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
advisorAutoProxyCreator.setUsePrefix(true);
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
/**
* 开启aop注解支持
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
2. 创建自定义Realm
// SpringBoot里面整合shiro,自定义Realm是继承的 AuthorizingRealm。
// AuthorizingRealm 是继承 AuthenticatingRealm,在其基础上多了一个 doGetAuthorizationInfo() 方法用于授权
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
// 自定义授权方法
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
// 自定义登录认证方法
// 方法参数:AuthenticationToken 是个接口。
// 有2个方法:Object getPrincipal(); Object getCredentials();
// 有2个子接口:
// RememberMeAuthenticationToken(方法:boolean isRememberMe();)
// HostAuthenticationToken(方法:String getHost();)
// 这2个子接口,都有一个共同的实现类:public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken{}
// 方法返回值:AuthenticationInfo 是个接口,实例化他的实现类用于返回,通常用SimpleAuthenticationInfo实现类作为返回值。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1 获取身份信息 可以authenticationToken.getPrincipal, 也可以强转 (UsernamePasswordToken) authenticationToken,强转后通过 .getUserName
String name = token.getPrincipal().toString();
//2 调用业务层获取用户信息(数据库中)
User user = userService.getUserInfoByName(name);
//3 判断并将数据完成封装
if(user!=null){
// 如果密码使用了加密,那么创建shiro配置类的时候,自定义realm就需要告诉shiro,使用的是哪种加密。若对加密进行了散列,还需要告诉shiro散列次数。
AuthenticationInfo info = new SimpleAuthenticationInfo(token.getPrincipal(), user.getPwd(), ByteSource.Util.bytes("salt"), this.getName());
return info;
}
}
return null;
}
3. 创建shiro配置类
@Configuration
public class ShiroConfig {
// 1. 配置自定义Realm
@Bean(value = "customerRealm")
public Realm getRealm() {
CustomerRealm customerRealm = new CustomerRealm();
// 若指定了加密方式,就需要告诉shiro,你使用的哪种加密匹配器
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
// 设置加密为MD5
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
// 设置加密的散列次数
hashedCredentialsMatcher.setHashIterations(3);
customerRealm.setCredentialsMatcher(hashedCredentialsMatcher);
return customerRealm;
}
// 2. 配置security安全管理器
@Bean
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier(value = "customerRealm") Realm realm) {
DefaultWebSecurityManager webSecurityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
webSecurityManager.setRealm(realm);
return webSecurityManager;
}
// 3. 配置ShiroFilterFactoryBean。
// 它是用来创建和配置 Shiro 过滤器链的工厂 bean。用于对 Web 请求进行安全控制。
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(SecurityManager securityManager) {
// 设置应用程序SecurityManager实例以供构造的 Shiro 过滤器使用。这是一个必需的属性 - 设置失败将引发初始化异常
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// =================== factoryBean常用方法介绍 ===================
// setLoginUrl(String loginUrl)
// 未认证时的跳转页面,可以是一个jsp页面,也可以是一个RESTFul接口
// setUnauthorizedUrl(String unauthorizedUrl)
// 未授权时的跳转页面,可以是一个jsp页面,也可以是一个RESTFul接口
// setSuccessUrl(String successUrl)
// 登录成功的跳转页面,可以是一个jsp页面,也可以是一个RESTFul接口
// setFilters(java.util.Map<String, javax.servlet.Filter> filters) --- 设置添加过滤器到过滤器链。
// 这个方法接收一个 Map<String, Filter> 参数,其中 key 是过滤器的名称,value 是对应的过滤器实例。如果你想在 Shiro 过滤器链中加入一个自定
// 义的过滤器,你可以使用 setFilters 方法来将这个过滤器添加到过滤器链中。例如创建了一个名为 "myFilter" 的自定义过滤器 MyFilter,并将它添加
// 到了 Shiro 过滤器链中。这样,在 Web 请求经过 Shiro 过滤器链时,就会先经过MyFilter 这个过滤器的处理。
factoryBean.setLoginUrl("/user/toLogin");
factoryBean.setUnauthorizedUrl("/user/noPerms"); // TODO: 2022/8/29 注意,若我们是利用注解来分配权限的,则改行代码不会生效,这是因为 Shiro 的注解授权方式和过滤器链方式是两种不同的授权方式,它们的处理方式也不同。
// 使用注解授权时,若没权限,Shiro 会抛出 UnauthorizedException 异常,可以用全局异常进行捕获处理。
// setFilterChainDefinitionMap(java.util.Map<String, String> filterChainDefinitionMap) --- 设置过滤器链规则。
// 一个请求进入应用程序时,Shiro会根据定义的过滤器链来逐个执行过滤器
// anon:无需认证就可访问
// authc:必须认证才能访问
// user:必须拥有“记住我”功能才能使用(较少使用)
// perms: 拥有对某个资源的权限才能访问
// role: 拥有某个角色权限才能访问
// 注意:配置顺序有先后,拦截 /** 应该放在最后
Map<String, String> map = new LinkedHashMap<>();
map.put("/user/login", "anon");
map.put("/user/toLogin", "anon");
map.put("/user/noPerms", "anon");
map.put("/user/register", "anon");
map.put("/**", "authc");
factoryBean.setFilterChainDefinitionMap(map);
// factoryBean.setFilterChainDefinitions();
// 这个方法和setFilterChainDefinitionMap() 是一样的,就是传参的不一样。 factoryBean.setFilterChainDefinitions("/login = anon") 这种写法
return factoryBean;
}
/**
* 开启对 Shiro 注解的支持
* 注入 authorizationAttributeSourceAdvisor 和 defaultAdvisorAutoProxyCreator ,如果不注入这两个对象,RequiresRoles 和RequiresPermissions 注解将无法使用。
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
@Bean
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
/*
* setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
* 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
* 加入这项配置能解决这个bug
*/
advisorAutoProxyCreator.setUsePrefix(true);
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}
}
4. 控制器
@PostMapping("/login")
public MsgResult login(@RequestBody User user){
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(user.getUsername(), user.getPassword());
try {
// 调用login方法会去走 我们自定义的realm
subject.login(usernamePasswordToken);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("用户名错误");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
}
return MsgResult.success();
}
@GetMapping("/logout")
public MsgResult logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return MsgResult.success();
}
@PostMapping("/register")
public MsgResult register(@RequestBody User user) {
// 加盐 加散列次数
Md5Hash md5Hash = new Md5Hash(user.getPassword(), "salt", 3);
user.setPassword(md5Hash.toHex());
userService.register(user);
return MsgResult.success();
}
链接:https://pan.baidu.com/s/1f5tuYtimPerBrOTJOB0RUw
提取码:0yfu
5. Remember me功能
https://www.bilibili.com/video/BV11e4y1n7BH?p=17&vd_source=61b6fb4e547748656e36b17ee95125fb
6. 多Realm的认证实现
前言
:例如:用户名密码校验,手机号验证么校验等。我们就可以使用配置多个Realm校验,根据不同的登录方式,在不同的realm中做处理达到不同的校验。
https://liuyanzhao.com/9318.html
实现多realm,Shiro是使用ModularRealmAuthenticator实现的,会使用其内部的AuthenticationStrategy组件判断认证成功还是失败。
AuthenticationStrategy这个接口,有三个实现类:
FirstSuccessfulStrategy:第一个Realm验证成功,整体认证视为成功。
AtLeatOneSuccessfulStrategy:只要有一个Realm验证成功即可,和FirstSuccessfulStrategy不同,将
返回所有Realm身份校验成功的认证信息。
AllSuccessfulStrategy:所有Realm验证成功才算成功,且返回所有Realm身份认证成功的认证信息。
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier(value = "customerRealm") Realm realm, @Qualifier(value = "customerRealm2") Realm realm) {
//1、创建安全管理器对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//2、创建ModularRealmAuthenticator对象,并设置认证策略。
ModularRealmAuthenticator modularRealmAuthenticator=new ModularRealmAuthenticator();
modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
//3、封装Realm集合
List<Realm> myRealms=new ArrayList<>();
myRealms.add(customerRealm);
myRealms.add(customerRealm2);
//4、将myReal存入defaultWebSecurityManager中
defaultWebSecurityManager.setRealms(myRealms);
//5、返回
return defaultWebSecurityManager;
}
7. SpringBoot整合Shiro实现授权
触发权限判断的方式有2种:
1. 通过在页面使用与shiro相关的标签
2. 在接口服务中通过注解
通常单体项目,可以使用shiro提供的缓存,将权限进行缓存处理,以避免每次查询权限都去数据库查。否则,你可以自己用redis来做换缓存。
// PrincipalCollection 是一个接口,代表了当前已经通过身份认证的所有身份的集合。
// 常用方法:
// "isEmpty":检查集合是否为空的布尔值。
// "size":获取PrincipalCollection中Subject对象的数量。
// "getPrimaryPrincipal":获取PrincipalCollection中第一个Subject对象的主要身份信息。
// "asList":将PrincipalCollection转换为List。
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String username = principalCollection.getPrimaryPrincipal().toString();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
List<Role> roleList = roleService.findRolesByUsername(username);
if (!CollectionUtils.isEmpty(roleList)) {
for (Role role : roleList) {
// 给用户授予角色
simpleAuthorizationInfo.addRole(role.getName());
Integer roleId = role.getId();
List<Permission> permissionList = permissionService.findPermissionsByRoleId(roleId);
if (!CollectionUtils.isEmpty(permissionList)) {
for (Permission permission : permissionList) {
// 给用户授予权限
simpleAuthorizationInfo.addStringPermission(permission.getCode());
}
}
}
}
return simpleAuthorizationInfo;
}
踩过的坑——权限注解没生效:
<!-- 需加入这个依赖 -->
<!-- AOP依赖,必须,否则shiro权限拦截验证不生效 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
链接:https://pan.baidu.com/s/1_AucsJg-t0F_Fg-HPHHbZQ
提取码:jalr
8. 改造shiro登录为JWT
https://www.jb51.net/article/269288.htm
在分布式场景下,原生shiro会有一些问题:
1. session共享问题:shiro是使用session来存储认证后授权信息的,若多个服务实例之间不共享session,切换到另一个服务实例时,就需要重新登录。
2. 只有web端才有session,手机端没得。
2. 缓存数据不一致问题:首先,一般来说都会对权限做缓存,不要每次都从数据库查权限,shiro利用的是它提供的cash,使用的是本地缓存。其次,就算你每次都去数据库查,数据库一般也会做集群,各节点之间也许会存在数据不一致的问题,虽然还是能解决 O(∩_∩)O哈哈~ 反正一般都是会 在分布式场景下使用缓存来缓存授权信息,以提高系统的性能和响应速度。
JWT:是一个非常轻巧的规范。这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。
由三部分组成:头部,载荷和签名。
Header 头部: 通常由两部分组成:令牌的类型,即JWT,以及使用的签名算法,如HMAC SHA256或RSA。
Payload 负载: 有效负载,通信双方要交换的内容
Signature 签名/签证
一个完整的 Token 示例: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
利用JWT实现分布式场景下的登录:
规定每次请求时,需要在请求头中带上 token ,通过 token 检验权限,如没有,则说明当前为游客状态(或者是登陆 login 接口等)
//JWT的缺点:
无法撤销!JWT 一旦签发,就不能被撤销。就是说token没有过期前,你想让它提前停止使用,不好意思没得办法。
解决思路虽有但不是很完美。要么你就不解决JWT退出登录的问题。或者你在设置对token设置过期的时候,不要设置太久,设置1天,1天过后就失效。如果令牌被盗用,让攻击者只能攻击一段时间,而不是永久。
//shiro整合JWT,就是需要将jwt的拦截加入到shiro的过滤器里面。
1. 首先要编写一个符合shiro的Filter,能让shiro识别 像什么authc,anon,perms,port等 他们属于shiro预设的过滤器类,直接用。我们要自定义就 通过继承 BasicHttpAuthenticationFilter 来实现,一旦获取到有请求头,就交给自定义Realm去做处理
2. 自定义token,shiro里面也有token【UsernamePasswordToken他是存的账号和密码】,我们要自己搞一个token【存账号和JWT的token值】
3. shiro配置类中,需要将我们的Filter加入到shiro的过滤器链
4. 让shiro的所有请求,都走我们的Filter
5. 关闭shiro自带的session
6. 改造controller里面的 利用subject.login方法。 网上我看整合JWT都对登录做自己的处理,自己来比对账号密码。
//1. 编写自定义Filter
@Component
public class JWTFilter extends BasicHttpAuthenticationFilter {
/**
* @author lihao
* @date 2023/3/3 14:29
* @apiNote 判断请求头是否有token
* @return boolean
**/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("token");
return token != null;
}
/**
* @param request
* @param response
* @param mappedValue
* @return boolean true:放行 false:不放行,会进入onAccessDenied()方法
* @author lihao
* @date 2022/8/25 23:40
* @apiNote 请求进入拦截器后,会先调用该方法。
**/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
//判断请求的请求头是否带上 "token"
if (isLoginAttempt(request, response)) {
//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
//token 错误
e.printStackTrace();
//非法跳转页面 , 注意,responseError这个方法并不是BasicHttpAuthenticationFilter的方法,而是这段代码逻辑想在这里处理传了token时发生异常的问题,将处理逻辑放到了responseError方法的里面,如果不用该方法处理,当登录失败也会执行onAccessDenied方法。根据这段代码逻辑,若不带token,直接返回false,就去执行onAccessDenied。 若待了token,但是token不对或者已经失效,executeLogin执行后就会抛异常,就能执行responseError方法
responseError(response, e.getMessage());
}
}
return false;
}
/**
* @author lihao
* @date 2023/3/3 14:29
* @apiNote 执行登录的方法
* @return boolean
**/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("token");
String username = JWTUtil.getUsername(token);
JWTToken jwtToken = new JWTToken(username, token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
// Subject subject = SecurityUtils.getSubject();// 两种方式都可以获取 subject 对象
Subject subject = this.getSubject(request, response);
subject.login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return onLoginSuccess(jwtToken, subject, request, response);
}
/**
* token认证executeLogin成功后,进入此方法,可以进行token更新过期时间
*/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
System.out.println("token认证executeLogin成功后,进入此方法,可以进行token更新过期时间");
return true;
}
/**
* 将非法请求跳转到
*/
private void responseError(ServletResponse response, String message) {
try {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//设置编码,否则中文字符在重定向时会变为空字符串
message = URLEncoder.encode(message, "UTF-8");
httpServletResponse.sendRedirect("/notRole" + message);
} catch (IOException e) {
}
}
/**
* 直接json形式返回结果token验证失败信息
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
HttpServletRequest httpRequest = (HttpServletRequest) request;
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setHeader("Access-Control-Allow-Origin", httpRequest.getHeader("Origin"));
httpResponse.setCharacterEncoding("utf-8");
JSONObject jsonObject = new JSONObject();
jsonObject.put("status", 403);
jsonObject.put("msg", "请先登录");
httpResponse.getWriter().print(jsonObject.toString());
return false;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//2. 自定义token
public class JWTToken implements AuthenticationToken {
private String token;
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
public JWTToken(String token) {
this.token = token;
}
}
////////////////////////////////////////////////////////////////////////////////////////////////
//3. 将自定义Filter加入到shiro过滤器,并让所有请求都走自定义过滤器
Map<String, Filter> linkedHashMap = new LinkedHashMap<>();
LinkedHashMap.put("jwt", new JWTFilter()); // TODO: 2022/8/26 特别注意,这里设置的key,将用做下面一个todo的一个标识
factoryBean.setFilters(linkedHashMap); // 设置添加新的过滤器
Map<String, String> map = new LinkedHashMap<>();
map.put("/user/login", "anon");
map.put("/user/toLogin", "anon");
map.put("/user/noPerms", "anon");
map.put("/user/register", "anon");
map.put("/user/**", "jwt"); // TODO: 2022/8/26 原本shiro对于必须认证的请求,使用authc这种过滤器,因为现在我们要用jwt,所以,这里的value改成了 上面那个jwt的 key。
factoryBean.setFilterChainDefinitionMap(map); // 设置过滤器链规则
return factoryBean;
////////////////////////////////////////////////////////////////////////////////////////////////
//4. 调整自定义realm
// 用于替换Shiro的token为我们的JWT
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JWTToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String token = (String) token.getCredentials();
String username = null;
try {
//jwt验证token
boolean verify = JwtUtil.verify(credentials);
if (!verify) {
throw new AuthenticationException("Token校验不正确");
}
username = JwtUtil.getClaim(credentials, JwtUtil.ACCOUNT);
} catch (Exception e) {
throw new BusinessException(CommonResultStatus.TOKEN_CHECK_ERROR,e.getMessage());
}
//交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,不设置则使用默认的SimpleCredentialsMatcher
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
token, // 传token或者token.getPrincipal()
token.getCredentials(), // 只能传 token.getCredentials()
this.getName() //realm name
);
return authenticationInfo;
}
////////////////////////////////////////////////////////////////////////////////////////////////
//5. 关闭shiro自带的session。在整合Shiro和JWT时,一般建议关闭Shiro自带的Session,因为JWT作为一种无状态的认证方式,不需要保存用户的登录状态在服务端的Session中。
@Bean
public DefaultWebSecurityManager securityManager(JwtRealm jwtRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(jwtRealm);
//关闭shiro自带的session
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
///////////////////////////////////////////////////////////////////////////////////////////////////
两种登录都可以
//登录
//@PostMapping("/user/login")
//public MsgResult login(@RequestBody User user){
// String username = user.getUsername();
//
// LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
// wrapper.eq(User::getUsername, username);
// User one = userService.getOne(wrapper);
// if (one!=null) {
// String token = JwtUtil.sign(one.getUsername(), one.getPassword());
// return MsgResult.success(token);
// }
// return MsgResult.fail("用户名或密码错误");
//}
// 登录
@PostMapping("/user/login")
public MsgResult login(@RequestBody User user){
String token = JwtUtil.sign(user.getUsername(), user.getPassword());
Subject subject = SecurityUtils.getSubject();
subject.login(new JWTToken(token));
return MsgResult.success(token);
}
在将JWT与Shiro集成的情况下登录方式有两种:
一种方法是使用Shiro提供的Subject对象进行身份验证。
使用Shiro Subject进行身份验证可以利用Shiro提供的强大功能,例如支持不同的身份验证方法、支持会话管理、支持多个Realm等等。同时,Shiro可以管理用户的角色和权限,可以根据需要对用户进行细粒度的授权。
一种方法是手动从数据库中查询用户名和密码,并在验证成功后返回JWT token。
手动从数据库中查询用户名和密码并返回JWT token可以更加灵活,可以适应不同的身份验证流程,例如OAuth 2.0,可以与其他身份验证方案集成,例如LDAP、CAS等等。
此外,这种方法可以减少对Shiro的依赖,使应用程序更加轻量级和可移植。但是,需要注意的是,手动实现身份验证可能会增加安全风险,例如密码泄露、注入攻击等等。因此,需要谨慎处理用户输入,并采取适当的安全措施,例如密码哈希、防止SQL注入等等。
关于接口BasicHttpAuthenticationFilter中的方法执行顺序: // 来源于 chatgpt , 最好自行百度验证 百度搜索接口BasicHttpAuthenticationFilter中的方法执行顺序 //
BasicHttpAuthenticationFilter 是 Shiro 框架中提供的一种基于 HTTP 基础认证的过滤器,用于对 HTTP 请求进行身份验证。
以下是 BasicHttpAuthenticationFilter 中常用方法的执行顺序:
onAccessDenied() 方法,处理未认证的请求,该方法会在所有的 Shiro 过滤器执行之前被调用。
isLoginAttempt() 方法,判断当前请求是否是登录请求,如果是登录请求,则调用 executeLogin() 方法进行登录认证。
executeLogin() 方法,执行登录认证的具体逻辑,如果认证成功,则将认证信息存入 Subject 中。
createToken() 方法,根据当前请求中的用户名和密码创建一个 AuthenticationToken 对象。
getAuthzHeader() 方法,从请求中获取 Authorization 头。
createToken() 方法,根据获取到的 Authorization 头创建一个 AuthenticationToken 对象。
getSubject() 方法,从当前线程中获取 Subject 对象。
getRememberMeParam() 方法,获取记住我参数。
isRememberMe() 方法,判断是否开启记住我功能。
executeLogin() 方法,执行基于用户名密码的登录认证。
onLoginSuccess() 方法,登录成功后的回调方法。
onLoginFailure() 方法,登录失败后的回调方法。
需要注意的是,如果 onAccessDenied() 方法中没有对未认证的请求进行处理,那么执行顺序将从第 2 步开始。另外,如果开启了记住我功能,那么在登录成功后,Shiro 会自动将记住我信息存储到 cookie 中。
// 以上是 BasicHttpAuthenticationFilter 中常用方法的执行顺序,可以根据需要在相应方法中进行自定义操作。