Shiro
一、基本功能
- Authentication:身份认证/登录,用于验证用户是不是拥有相应的身份。
- Authorization:授权,验证某个已认证的用户是否拥有某个权限。
- Session Manager:会话管理,既用户登录后就是一次会话,没有退出之前,所有的信息都在会话中。会话管理所包含的功能有:
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储。
- Web Support:Web支持,可以非常容易的集成到Web环境。
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次都去查询,这样可以提高效率。
- Concurrency:shiro支持多线程应用的并发验证,即在一个线程中开启另一个线程,可以把权限自动传播过去。
- Testing:提供测试支持。
- Rum As:允许一个用户假装另一个用户的身份进行访问。
- Remember Me:记住我。
1.shiro架构
- Subject:主体,外部应用于subject进行交互,subject记录当前操作用户。它在shiro中是一个接口,定义了很多认证授权相关的方法,外部程序通过subject进行认证授权,而subject是通过SecurityManager安全管理器进行认证授权。
- SecurityManger:安全管理器,对全部的subject进行安全管理,是shiro的核心。实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManger进行会话管理。它是一个接口,继承了Authenticator,Authorizer,SessionManger这三个接口。
- Authenticator:认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器。
- Authorizer:授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限。
- realm:领域,相当于datasource数据源,securityManger进行安全认证需要通过realm获取用户权限数据。
注意:不要把realm理解成只是从数据源取数据,在realm中还有认证授权校验的相关的代码。
- SessionManger:会话管理,shiro框架定义了一套会话管理,它不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录。
- SessionDAO:会话dao,是对session会话操作的一套接口,比如要将session存储到数据库,可以通过jdbc将会话存储到数据库。
二、shiro认证
- 身份验证:用户需要提供principals(身份)、credentials(证明),从而应用能验证用户身份。
- principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个principals,但只有一个Primary principals,一般是用户名/密码/手机号。
- credentials:证明/凭证,即只有主体知道的安全值,如密码/数字证书等。
注意:最常见的principals和credentials组合就是用户名、密码。
1.认证流程
1.1导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
1.2shiro.ini
用于创建securityManager工厂,用于模拟真实用户。
#配置数据信息,后面肯定数据存到数据库,从数据库读取
#配置用户
[users]
#账号=密码
zhangsna=zs123
admin=admin123
1.3主要代码
public void shiroTest(){
//1.读取配置文件,创建安全管理器SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.createInstance();
//将安全管理器设置到当前线程
SecurityUtils.setSecurityManager(securityManager);
//从当前线程创建主体对象 Subject
Subject subject = SecurityUtils.getSubject();
//创建Token封装身份凭证(账号密码)web时是表单传输过来的
AuthenticationToken token = new UsernamePasswordToken("admin", "admin123");
//获取认证(登录)状态
boolean authenticated = subject.isAuthenticated();
System.out.println("是否通过认证:"+authenticated);//false
if(!authenticated){
try {
subject.login(token);
}catch (UnknownAccountException e){
System.out.println("账号不存在!");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误!");
}
}
//再次获取认证状态
authenticated=subject.isAuthenticated();
System.out.println("现在是否通过认证:"+authenticated);//ture
//获取认证信息
Object principal = subject.getPrincipal();
System.out.println(principal);//admin
//退出登录
subject.logout();
//再次获取认证状态
authenticated = subject.isAuthenticated();
System.out.println("退出后的认证状态 :"+authenticated);//false
}
2.自定义realm
- 自定义的realm一般会实现AuthorizingRealm。
2.1shiro.ini
#配置自定义信息
[main]
#创建自定义Realm(类似于Spring IOC)
customRealm= com.jutixueyuan.realm.CustomRealm
#将自定义Realm设置给安全管理器 (类似于Spring DI)
securityManager.realm=$customRealm
2.2 自定义realm代码
public class CustomRealm extends AuthorizingRealm {
/**
* 获取授权信息方法:开发者在此方法中获取数据库中当前认证通过的权限信息方法
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 获取认证信息方法:开发在此方法中完成调用数据库查询出对应的用户身份信息的方法
* @param token 带有账号密码token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.获取token中的身份(账号)
String username = (String) token.getPrincipal();
//2.模拟数据库查询账号
List<String> users = Arrays.asList("admin","lucy","lili","jack","rose");
System.out.println("username :"+username);
//数据库账号不存在
if(!users.contains(username)){
return null;
}
//数据库账号存在,数据库中数据查询出来封装对象
User user = new User(username, "admin123");
//3. 创建认证信息对象
String credentials = user.getPassword();
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, credentials, this.getName());
return simpleAuthenticationInfo;
}
}
2.3测试代码
public void shiroTest(){
//1.读取配置文件,创建安全管理器SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.createInstance();
//将安全管理器设置到当前线程
SecurityUtils.setSecurityManager(securityManager);
//从当前线程创建主体对象 Subject
Subject subject = SecurityUtils.getSubject();
//创建Token封装身份凭证(账号密码)web时是表单传输过来的
AuthenticationToken token = new UsernamePasswordToken("admin", "admin123");
//获取认证(登录)状态
boolean authenticated = subject.isAuthenticated();
System.out.println("是否通过认证:"+authenticated);
if(!authenticated){
try {
subject.login(token);
}catch (UnknownAccountException e){
System.out.println("账号不存在!");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误!");
}
}
//再次获取认证状态
authenticated=subject.isAuthenticated();
System.out.println("现在是否通过认证:"+authenticated);
//获取认证信息
Object principal = subject.getPrincipal();
System.out.println("认证身份"+principal);
//退出登录
subject.logout();
//再次获取认证状态
authenticated = subject.isAuthenticated();
System.out.println("退出后的认证状态 :"+authenticated);
}
3.加密算法
- 对密码进行加密,常用的加密方式有md5、sha1。都存在弱点,建议使用SHA2或SHA3加密算法。
- 使用时:在程序中对原始密码+盐(随机数)+加密次数进行散列,将散列值存储到数据库中,并且还要将盐和加密次数存储到数据库中。
3.1两种加密算法代码
// 原始密码+盐+加密多次
Md5Hash md5Hash=new Md5Hash(password,salt,hashIteration);
System.out.println(md5Hash);//95251496c342ca39f21c11dee6845ef4
//SHA系列加密
Sha256Hash sha256Hash = new Sha256Hash(password, salt, hashIteration);
System.out.println(sha256Hash.toString());
3.2shiro.ini
#配置自定义信息
[main]
#创建自定义Realm(类似于Spring IOC)
customRealm= com.jutixueyuan.realm.CustomRealm
#创建凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#设置算法和加密次数
credentialsMatcher.hashAlgorithmName=MD5
#设置加密次数
credentialsMatcher.hashIterations=3
#设置自定Realm的凭证匹配器
customRealm.credentialsMatcher=$credentialsMatcher
#将自定义Realm设置给安全管理器 (类似于Spring DI)<property name="realm" ref="customRealm">
securityManager.realm=$customRealm
3.3自定义realm
public class CustomRealm extends AuthorizingRealm {
/**
* 获取授权信息方法:开发者在此方法中获取数据库中当前认证通过的权限信息方法
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
/**
* 获取认证信息方法:开发在此方法中完成调用数据库查询出对应的用户身份信息的方法
* @param token 带有账号密码token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.获取token中的身份(账号)
String username = (String) token.getPrincipal();
//2.模拟数据库查询账号
List<String> users = Arrays.asList("admin","lucy","lili","jack","rose");
System.out.println("username :"+username);
//数据库账号不存在
if(!users.contains(username)){
return null;
}
//数据库账号存在,数据库中数据查询出来封装对象
User user = new User(username, "95251496c342ca39f21c11dee6845ef4","qazw");
//3. 创建认证信息对象
String credentials = user.getPassword();
//获取User的盐
String salt = user.getSalt();
ByteSource credentialSalt = ByteSource.Util.bytes(salt);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, credentials, credentialSalt,this.getName());
return simpleAuthenticationInfo;
}
}
三、shiro授权
- 关键对象:主体(subject)、资源(resource)、权限(permission)、角色(role)。
1.三种授权方式
- 编程式:通过写if/else授权代码块完成
Subject subject = SecurityUtils.getSubject();
if(subject.hasRole("admin")) {
//有权限
} else {
//无权限
}
- 注解式:通过在执行的java方法上放置相应的注解完成
@RequiresPermissions("admin:list")
public void list() {
System.out.println("有权限才能执行此方法");
}
- jsp标签:在jsp页面通过相应的标签完成
<shiro:hasRole name="admin">
执行代码
</shiro:hasRole>
2.授权流程
2.1shiro.ini代码
#配置数据信息,后面肯定数据存到数据库,从数据库读取
#配置用户
[users]
#账号=密码,角色1,角色2,角色N 理论上一个用户可以拥有多个角色,实际一般一个用户一个角色
zhangsna=zs123,role1
admin1=admin123,admin
lisi=lisi123,role2
#配置角色
[roles]
#语法 角色名称=权限表达式
#权限表达式语法
# 1. * 星号 表示当前角色拥有所有的权限,超级管理员级别
# 2. 模块:* 用户项目具体某一个模块的所有权限
# 如 : user:* ,teacher:*
# 3. 模块:操作 具体某一个功能对应的权限
# 如 : user:list,user:insert ,student:list,student:update
#注意: 模块:操作 中间风格符必须是 冒号: 官方推荐语法
# 一般表达式语法 模块:insert/update/delete/list 增删改查
admin=*
role1=user:*
role2=user:list
2.2授权代码
public void shiroTest(){
//1.读取配置文件,创建安全管理器SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.createInstance();
//将安全管理器设置到当前线程
SecurityUtils.setSecurityManager(securityManager);
//从当前线程创建主体对象 Subject
Subject subject = SecurityUtils.getSubject();
//创建Token封装身份凭证(账号密码)web时是表单传输过来的
AuthenticationToken token = new UsernamePasswordToken("lisi", "lisi123");
//获取认证(登录)状态
boolean authenticated = subject.isAuthenticated();
System.out.println("是否通过认证:"+authenticated);
if(!authenticated){
try {
subject.login(token);
}catch (UnknownAccountException e){
System.out.println("账号不存在!");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误!");
}
}
//再次获取认证状态
authenticated=subject.isAuthenticated();
System.out.println("现在是否通过认证:"+authenticated);
/**
*
* 如何判断当前用户是否拥有某个资源的权限
*
* 1,通过角色名称判断 (不推荐),角色权限随时可以变的
*
* 2,通过权限表达式来判断(推荐),因为一个资源对应的权限表示一旦确定,永远都不会改变
*
*
*/
boolean hasRole = subject.hasRole("admin");
if(hasRole){
System.out.println("拥有admin角色");
}else{
System.out.println("没有admin角色");
}
boolean permitted = subject.isPermitted("user:list");
if(permitted){
System.out.println("执行用户查询操作...");
}
//获取认证信息
Object principal = subject.getPrincipal();
System.out.println(principal);
//退出登录
subject.logout();
//再次获取认证状态
authenticated = subject.isAuthenticated();
System.out.println("退出后的认证状态 :"+authenticated);
}
3.自定义realm进行授权
3.1shiro.ini代码
#配置自定义信息
[main]
#创建自定义Realm(类似于Spring IOC)
customRealm= com.jutixueyuan.realm.CustomRealm
#创建凭证匹配器
credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher
#设置算法和加密次数
credentialsMatcher.hashAlgorithmName=MD5
#设置加密次数
credentialsMatcher.hashIterations=3
#设置自定Realm的凭证匹配器
customRealm.credentialsMatcher=$credentialsMatcher
#将自定义Realm设置给安全管理器 (类似于Spring DI)<property name="realm" ref="customRealm">
securityManager.realm=$customRealm
3.2自定义的realm
- 修改自定义realm中的doGetAuthorizationInfo()方法。
public class CustomRealm extends AuthorizingRealm {
/**
* 获取授权信息方法:开发者在此方法中获取数据库中当前认证通过的权限信息方法
*
* @param principals 认证通过的身份信息
* @return
* 获取授权信息的思路
* 1.获取认证通过的主身份,User对象
* User user = (User) principals.getPrimaryPrincipal();
* 2.根据用户的角色id,查询此角色对应的权限表中的权限表达式
* 2.1 根据角色id查询角色权限中间此角色id对应的权限id
* 1,2,3,4 对应权限id
* 再根据权限id查询此角色对应的权限表达式
* user:list
* user:insert
* user:delete
* user:update
*3.创建授权信息对象
* 将当前用户对应的角色对应的权限表达式设置给Shiro框架对象AuthorizationInfo
*4.Shiro框架底层将用户入口要判断的权限表达式和当前用户的权限表达式进行对比
*
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
System.out.println("PrimaryPrincipal:"+user);
//模拟从数据库中查询出对应的权限表达式
List<String> permissions = new ArrayList<>();
permissions.add("user:list");
permissions.add("user:insert");
permissions.add("user:delete");
permissions.add("user:update");
//创建授权信息对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissions);
return simpleAuthorizationInfo;
}
/**
* 获取认证信息方法:开发在此方法中完成调用数据库查询出对应的用户身份信息的方法
* @param token 带有账号密码token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.获取token中的身份(账号)
String username = (String) token.getPrincipal();
//2.模拟数据库查询账号
List<String> users = Arrays.asList("admin","lucy","lili","jack","rose");
System.out.println("username :"+username);
//数据库账号不存在
if(!users.contains(username)){
return null;
}
//数据库账号存在,数据库中数据查询出来封装对象
User user = new User(username, "95251496c342ca39f21c11dee6845ef4","qazw");
//3. 创建认证信息对象
String credentials = user.getPassword();
//获取User的盐
String salt = user.getSalt();
ByteSource credentialSalt = ByteSource.Util.bytes(salt);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, credentials, credentialSalt,this.getName());
return simpleAuthenticationInfo;
}
}
3.3测试代码
public void shiroTest(){
//1.读取配置文件,创建安全管理器SecurityManager
IniSecurityManagerFactory factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.createInstance();
//将安全管理器设置到当前线程
SecurityUtils.setSecurityManager(securityManager);
//从当前线程创建主体对象 Subject
Subject subject = SecurityUtils.getSubject();
//创建Token封装身份凭证(账号密码)web时是表单传输过来的
AuthenticationToken token = new UsernamePasswordToken("admin", "admin123");
//获取认证(登录)状态
boolean authenticated = subject.isAuthenticated();
System.out.println("是否通过认证:"+authenticated);
if(!authenticated){
try {
subject.login(token);
}catch (UnknownAccountException e){
System.out.println("账号不存在!");
}catch (IncorrectCredentialsException e){
System.out.println("密码错误!");
}
}
//再次获取认证状态
authenticated=subject.isAuthenticated();
System.out.println("现在是否通过认证:"+authenticated);
//判断用户是否拥有某一个权限
boolean userInsert = subject.isPermitted("user:list");
System.out.println("用户做新增功能"+userInsert);
//获取认证信息
Object principal = subject.getPrincipal();
System.out.println("认证身份"+principal);
//退出登录
subject.logout();
//再次获取认证状态
authenticated = subject.isAuthenticated();
System.out.println("退出后的认证状态 :"+authenticated);
}
四、shiro与SpringBoot集成
4.1shiro的过滤器
- web项目的所有请求都会经过shiro过滤器,shiro框架针对不同的资源进行了分类(11个过滤器)。
- 配置从上到下,访问资源只要匹配到某一个过滤器后,就不会匹配后面过滤器,匹配精度越高一定越要配置上面。
过滤器使用别名 | 过滤器类型 |
anon | |
authc | |
authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
logout | |
noSessionCreation | |
perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
port | |
rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
roles | |
ssl | |
user |
说明:
- anon:表示可以匿名使用。通过此过滤器的资源不需要认证,直接放行,所有静态资源不要认证的资源设置此过滤器即可。
- authc:表示需要认证才能使用。
- logout:退出认证过滤,匹配到此过滤器会自动清除当前认证的cookie和session的数据,并默认重定向跳转到项目的根路径:/
4.2shiro标签库
4.2.1在Thymeleaf页面引入shiro标签库
<html xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
4.2.2标签的介绍
标签名称 | 标签条件(均是显示标签内容) |
| 登录之后 |
| 不在登录状态时 |
| 用户在没有RememberMe时 |
| 用户在RememberMe时 |
<shiro:hasAnyRoles name=“abc,123” > | 在有abc或者123角色时 |
<shiro:hasRole name=“abc”> | 拥有角色abc |
<shiro:lacksRole name=“abc”> | 没有角色abc |
<shiro:hasPermission name=“abc”> | 拥有权限资源abc |
<shiro:lacksPermission name=“abc”> | 没有abc权限资源 |
| 显示用户身份名称 |
<shiro:principal property=“username”/> | 显示用户身份中的属性值 |
4.3引入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.5.0</version>
</dependency>
4.4shiroConfig代码
@Configuration
public class ShiroConfig {
/*配置Shiro方言,让Thymeleaf支持Shiro标签*/
@Bean
public ShiroDialect shiroDialect() {
return new ShiroDialect();
}
//自定义退出认证过滤器,不是IOC
public LogoutFilter logoutFilter(){
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/user/loginPage");
return logoutFilter;
}
//自定义表单认证过滤器
public MyFormAuthenticationFilter myFormAuthenticationFilter(){
return new MyFormAuthenticationFilter();
}
/**
* 创建Shiro框架和过滤器
* ShiroFilterFactoryBean 工厂Bean,Spring会自动执行 getObject方法,底层返回:AbstractShiroFilter
*
* 就是 一个 javax.servlet.Filter
*
* 注意: 方法名只能叫做 shiroFilterFactoryBean
*
* @return
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filters=new HashMap<>();
//把开发者自定义的过滤器作为logout过滤器
filters.put("logout",logoutFilter());
//把开发者自定义过滤器作为authc过滤器
filters.put("authc",myFormAuthenticationFilter());
//配置开发者自定义过滤器
shiroFilterFactoryBean.setFilters(filters);
//1.配置认证成功跳转到页面首页,如果Shiro有记录最后一次认证前的访问地址,此配置就会失效,认证成功以后会跳转到最后一次认证前记录的地址
shiroFilterFactoryBean.setSuccessUrl("/index");
//2.配置认证失败跳转的页面,跳转登录失败页面
shiroFilterFactoryBean.setLoginUrl("/user/loginError");
//3.配置认证成功以后强制访问没有权限的页面,跳转到提示页面 : 对注解配置权限无效
//shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized.html");
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager());
/**
* 配置Shiro框架的过滤器链
*
* 1.Shiro 框架会过滤用户的所有请求:包括css,js,image....
*
* 这些资源Shiro根据不同的情况进行分类
*
* 第一类:静态资源一般直接放行
* 如:css,js,image
* 第二类:需要认证但是不需要权限的资源
* 如:后台首页
* 第三类:既要认证也需要权限访问资源
* 如:员工管理,部门管理....
*
* Shiro就根据这些不同的情况设置不同过滤,不同的资源配置不同过滤器即可
* 总共设计了11个过滤器
*
* 常见的过滤器
* 别名 过滤器全限定名
* 1 anon org.apache.shiro.web.filter.authc.AnonymousFilter
* 匿名过滤器,通过此过滤器的资源不需要认证,直接放行,所有静态资源不要认证的资源就设置此过滤器即可
* 2 authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter
* 表单认证过滤器
* 匹配到此过滤器的资源必须是认证通过
* 1,如果用户访问是认证页面(登录操作),并且post(必须)方式提交,
* 此过滤器就开始认证操作,调用自定CustomRealm完成认证过滤
* 2,如果用户访问一个项目其他非认证(登录)页面匹配到,Shiro框架自动从Session获取认证信息
* 有:说明认证过,直接放行
* 没有:说明没有认证,跳转认证失败页面
* 3 logout org.apache.shiro.web.filter.authc.LogoutFilter
* 退出认证过滤,匹配到此过滤器会自动清除当前认证的Cookie和Session中的数据
* 并且默认重定向跳转到项目的根路径 : /
* 4 perms org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
* 权限授权过滤器,噢诶之资源访问需要权限的资源的过滤器
* 使用语法
*
* map.put("资源",perms[权限表达式])
* 如:
* map.put("/user/userPage","perms[user:userPage]")
* 当用户认证通过以后,再访问此页面时候会去执行授权逻辑,判断用户是否拥有对应资源的权限表达式
* 此种方式缺点:如果项目需要配置权限资源很多,需要配置大量代码,非常繁琐,难以维护管理配置代码
*
* 解决方案:使用注解分开方式替代
* 5 user org.apache.shiro.web.filter.authc.UserFilter
* 用户认证以后记住我过滤器
* Shiro默认是从Session获取数据判断是否认证
* 配置此过滤器对应的资源就可以从Cookie获取身份数据判断是否认证
* 缺陷:所有需要认证才能访问的资源全部需要配置,会造成代码大量配置臃肿
* 解决方案:将Cookie数据取出来放到Session即可
*/
/*
* 过滤器链的配置 map集合
*
* map.put("资源",过滤器别名)
* 配置从上到下,访问资源只要匹配到某一个过滤器以后,就不会再匹配后面过滤器
* 匹配精度越高一定越要配置上面
*
*
* */
HashMap<String, String> filterChainDefinitionMap = new HashMap<>();
filterChainDefinitionMap.put("/css/**","anon");
filterChainDefinitionMap.put("/images/**","anon");
filterChainDefinitionMap.put("/favicon.ico","anon");
filterChainDefinitionMap.put("/js/**","anon");
filterChainDefinitionMap.put("/user/loginPage","anon");
/*配置退出认证过滤器*/
filterChainDefinitionMap.put("/logout","logout");
//配置记住我过滤器
filterChainDefinitionMap.put("/index","user");
/*配置权限授权过滤器*/
/*filterChainDefinitionMap.put("/user/userPage","perms[user:userPage]");
filterChainDefinitionMap.put("/student/studentPage","perms[student:studentPage]");
filterChainDefinitionMap.put("/teacher/teacherPage","perms[teacher:teacherPage]");*/
filterChainDefinitionMap.put("/**","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* Shiro框架的核心对象
* @return
*/
@Bean
public DefaultWebSecurityManager securityManager(){
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(customRealm());
//设置缓存管理器
defaultWebSecurityManager.setCacheManager(cacheManager());
//设置会话管理器
defaultWebSecurityManager.setSessionManager(sessionManager());
//设置记住我管理器
defaultWebSecurityManager.setRememberMeManager(remeberMeManager());
return defaultWebSecurityManager;
}
//设置记住我管理器
@Bean
public RememberMeManager remeberMeManager(){
CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(cookie());
return cookieRememberMeManager;
}
@Bean
public Cookie cookie(){
SimpleCookie rememberMe = new SimpleCookie("rememberMe");
rememberMe.setMaxAge(3600*24*7);//单位:秒
return rememberMe;
}
//设置会话管理器
@Bean
public SessionManager sessionManager(){
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
//单位:毫秒 milliseconds
defaultWebSessionManager.setGlobalSessionTimeout(10000);
//清除地址栏后面重写的jsessionid
defaultWebSessionManager.setSessionIdUrlRewritingEnabled(false);
return defaultWebSessionManager;
}
//设置缓存管理器
@Bean
public CacheManager cacheManager(){
EhCacheManager ehCacheManager = new EhCacheManager();
return ehCacheManager;
}
//自定义realm
@Bean
public CustomRealm customRealm(){
CustomRealm customRealm = new CustomRealm();
//设置凭证匹配器
customRealm.setCredentialsMatcher(credentialsMatcher());
return customRealm;
}
//设置凭证匹配器
@Bean
public HashedCredentialsMatcher credentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");
hashedCredentialsMatcher.setHashIterations(3);
return hashedCredentialsMatcher;
}
//设置Shiro框架对注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}
/*设置Spring框架支持集成其他框架可以使用AOP*/
@Bean
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
/*设置可以让Shiro框架使用AOP为表现层创建代理(Shiro权限判断的注解全部在表现层)*/
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
//创建对SpringMVC抛出异常处理解析器
@Bean
public SimpleMappingExceptionResolver simpleMappingExceptionResolver(){
Properties properties = new Properties();
/*此时跳转地址在对应SpringMVC视图解析前后缀对应位置,逻辑地址*/
properties.put("org.apache.shiro.authz.UnauthorizedException","/unauthorized");
SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
simpleMappingExceptionResolver.setExceptionMappings(properties);
return simpleMappingExceptionResolver;
}
}
4.5自定义realm代码
public class CustomRealm extends AuthorizingRealm {
/**
* 获取授权信息方法:开发者在此方法中获取数据库中当前认证通过的权限信息方法
* @param principals 认证通过的身份信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
User user = (User) principals.getPrimaryPrincipal();
Integer roleId = user.getRoleId();
//模拟根据当前认证用户的角色id查询出对应权限的权限表达式
List<String> permissions = new ArrayList<>();
permissions.add("user:insert");
permissions.add("user:list");
permissions.add("user:update");
permissions.add("student:list");
permissions.add("user:userPage");
permissions.add("teacher:teacherPage");
//创建授权信息对象
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.addStringPermissions(permissions);
System.out.println("CustomRealm.doGetAuthorizationInfo");
return simpleAuthorizationInfo;
}
/**
* 获取认证信息方法:开发在此方法中完成调用数据库查询出对应的用户身份信息的方法
* @param token 带有账号密码token
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//1.获取token中的身份(账号)
String username = (String) token.getPrincipal();
//2.模拟数据库查询账号
List<String> users = Arrays.asList("admin","lucy","lili","jack","rose");
System.out.println("username :"+username);
//数据库账号不存在
if(!users.contains(username)){
return null;
}
//账号存在
User user = new User(username,"2a10903f4b947c2ebcec21326820e7a4","qazw",10);//qwer123
System.out.println("CustomReaml.doGetAuthenticationInfo");
//3.创建认证信息对象
/**
* SimpleAuthenticationInfo(Object principal, Object hashedCredentials, ByteSource credentialsSalt, String realmName)
* principal : 身份,数据库查询的User对象
* hashedCredentials ; 凭证,user.getPassword() : 数据库中加密的密码
* ByteSource credentialsSalt
* realmName : 任意字符串,null也可以
*/
//获取密码
String hashedCredentials = user.getPassword();
//获取User的盐
String salt = user.getSalt();//qazw
//将salt封装ByteSource对象
ByteSource credentialsSalt = ByteSource.Util.bytes(salt);
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,hashedCredentials,credentialsSalt,this.getName());
return simpleAuthenticationInfo;
}
}
4.6自定义表单认证过滤器代码
public class MyFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
//在执行父类方法之前清除掉SaveRequest对象
WebUtils.getAndClearSavedRequest(request);
return super.onLoginSuccess(token, subject, request, response);
}
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//从请求中获取Shiro的 主体
Subject subject = getSubject(request, response);
//从主体中获取Shiro框架的Session
Session session = subject.getSession();
//如果主体没有认证(Session中认证)并且 主体已经设置记住我了
if (!subject.isAuthenticated() && subject.isRemembered()) {
//获取主体的身份(从记住我的Cookie中获取的)
User principal = (User) subject.getPrincipal();
//将身份认证信息共享到 Session中:Session中的名称可以随便写
session.setAttribute("USER_IN_SESSION", principal);
}
return subject.isAuthenticated() || subject.isRemembered();
}
}
4.7出现的问题及其解决方式
4.7.1认证成功后跳转到首页的问题
- 当我们随意输入一个地址(不是/index)时,页面会被拦截,跳转到/user/loginError(错误重新登录界面),然后当我们输入了正确的用户及密码时,页面不会主动跳转到/index页面,而会跳转到我们随意输入的那个地址。
- 原因:Shiro框架表单认证过滤器在没有认证之前,会自动记录浏览器在当前项目中最后一次访问的任何一个页面,并且保存到Session中,当用户认证成功以后,Shiro自动跳转到这个页面,不管页面存不存在,正确与否。
- 解决方式:自定表单认证过滤器(MyFormAuthenticationFilter ),并且onLoginSuccess方法,清除保存在Session中跳转的地址。
4.7.2为什么要有缓存
- 因为认证通过以后访问的页面,每访问一次都要进行一次授权,都会执行一次授权方法,若一个页面中有N个需要授权的界面时,那么就会执行N次授权方法,而授权相关的权限数据都是从数据库查询的,会造成频繁的查询数据库,影响系统性能。
4.7.3MyFormAuthenticationFilter 中isAccessAllowed ()方法作用
- 记住我功能生效以后,Shiro会将用户身份数据存放到本地Cookie,但是Shiro在运行过程中,使用Session中获取身份进行进行判断处理的,所以记住我以后会存在一个问题。
- 解决: Shiro从Cookie获取的信息并没有自动设置到Session中,所以我们需要自己把Cookie记住我信息存放到Session中。
4.7.4登录界面的细节问题
- 登录表单的提交地址必须是认证失败地址,shiro才认为用户在做认证操作。