Apache·Shiro是一个功能强大且易于使用的Java权限框架。shiro可以完成认证、授权、加密、会话管理、与web集成、缓存等。
shiro官网地址
1. 易于使用:使用市容构建系统安全框架简单,能快速掌握。
2. 全面:市容包含系统安全框架所需要的功能。
3. 灵活:可以在任何应用程序环境中工作。虽然可以在web,EJB和IOC环境下工作,但不需要依赖他们,它不强制要求任何规范。
4. 强力支持web:shiro具有出色的web应用程序支持,可以基于应用程序URl和web协议创建灵活的安全策略,同时还提供了一组jsp库来控制页面输出。
5. 兼容性强:shiro的设计模式使其易于与其他框架和应用程序(Spring、Grails等)集成。
1. Spring Security基于Spring开发,项目若使用Spring做基础,配合SpringSecurity更方便。
2. Spring Security功能,社区资源比shiro丰富
3. shiro配置和使用易于Spring Security
4. shiro依赖性低,可以不依赖任何框架和容器,独立运行,而Spring Security依赖Spring容器
1. Subject:应用代码直接交互的对象,代表了当前“用户”,即与当前应用交互的任何东西,而与Subject的所有交互会委托给SecurityManager。
2. SecurityManager:安全管理器,所有与安全有关的操作都会与SecurityManager交互,所有Subject由它管理,是shiro的核心,相当于SpringMVC中的DispatcherServlet。
3. Realm:shiro从Realm获取安全数据(如用户、角色、权限),当SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户,然后确定用户身份是否合法,也需要从Realm得到用户相应的角色权限进行验证用户是否能进行操作,可以把Realm看成DataSource。
✨授权,控制谁访问哪些资源
✨主体(Subject),访问应用的用户
✨资源(Resource),在应用中用户可以访问的URL
✨权限(Permission),在应用中用户能否访问某个资源,shiro支持用户模块的所有权限和某个用户的权限
✨角色(Role),权限的集合,一般情况下会赋予用户角色而非权限,而不同的角色会拥有一组不同的权限,赋权方便
编程式:通过if/else授权实现
if(subject.hasRole("admin")){
//有权限
}else{
//无权限
}
注解式:通过在执行的Java方法上放置相应的注解完成,没有权限将抛出异常
@RequiresRoles("admin")
public void hello(){
//有权限
}
JSP/GSP标签:在JSP/GSP页面通过相应的标签完成
<shiro:hasRole name="admin">
<!-有权限->
</shiro:hasRole>
先调用Subject.isPermitted/hasRole,它会委托给SecurityManager,SecurityManager接着委托给Authorizer
Authorizer是真正的授权者,若调用isPermitted(“user:view”),它会先通过PermisssionResolver把字符串转换成相应的Permission实例
Authorizer会判断Realm的角色是否和传入的角色匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,若匹配,则isPermitted返回true
md5是一个摘要算法, 任何长度得出的结果都是128位二进制数, 一般以16进制展示
public class MD5 {
public static void main(String[] args) {
String pw="z3";
Md5Hash md5Hash=new Md5Hash(pw);
System.out.println("直接加密:"+md5Hash.toHex());
Md5Hash md5Hash1=new Md5Hash(pw,"salt");
System.out.println("带盐加密:"+md5Hash1.toHex());
Md5Hash md5Hash2=new Md5Hash(pw,"salt",3);
System.out.println("带盐的三次加密:"+md5Hash2.toHex());
SimpleHash simpleHash=new SimpleHash("MD5",pw,"salt",3);//可以指定加密方式
System.out.println("Md5Hash的父类加密:"+simpleHash.toHex());
}
}
✨ 1. 收集用户身份/凭证,如用户名、密码
✨ 2. 调用Subject.login进行登录,失败将得到相应异常,并提示用户错误信息
✨ 3. 创建自定义Realm类,继承org.apache.shiro.realm.AuthorizingRealm类,实现doGetAuthenticationInfo()方法
shiro默认的登录认证不加密,若想实现加密认证需要自定义登录认证,自定义Realm。
//相关依赖,若导入失败可切换版本
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies>
//自定义的Realm
public class MyRealm extends AuthenticatingRealm {
//shiro的login方法底层会调用该方法进行认证,该方法只获取需要认证的信息,认证逻辑仍需shiro的低层认证逻辑完成
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取身份信息
String s = authenticationToken.getPrincipal().toString();
//获取凭证信息
String password = new String((char[]) authenticationToken.getCredentials());
//获取数据库中数据
if (s.equals("zhangsan")){
String pwd="7174f64b13022acd3c56e2781e098a5f";
//创建封装要校验逻辑的对象,封装数据返回
AuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),pwd,
ByteSource.Util.bytes("salt"),authenticationToken.getPrincipal().toString());
return authenticationInfo;
}
return null;
}
}
//要让自定义Realm生效,需要在ini配置文件或springboot中进行配置
[main]
md5CredentialsMatcher=org.apache.shiro.authc.credential.Md5CredentialsMatcher
md5CredentialsMatcher.hashIterations=3
;指定自定义Ream的路径
myrealm=com.qjy.shiro.MyRealm
myrealm.credentialsMatcher=$md5CredentialsMatcher
securityManager.realms=$myrealm
[users]
zhangsan=7174f64b13022acd3c56e2781e098a5f,role1,role2
lisi=l4
[roles]
role1=user:insert,user:select
//登录认证
public class ShiroRun {
public static void main(String[] args) {
//1初始化获取SecurityManager
IniSecurityManagerFactory iniSecurityManagerFactory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager instance = iniSecurityManagerFactory.getInstance();//获取相关实例,拿到认证管理
SecurityUtils.setSecurityManager(instance);//将认证管理器存到工具
//2通过工具获取Subject对象
Subject subject = SecurityUtils.getSubject();
//3创建token对象,web应用用户名密码从页面传递
AuthenticationToken token = new UsernamePasswordToken("zhangsan", "z3");
//4完成登录
try {
subject.login(token);
boolean role1 = subject.hasRole("role1");
System.out.println("登录成功");
System.out.println("是否有该角色:"+role1);
boolean permitted = subject.isPermitted("user:insert");
System.out.println("是否有该权限:"+permitted);
} catch (UnknownAccountException e) {
e.printStackTrace();
System.out.println("该用户不存在");
} catch (IncorrectCredentialsException e) {
e.printStackTrace();
System.out.println("密码错误");
} catch (AuthenticationException e) {
e.printStackTrace();
}
}
}
返回false正常,因为这时候doGetAuthorizationInfo里没有给权限,权限和角色不是从ini取的(弹幕是个好东西)
先搭建好环境,建好数据库
//相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
# yml配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
spring:
datasource:
url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
//controller层
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/login")
public String userLogin(String name, String pwd) {
//获取subject对象
Subject subject = SecurityUtils.getSubject();
//封装请求的数据到token
AuthenticationToken token = new UsernamePasswordToken(name, pwd);
//调用login方法进行验证
try {
subject.login(token);
return "登录成功";
} catch (AuthenticationException e) {
e.printStackTrace();
System.out.println("登录失败");
return "登录失败";
}
}
}
shiro内置过滤器详解
//shiro的过滤拦截
@Configuration
public class ShiroConfig {
@Resource
private MyRealm myRealm;
//配置SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
//创建DefaultWebSecurityManager对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");//表示采用md5加密
matcher.setHashIterations(3);//设置加密次数
//将加密对象储存在MyRealm
myRealm.setCredentialsMatcher(matcher);
//将MyRealm储存在DefaultWebSecurityManager对象
defaultWebSecurityManager.setRealm(myRealm);
return defaultWebSecurityManager;
}
//配置shiro内置过滤器拦截
@Bean
public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
//设置不认证即可访问的资源
definition.addPathDefinition("/user/login","anon");
//设置需要登录认证的拦截范围
definition.addPathDefinition("/**","authc");
return definition;
}
@Bean
public DefaultWebSecurityManager securityManager(MyRealm myRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
// [重点]解决报错 org.apache.shiro.UnavailableSecurityManagerException
ThreadContext.bind(securityManager);
return securityManager;
}
}
✨最后一个bean注入可以解决【Exception】org.apache.shiro.UnavailableSecurityManagerException(解决问题的文章)
//自定义的shiro
@Component
public class MyRealm extends AuthorizingRealm {
@Resource
private IUserService 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.getByName(name);
//非空判断,将数据封装返回
if (user!=null){
AuthenticationInfo authenticationInfo=new SimpleAuthenticationInfo(
authenticationToken.getPrincipal(),user.getPwd(),
ByteSource.Util.bytes("salt"),
authenticationToken.getCredentials().toString()
);
return authenticationInfo;
}
return null;
}
}
//即查询数据库
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
@Resource
private UserMapper userMapper;
@Override
public User getByName(String name) {
LambdaQueryWrapper<User> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(User::getName,name);
System.out.println();
return userMapper.selectOne(queryWrapper);
}
}
当应用程序配置多个realm时(r如账号密码校验,手机号校验等),shiro的ModularRealmAuthenticator会使用内部的AuthenticationStrategy判断认定成功还是失败
AuthenticationStrategy是一个无状态组件组件,他在身份验证中被询问四次(四次交互需要的必要状态都将被作为方法参数):
✨在所有realm被调用之前
✨在调用realm的getAuthenticationInfo方法之前
✨在调用realm的getAuthenticationInfo方法之后
✨在所有realm被调用之后
认证策略的另外一项工作就是聚合所有realm的结果信息封装至AuthenticInfo实例中,并将此信息返回,以此作为Subject的身份信息
//代码实现
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
//创建DefaultWebSecurityManager对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//创建认证对象,并设置认证策略
ModularRealmAuthenticator modularRealmAuthenticator=new ModularRealmAuthenticator();
//认证方式需全部realm都成功
modularRealmAuthenticator.setAuthenticationStrategy(new AllSuccessfulStrategy());
defaultWebSecurityManager.setAuthenticator(modularRealmAuthenticator);
//封装myRealm集合
List<Realm> list=new ArrayList<>();
list.add(myRealm1);
list.add(myRealm2);
//将myRealm存入defaultWebSecurityManager对象
defaultWebSecurityManager.setRealms(list);
return defaultWebSecurityManager;
}
即再次访问相同资源时可以无需登录
流程:
✨首先在登录页面选中rememberMe,然后登录成功,如果是浏览器登录,一般会把RememberMe的cookie写到客户端并保存
✨关闭浏览器再打开,浏览器还会记住你
✨访问一般的网页,仍能识别你,且能正常访问
✨如果在电商平台进行支付时,仍需查验身份
//shiroConfiguration添加的相关配置
//配置SecurityManager
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager() {
//创建DefaultWebSecurityManager对象
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
//创建加密对象,设置相关属性
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("md5");//表示采用md5加密
matcher.setHashIterations(3);//设置加密次数
//将加密对象储存在MyRealm
myRealm.setCredentialsMatcher(matcher);
//将MyRealm储存在DefaultWebSecurityManager对象
defaultWebSecurityManager.setRealm(myRealm);
//设置rememberMe
defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
return defaultWebSecurityManager;
}
//cookie属性设置
public SimpleCookie rememberMeCookie(){
SimpleCookie cookie=new SimpleCookie("rememberMe");
//设置跨域
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setMaxAge(30*24*60*60);
return cookie;
}
public CookieRememberMeManager rememberMeManager(){
CookieRememberMeManager meManager=new CookieRememberMeManager();
meManager.setCookie(rememberMeCookie());
//注意 setCipherKey的参数必须是16位,否则后面rememberMe不成功
meManager.setCipherKey("1234567890987654".getBytes());
return meManager;
}
//配置shiro内置过滤器拦截
@Bean
public DefaultShiroFilterChainDefinition defaultShiroFilterChainDefinition(){
DefaultShiroFilterChainDefinition definition = new DefaultShiroFilterChainDefinition();
//设置不认证即可访问的资源
definition.addPathDefinition("/user/login","anon");
//设置需要登录认证的拦截范围
definition.addPathDefinition("/**","authc");
//添加存在用户的过滤器(rememberMe)
definition.addPathDefinition("/**","user");
return definition;
}
✨在登录方法中添加参数**@RequestParam(defaultValue = “false”) boolean rememberMe**,通过前端传过来的值判断是否“记住我”
✨登出,在前端设置相关按钮,在shiroConfig设置拦截
definition.addPathDefinition(“/logout”,“logout”);
用户登录成功后,需要验证其具有的的权限,而shiro提供了Realm的doGetAuthorizationInfo方法进行判断,触发显现判断的两个方法:
✨在页面属性通过shiro:*****(标签)属性判断
✨在接口服务通过注解@Requires****(关键字)进行判断
验证用户是否登录=等同于subject.isAuthenticated()(成功返回true)
@RequiresAuthentication
验证用户是否被记忆=subject.isRemembered()(被记忆返回true)
@RequiresUser
验证是否是一个guest请求,此时subject.getPrincipal()为空
@RequiresGuest
验证subject是否有相应角色,有则访问,无则抛异常AuthorzationException
验证subject是否有相应权限,有则访问,无则抛异常AuthorzationException
✨先在数据库中建立相关表
//mapper层查询数据库
@Select("select role FROM roles in id=(SELECT rId FROM role_user WHERE uId=(SELECT id FROM `user`WHERE name=#{name}));")
List<String> getRoles(@Param(value = "name")String name);
//service层调用
public List<String> getRoles(String name) {
return userMapper.getRoles(name);
}
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
//创建对象
SimpleAuthorizationInfo authorizationInfo=new SimpleAuthorizationInfo();
String s = principalCollection.getPrimaryPrincipal().toString();
List<String> roles = userService.getRoles(s);
//储存角色
authorizationInfo.addRoles(roles);
//返回信息
return authorizationInfo;
}
与获取角色信息验证类似,只不过自定义授权方法中通过authorizationInfo.addStringPermissions();方法接收保存权限信息的list
EhCache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,JavaEE和轻量级容器。可以和大部分Java 项目整合,它支持内存和磁盘存储,默认存储在内存中,内存不够时可把数据同步到磁盘中。EhCache支持基于Filter的Cache实现,也支持Gzip压缩算法。
EhCache直接在JVM虚拟机中缓存,速度快,效率高
EhCache缺点时缓存共享麻烦,集群分布式应用使用不方便。
//相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
✨在resource中添加ehcache配置{相关配置),也可自行在网上查找
//缓存管理器
public EhCacheManager getCacheManager() {
EhCacheManager manager = new EhCacheManager();
InputStream is=null;
try {
//通过该路径获取数据流
is= ResourceUtils.getInputStreamForPath("classpath:ehcache/ehcache-shiro.xml");
} catch (IOException e) {
throw new RuntimeException(e);
}
//如果存在错误可能是导包错误
CacheManager cacheManager = new CacheManager(is);
manager.setCacheManager(cacheManager);
return manager;
}
✨将该manager利用setCacheManager方法配置到SecurityManager中
会话管理器,负责创建和管理用户的会话(Session)生命周期,它能够在任何环境中在本地管理用户会话,即使没有Web/Servlet/EJB容器,也一样可以保存会话。默认情况下,Shiro会检测当前环境中现有的会话机制(比如Servlet容器)进行适配,如果没有(比如独立应用程序或者非Web环境),它将会使用内置的企业会话管理器来提供相应的会话管理服务,其中还涉及一个名为SessionDAO的对象。SessionDAO负责Session的持久化操作(CRUD),允许Session数据写入到后端持久化数据库。
SessionManager由SecurityManager管理。shiro提供了三种实现:
✨session的实现
Session session=SecurityUtils.getSubject().getSession();
session.setAttribute(“key”,“value”)
✨说明
Controller中的request,在shiro.过滤器中的doFilerInternal方法,被包装成ShiroHttpServletRequest。
SecurityManager和SessionManager.会话管理器决定session来源于ServletReques.t还是由Shiro管理的会话。
无论是通过request.getSession或 subject.getSession获取到session,操作session,两者都是等价的。e