权限管理,可以实现用户访问系统的控制,权限管理分为认证和授权
Shiro(Apache Shiro)是Java的一个安全框架,Shiro属于轻量级框架,可以实现认证、授权、加密、会话管理、与Web集成、缓存等
Shiro核心类示意图
主要类:
Authentication: 身份认证
Authorization: 授权,即权限验证,验证某个已认证用户是否拥有某个权限
Seesion Manager: 会话管理,每次用户登录都是一次会话,在没有退出之前,他的所有信息都在会话中
Cryptography: 加密,保护数据的安全性
Web Support: Web支持,可以容易的集成到Web环境中
Catching: 缓存,用户登录后,其用户信息、所拥有的角色、权限不用每次去查,可以提高效率
Concurrency: shiro支持多线程应用的并发验证,即在一个线程中开启另一个线程,能把权限自动传播过去
Testing: 提供测试支持
Run As: 允许一个用户以另一个用户的身份进行访问
Remember Me: 记住我,即一次登录后,下次就不用登录了
Shiro架构图:
主要组件:
Subject: 主体,主体可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
SecurityManager: 安全管理器,主体进行认证和授权都是通过securityManager进行
Authenticator: 认证器,主体进行认证最终通过Authenticator进行的。
Authorizer: 授权器,主体进行授权最终通过authenticator进行的。
SessionManager: 会话管理,web应用中一般是用web容器对session进行管理,Shiro也提供一套session管理的方式。
SessionDao: 通过SessionDao管理Session数据
CacheManager: 缓存管理器,主要对Session和授权数据进行缓存
Realm: 领域,相当于数据源,通过Realm存取认证、授权相关数据
Cryptography: 密码管理,提供一套加密/解密的组件,便于开发
登录认证
1. 整合Shiro,添加Shiro所需要的pom依赖
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.24</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
<!-- shiro核心jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro整合到web工程所依赖的jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro缓存依赖的jar包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.4.0</version>
</dependency>
<!-- shiro整合与Spring的整合-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2. 在spring当中配置shiro过滤器和安全管理器
<!-- 配置shiro过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--配置登录认证的路径
如果请求是login,则做去执行认证操作(调用对应的Realm执行认证操作);
如果请求不是login请求,且没有认证,则会直接执行login请求(下面贴有login请求对应的方法)
-->
<property name="loginUrl" value="/login"/>
<property name="securityManager" ref="securityManager"></property>
<!-- 配置shiro过滤器pattern -->
<property name="filterChainDefinitions">
<value>
/static/** = anon <!--不需要登录验证-->
/login.jsp = anon <!--不需要登录验证-->
/**=authc <!--除指定请求外,其它所有的请求都需要身份验证-->
</value>
</property>
</bean>
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"></bean>
login请求对应的控制器方法
@Controller
public class LoginController {
@RequestMapping("/login")
public String login() {
return "redirect:login.jsp";
}
}
3. 在web.xml当中配置过滤器拦截所有请求,进行处理
<!-- 拦截到所有请求,使用spring一个bean(ShiroFilter)来进行处理 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<!-- 是否filter中的init和 destroy-->
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
4. 配置及编写用于认证操作的Realm
把自己写的自定义Realm交给Spring管理
<!--自定义realm-->
<!-- 配置realm数据源 -->
<!-- class为自己编写的realm -->
<bean id="employeeRealm" class="com.eh.web.realm.EmployeeRealm"></bean>
并将Realm配置到安全管理器中
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="employeeRealm"/>
<!-- 注入缓存-->
<property name="cacheManager" ref="ehCache"/>
</bean>
自定义realm编写认证操作
public class EmployeeRealm extends AuthorizingRealm {
@Autowired
private EmployeeService employeeService;
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("来到了认证");
//获取表单传来的参数(表单传的参数已经被封装到token中)
String username = (String) token.getPrincipal();
System.out.println(username);
//到数据库中查询有没有当前用户
Employee employee = employeeService.getEmployeeWithName(username);
System.out.println(employee);
if (employee == null) {
//没有该用户
return null;
}
//认证
//参数: 主体(当前用户) 正确的密码,盐(此处省略没写,下面会单独介绍),当前realm名称
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(employee, employee.getPassword(), this.getName());
//将认证信息返回(成功还是失败) 定义一个过滤器,用来监听该认证是否成功
return info;
}
}
5. 编写用于监听认证是否成功的过滤器
将该过滤器交给Spring管理
<!-- 配置自定义的监听的过滤器-->
<bean id="myFormFilter" class="com.eh.web.filter.MyFormFilter"></bean>
<!-- 配置shiro过滤器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--配置登录认证的路径
如果请求是login,则做去执行认证操作(调用对应的Realm执行认证操作);
如果请求不是login请求,且没有认证,则会直接执行login请求(下面贴有login请求对应的方法)
-->
<property name="loginUrl" value="/login"/>
<!--将监听的过滤器配置到shiro中-->
<property name="filters">
<map>
<entry key="authc" value-ref="myFormFilter"/>
</map>
</property>
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="employeeRealm"/>
</bean>
<!-- 配置shiro过滤器pattern -->
<property name="filterChainDefinitions">
<value>
/static/** = anon <!--不需要登录验证-->
/login.jsp = anon <!--不需要登录验证-->
/logout = logout <!--取消认证-->
/**=authc <!--除指定请求外,其它所有的请求都需要身份验证-->
</value>
</property>
</bean>
//表单认证过滤器
public class MyFormFilter extends FormAuthenticationFilter {
//拦截成功
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {
System.out.println("认证成功");
AjaxRes ajaxRes = new AjaxRes();
ajaxRes.setSuccess(true);
ajaxRes.setMsg("登录成功");
//把对象转换成json类型的字符串(前端请求使用的ajax请求)
String JsonString = new ObjectMapper().writeValueAsString(ajaxRes);
//设置字符集编码
response.setCharacterEncoding("utf-8");
//相应给浏览器
response.getWriter().print(JsonString);
//返回false 表示不需要执行下一个拦截器
return false;
}
//拦截失败
protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
System.out.println("认证失败");
AjaxRes ajaxRes = new AjaxRes();
//根据不同的异常相应不同的信息
if (e != null) {
//获取异常名称
String name = e.getClass().getName();
if (name.equals(UnknownAccountException.class.getName())) {
//账号错误
ajaxRes.setSuccess(false);
ajaxRes.setMsg("账号不存在");
} else if (name.equals(IncorrectCredentialsException.class.getName())) {
//密码错误
ajaxRes.setSuccess(false);
ajaxRes.setMsg("密码错误");
} else {
//未知异常
ajaxRes.setSuccess(false);
ajaxRes.setMsg("未知错误");
}
}
//设置字符集编码
response.setCharacterEncoding("utf-8");
try {
//把对象转换成json类型的字符串
String JsonString = new ObjectMapper().writeValueAsString(ajaxRes);
response.setCharacterEncoding("utf-8");
//相应给浏览器
response.getWriter().print(JsonString);
} catch (IOException ex) {
ex.printStackTrace();
}
//返回false 表示不需要执行下一个拦截器
return false;
}
}
浏览器请求及相应转发代码
$.post("login", $("form").serialize(), function (data) {
//把json格式的字符串转换成json数据
data = $.parseJSON(data)
if (data.success) {
//成功
window.location.href = "${pageContext.request.contextPath}/index.jsp";
} else {
alert(data.msg);
}
})
用户退出功能的实现:
前端界面:
<a style="font-size: 18px; color: white;text-decoration: none;" href="${pageContext.request.contextPath}/logout">注销</a>
在shiro过滤器中添加logout的处理
<!-- 配置shiro过滤器pattern -->
<property name="filterChainDefinitions">
<value>
/static/** = anon <!--不需要登录验证-->
/login.jsp = anon <!--不需要登录验证-->
/logout = logout <!--取消认证-->
/**=authc <!--除指定请求外,其它所有的请求都需要身份验证-->
</value>
</property>
这样整个认证登录过程就实现实现了
用户授权
授权方法调用的时机:
- 有shiro标签的使用
<shiro:hasPermission name="employee:delete">
<a href="#" class="easyui-linkbutton" data-options="iconCls:'icon-remove',plain:true" id="delete">离职</a>
</shiro:hasPermission>
- 有权限(shiro)的注解
@RequiresPermissions("employee:index")
@RequestMapping("/employee")
public String employee() {
return "employee";
}
注:在使用shiro注解时,需要在配置文件汇总添加shiro的注解扫描:
<!--
配置为true即使用cglib继承的方式,
false为jdk的接口动态代理 控制器没有实现接口
-->
<aop:config proxy-target-class="true" ></aop:config>
<!-- 使用第三方去扫描shiro的注解 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor ">
<property name="securityManager" ref="securityManager"></property>
</bean>
授权方法
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
System.out.println("授权");
Employee employee = (Employee) principal.getPrimaryPrincipal();
//查询角色和对应的权限
List<String> roles = new ArrayList<>();
List<String > permissions = new ArrayList<>();
//判断当前用户是否是管理员, 如果是管理员则拥有所有权限
if (employee.getAdmin()) {
//拥有所有权限
permissions.add("*:*");
} else {
//查询角色
roles = employeeService.getRolesById(employee.getId());
System.out.println("------------------" + roles);
//查询权限
permissions = employeeService.getPermissionsById(employee.getId());
System.out.println("------------------" + permissions);
}
//赋给授权信息
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(roles);
info.addStringPermissions(permissions);
return info;
}
没有权限结果处理,当没有权限访问某一页面时,会报500错误,这时候可以定义一个处理没有权限的方法
//未授权异常
@ExceptionHandler(AuthorizationException.class)
public void ExceptionHandler(HandlerMethod method, HttpServletResponse response) throws IOException {//拿取发生异常的方法
//判断当前请求是不是Json(AJAX)请求,如果是(则在该方法中无法实现跳转),返回json数据给浏览器,让其自己做跳转
//获取发生异常的方法
//获取方法上的注解,通过方法上是否有@ResponseBody注解来判断
ResponseBody responseBody = method.getMethodAnnotation(ResponseBody.class);
if (responseBody != null) {
//则该请求为(Ajax)json请求
AjaxRes ajaxRes = new AjaxRes();
ajaxRes.setMsg("没有权限操作");
ajaxRes.setSuccess(false);
String s = new ObjectMapper().writeValueAsString(ajaxRes);
response.setCharacterEncoding("utf-8");
response.getWriter().print(s);
} else {
response.sendRedirect("nopermission.jsp");
}
}
这样就实现了用户授权
补充:
Shiro还有一些比较常用但前面还没有介绍的业务,这里补充说明一下
Shiro缓存的使用
- 所需要的pom依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.2</version>
</dependency>
- 添加shiro缓存配置
添加shiro-ehcache.xml
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
diskPersistent="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
- 将该缓存配置添加到安全管理器(SecurityManager)中
<!-- 配置shiro安全管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="employeeRealm"/>
<!--注入缓存-->
<property name="cacheManager" ref="ehCache"/>
</bean>
Shiro密码加密
- 添加凭证匹配器
<!-- 凭证匹配器 -->
<bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher">
<!-- 散列算法 -->
<property name="hashAlgorithmName" value="md5"/>
<!-- 散列次数 -->
<property name="hashIterations" value="2"></property>
</bean>
并将该凭证匹配器配置到自定义Realm(数据源)中
<!--自定义realm-->
<!-- 配置realm数据源 -->
<bean id="employeeRealm" class="com.eh.web.realm.EmployeeRealm">
<property name="credentialsMatcher" ref="credentialsMatcher"/>
</bean>
- 在保存用户时, 给用户密码进行加密处理
@Override
public void saveEmployee(Employee employee) {
//把密码进行加密,把该用户名当做盐 散列次数2
Md5Hash md5Hash = new Md5Hash(employee.getPassword(), employee.getUsername(), 2);
employee.setPassword(md5Hash.toString());
//保存员工
employeeMapper.insert(employee);
//保存关系表
for (Role role : employee.getRoles()) {
employeeMapper.insertEmployeeAndRoleRel(employee.getId(), role.getRid());
}
}
- 认证时,添加密码处理
//参数 正确的密码,盐,当前realm名称
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(employee, employee.getPassword(), ByteSource.Util.bytes(employee.getUsername()), this.getName());