原理回顾
什么是权限管理?
权限管理是系统的安全范畴,要求必须是合法的用户才可以访问系统(用户认证),且必须具有该 资源的访问权限才可以访问该 资源(授权)。
认证:对用户合法身份的校验,要求必须是合法的用户才可以访问系统。
授权:访问控制,必须具有该 资源的访问权限才可以访问该 资源。
权限模型:标准权限数据模型包括 :用户、角色、权限(包括资源和权限)、用户角色关系、角色权限关系。
权限分配:通过UI界面方便给用户分配权限,对上边权限模型进行增、删、改、查操作。
权限控制:
shiro认证流程:(掌握)
Authenticator:认证器( shiro提供)
realm(一般需要自定义):相当于数据源,认证器需要realm从数据源查询用户身份信息及权限信息。
权限管理是系统的安全范畴,要求必须是合法的用户才可以访问系统(用户认证),且必须具有该 资源的访问权限才可以访问该 资源(授权)。
认证:对用户合法身份的校验,要求必须是合法的用户才可以访问系统。
授权:访问控制,必须具有该 资源的访问权限才可以访问该 资源。
权限模型:标准权限数据模型包括 :用户、角色、权限(包括资源和权限)、用户角色关系、角色权限关系。
权限分配:通过UI界面方便给用户分配权限,对上边权限模型进行增、删、改、查操作。
权限控制:
- 基于角色的权限控制:根据角色判断是否有操作权限,因为角色的变化 性较高,如果角色修改需要修改控制代码,系统可扩展性不强。
- 基于资源的权限控制:根据资源权限判断是否有操作权限,因为资源较为固定,如果角色修改或角色中权限修改不需要修改控制代码,使用此方法系统可维护性很强。建议使用。
- 对于粗颗粒权限管理,建议在系统架构层面去解决,写系统架构级别统一代码(基础代码)。所谓粗颗粒权限,比如对系统的url、菜单、jsp页面、页面上按钮、类方法进行权限管理,即对资源类型进行权限管理。
- 对于细颗粒权限管理:所谓细颗粒权限, 比如用户id为001的用户信息(资源实例)、类型为t01的商品信息(资源实例),对资源实例进行权限管理,理解对数据级别的权限管理。细颗粒权限管理是系统的业务逻辑,业务逻辑代码不方便抽取统一代码,建议在系统业务层进行处理。
- 企业开发常用的方法,使用web应用中filter来实现,用户请求url,通过filter拦截,判断用户身份是否合法(用户认证),判断请求的地址是否是用户权限范围内的url(授权)。
shiro认证流程:(掌握)
- 1.subject(主体)请求认证,调用subject.login(token)
- 2.SecurityManager (安全管理器)执行认证
- 3.SecurityManager通过ModularRealmAuthenticator进行认证。
- 4.ModularRealmAuthenticator将token传给realm,realm根据token中用户信息从数据库查询用户信息(包括身份和凭证)
- 5.realm如果查询不到用户给ModularRealmAuthenticator返回null,ModularRealmAuthenticator抛出异常(用户不存在)
- 6.realm如果查询到用户给ModularRealmAuthenticator返回AuthenticationInfo(认证信息)
- 7.ModularRealmAuthenticator拿着AuthenticationInfo(认证信息)去进行凭证(密码 )比对。如果一致则认证通过,如果不致抛出异常(凭证错误)。
Authenticator:认证器( shiro提供)
realm(一般需要自定义):相当于数据源,认证器需要realm从数据源查询用户身份信息及权限信息。
shiro与项目集成开发
shiro与spring web项目整合
shiro与spring web项目整合在"基于url拦截实现的工程" 基础上整合,基于url拦截实现的工程的技术架构师SpringMVC+Mybatis,整合时注意两点:
1.shiro与Spring整合
2.加入shiro对web应用的支持
创建web工程
创建新工程permission_web_shiro
取消原springmvc认证和授权拦截器
去除springmvc.xml中配置的LoginInterceptor和PermissionInterceptor拦截器
导入shiro相应的jar包
主要包括:
shiro-web:shiro整合web项目必须的包
shiro-spring:shiro整合Spring项目需要的整合包,依赖于shiro-web包
shiro-core:shiro核心包。必须导入的包。
在web.xml中配置shiro的filter
在web系统中,shiro也通过filter进行拦截,filter拦截器后将操作权交给Spring中配置的filterChain(过滤器链),shiro提供很多filter。要使用代理filter类DelegatingFilterProxy
<!-- shiro的filter -->
<!-- shiro过滤器,DelegatingFilterProxy通过代理模式将Spring容器中的bean和filter关联起来 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 设置targetFilterLifecycle为true 由servlet控制filter生命周期 -->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
<!-- 设置Spring容器filter的bean id,如果不设置则在Spring注册的bean中查找与filter-name一致的bean -->
<init-param>
<param-name>targetBeanName</param-name>
<param-value>shiroFilter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
applicationContext-shiro.xml
<!-- web.xml中shiro的filter对应的bean -->
<!-- Shiro的web过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager"/>
<!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 -->
<property name="loginUrl" value="/login.action"/>
<!-- 认证成功统一跳转到first.action,建议不配置,默认情况下,shiro认证成功后自动跳转上一个请求路径 -->
<property name="successUrl" value="/first.action"/>
<!-- 通过unauthorizedUrl 指定没有权限操作时的跳转页面 -->
<property name="unauthorizedUrl" value="/refuse.jsp"/>
<!-- 过滤器链定义,从上向下执行,一般将/**放在最下边 -->
<property name="filterChainDefinitions">
<value>
<!-- 退出拦截,请求logout.action执行退出操作 shiro自动清除Session-->
/logout.action = logout
<!-- 无权访问页面 anon表示可以匿名访问 -->
/refuse.jsp = anon
<!-- 验证码可以匿名访问 -->
/validatecode.jsp = anon
<!-- perms[xx] 表示有xx权限才可以访问 -->
/item/queryItem.action = perms[item:query]
/item/editItem.action = perms[item:edit]
<!-- 对静态资源设置匿名访问 -->
/js/** = anon
/images/** = anon
/styles/** = anon
<!-- /** = authc 表示所有的URL都必须认真通过才可以进行访问-->
/** = authc
</value>
</property>
</bean>
<!-- 安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="customRealm" />
</bean>
<!-- 自定义Realm -->
<bean id="customRealm" class="liuxun.ssm.shiro.CustomRealm"/>
securityManager:这个属性是必须的
loginUrl:没有登录认证的用户请求将跳转到此地址进行认证,不是必须的属性,不输入地址的话会自动寻找web项目的根目录下的"login.jsp" 页面
自定义Realm模拟测试
此Realm先不从数据库查询权限数据,当前需要先将shiro整合完成。package liuxun.ssm.shiro;
import java.util.ArrayList;
import java.util.List;
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.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import liuxun.ssm.po.ActiveUser;
import liuxun.ssm.po.SysPermission;
public class CustomRealm extends AuthorizingRealm {
// 设置Realm名称
@Override
public void setName(String name) {
super.setName("CustomRealm");
}
// 支持UsernamePasswordToken
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 从token中获取用户身份信息
String username = (String) token.getPrincipal();
// 拿着username从数据库中进行查询
// ....
// 如果查询不到返回null
if (!username.equals("zhangsan")) {
return null;
}
// 获取从数据库查询出来的用户密码
String password = "123"; // 这里使用静态数据进行测试
// 根据用户id从数据库中取出菜单
// ...先使用静态数据
List<SysPermission> menus = new ArrayList<SysPermission>();
SysPermission sysPermission_1 = new SysPermission();
sysPermission_1.setName("商品管理");
sysPermission_1.setUrl("/item/queryItem.action");
SysPermission sysPermission_2 = new SysPermission();
sysPermission_2.setName("用户管理");
sysPermission_2.setUrl("/user/query.action");
menus.add(sysPermission_1);
menus.add(sysPermission_2);
// 构建用户身份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(username);
activeUser.setUsername(username);
activeUser.setUsercode(username);
activeUser.setMenus(menus);
// 返回认证信息由父类AuthenticationRealm进行认证
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(activeUser, password,
this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
//获取身份信息
ActiveUser activeUser = (ActiveUser) principals.getPrimaryPrincipal();
//用户id
String userid = activeUser.getUserid();
// 根据用户id从数据库中查询权限数据
// ...这里使用静态数据模拟
List<String> permissions = new ArrayList<String>();
permissions.add("item:query");
permissions.add("item:update");
//将权限信息封装为AuthorizationInfo
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
//基于资源权限的访问控制
for (String permission : permissions) {
simpleAuthorizationInfo.addStringPermission(permission);
}
// 如果基于角色进行访问控制
// for (String role : roles) {
// simpleAuthorizationInfo.addRole(role);
// }
return simpleAuthorizationInfo;
}
}
登录
(1) 登录原理
使用FormAuthenticationFilter过滤器实现,原理如下:
当用户没有认证时,请求loginUrl进行认证,用户身份和用户密码提交数据到loginUrl,FormAuthorizationFilter拦截去除request中的username和password(两个参数名称是可以配置的),FormAuthorizationFilter调用Realm传入一个token(username和password),realm认证时根据username查询用户信息(在ActiveUser中存储,包括userid、usercode、username、menus) 如果查询不到,Realm返回null,FormAuthorizationFilter向request域中填充了一个参数"shiroLoginFailure" 记录了异常信息。
(2) 登录页面
由于FormAuthorizationFilter的用户身份和密码的input的默认值(username和password),修改页面中账号和密码的input的name属性为username和password。
(3)控制器登录代码的实现,如下:
//用户登录提交方法
@RequestMapping("/login")
public String login(HttpServletRequest request) throws Exception{
//shiro在认证通过后出现错误后将异常类路径通过request返回
//如果登陆失败从request中获取认证异常信息,shiroLoginFailure就是shiro异常类的全限定名
String exceptionClassName = (String)request.getAttribute("shiroLoginFailure");
if (exceptionClassName!=null) {
if (UnknownAccountException.class.getName().equals(exceptionClassName)) {
throw new CustomException("账号不存在");
} else if(IncorrectCredentialsException.class.getName().equals(exceptionClassName)){
throw new CustomException("用户名/密码错误");
}else {
throw new Exception(); //最终在设置的异常处理器中生成未知错误
}
}
//此方法不处理登录成功(认证成功)的情况
//如果登录失败还到login页面
return "login";
}
首页
1.认证成功后用户菜单在首页显示(从activeUser获取)
2.认证后用户的信息在页头显示(从activeUse