授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
:::tip 注意
注意:授权是基于认证基础之上的
:::
授权的三种方式
- 编程方式:如使用if判断当前subject是否有权限,这种方法判断时候实际上已经进入了方法内部。
- 注解方式:在方法上添加权限注解,在进入方法前进行验证。
- jsp标签方式:通过shiro提供的jsp标签根据权限确定是否对用户显示按钮等操作,如果直接根据url方式请求则可以直接访问不需要授权。
授权步骤
1.在需要授权的方法上添加@RequirePermission注解
在使用注解时需要注意三点:①注解本身,必须要定义个注解 ②被注解的类,的注解要放在方法上还是类上 ③怎样才能让注解生效
比如我在查询列表上添加@RequiresPermissions("user:list")
注解,就表示访问这个方法需要用户拥有user 的llist权限才能访问,否则就会抛出UnauthorizedException
这个异常。
2.在spring-shiro文件中配置代理类解析注解
<!-- 开启aop,对类代理 -->
<aop:config proxy-target-class="true"></aop:config>
<!-- 开启shiro注解支持 -->
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager" />
</bean>
:::warning 注意:
我在Controller添加了此方法后,此时在我的UserRealm中授权信息是空,也就是说当前用户没有任何权限,但是当我访问了这个方法之后居然还可以正常访问。。。。通过百度大法找到了问题的答案:
我们知道Shiro的注解授权是利有Spring的AOP实现的。在程序启动时会自动扫描作了注解的Class,当发现注解时,就自动注入授权代码实现。也就是说,要注入授权控制代码,第一处必须要让框架要可以扫描找被注解的Class 。
spring.xml
<context:component-scan base-package="cn.edu.neusoft" >
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
spring-mvc.xml
<context:component-scan base-package="cn.edu.neusoft" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
而我们的Srping项目在ApplicationContext.xml中一般是不扫描Controller的,所以也就无法让写在Controller中的注解授权生效了。因此正确的作法是将这配置放到springmvc的配置文件中.这样Controller就可以通过注解授权了。
不过问题来了,通过上面的配置Controller是可以通过注解授权了,但是Services中依然不能通过注解授权。虽然说,如果我们在Controller控制了授权,那么对于内部调用的Service层就可以不再作授权,
但也有例外的情况,比如Service除了给内部Controller层调用,还要供远程SOAP调用,那么就需要对Service进行授权控制了。同时要控制Controller和Service,那么采用相同的方式,我们可以在ApplicationContext.xml中配置类同的配置,以达到相同的效果。
:::
3.配置Shiro的异常处理
因为SpringMVC有一个默认的异常处理机制,所以需要定义Shiro的特粗异常处理程序才能生效,这里就是如果有无权限这个异常重定向到nopermisson这个页面
<!--shiro权限异常处理-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="org.apache.shiro.authz.UnauthorizedException">redirect:/admin/system/nopermission</prop>
</props>
</property>
</bean>
::: tip 没有用ajax的忽略这里:
到这里就完成第一步了,不过又遇到一个问题,我是使用ajax请求,而在请求之前没到Controller时候请求就已经被拦截了,响应回了一个nopermission这个页面,而用ajax有不能自动跳转到这个页面,很难受。。。
正在解决中。。(找了一天终于解决了):
参考文章:https://blog.csdn.net/catoop/article/details/69210140
原理就是通过一个 BaseController 来统一处理,然后被其他 Controller 继承即可,对于JSON和页面跳转,我们只需要做一个Ajax判断处理即可。
BaseController代码:
/**
* @author Chen
* @create 2019-05-02 19:26
*/
public abstract class BaseController {
/**
* 权限异常
*/
@ExceptionHandler({ UnauthorizedException.class, AuthorizationException.class })
public String authorizationException(HttpServletRequest request, HttpServletResponse response) {
if (WebUtilsPro.isAjaxRequest(request)) {
// 输出JSON
Map<String,Objectmap = new HashMap<>();
map.put("type", "error");
map.put("msg", "无权限");
writeJson(map, response);
return null;
} else {
return "redirect:/system/403";
}
}
/**
* 输出JSON
*
* @param response
* @author SHANHY
* @create 2017年4月4日
*/
private void writeJson(Map<String,Objectmap, HttpServletResponse response) {
PrintWriter out = null;
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
out = response.getWriter();
//这里用了一个阿里的fastjson,直接添加一下依赖即可
out.write(String.valueOf(new JSONObject(map)));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
}
}
}
WebUtilsPro 代码:
package cn.edu.neusoft.common;
import javax.servlet.http.HttpServletRequest;
/**
* @author Chen
* @create 2019-05-02 19:27
*/
public class WebUtilsPro {
/**
* 是否是Ajax请求
*
* @param request
* @return
* @author SHANHY
* @create 2017年4月4日
*/
public static boolean isAjaxRequest(HttpServletRequest request) {
String requestedWith = request.getHeader("x-requested-with");
if (requestedWith != null && requestedWith.equalsIgnoreCase("XMLHttpRequest")) {
return true;
} else {
return false;
}
}
}
然后继承这个BaseController 就OK 了。
:::
4.加载权限表达式
::: tip 说明:
我们用注解在很多个方法上都标注了权限,用户必须有这个权限才能进行访问,在Realm需要将用户具有的权限告诉Shiro,而这么多方法肯定要保存到数据库中,我们也不能一个一个去加到数据库中。
我们通过获取注解中的属性来添加到数据库:(数据库中存了权限表达式以及权限名称)
由于Shiro没有给我们提供自定义权限名称的注解,于是我们自定义一个注解用于获取权限名称添加到数据库中。
:::
PermissionController:
/**
* @author Chen
* @create 2019-05-01 20:28
*/
@Controller
@RequestMapping("admin/permission")
public class PermissionController {
@Autowired
private PermissionService permissionService;
//请求映射处理映射器
//springmvc在启动时候将所有贴有请求映射标签:RequestMapper方法收集起来封装到该对象中
@Autowired
private RequestMappingHandlerMapping rmhm;
@ResponseBody
@RequestMapping("reload")
public Map<String,Object> reload(){
Map<String,Object> map = new HashMap<String, Object>();
//将系统中所有权限表达式加载进入数据库
//0:从数据库中查询出所有权限表达式,然后对比,如果已经存在了,跳过,不存在添加
List<String> resourcesList = permissionService.getAllResources();
//1:获取controller中所有带有@RequestMapper标签的方法
Map<RequestMappingInfo, HandlerMethod> handlerMethods = rmhm.getHandlerMethods();
Collection<HandlerMethod> methods = handlerMethods.values();
for (HandlerMethod method : methods) {
//2:遍历所有方法,判断当前方法是否贴有@RequiresPermissions权限控制标签
RequiresPermissions anno = method.getMethodAnnotation(RequiresPermissions.class);
if(anno != null){
//3:如果有,解析得到权限表达式,封装成Permission对象保存到Permission表中
//权限表达式
String resource = anno.value()[0];
//去除重复的
if(resourcesList.contains(resource)){
continue;
}
Permission p = new Permission();
p.setResource(resource);
//设置权限名称
p.setName(method.getMethodAnnotation(PermissionName.class).value());
//保存
permissionService.addPermission(p);
}
}
map.put("type","success");
map.put("msg","加载成功!");
return map;
}
}
注解类PermissionName
package cn.edu.neusoft.realm;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Chen
* @create 2019-05-01 20:20
* 自定义权限名称注解
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionName {
String value();
}
我们只要像这样加上注解请求指定路径即可将权限加载到数据库中。
@RequiresPermissions("user:delete")
@PermissionName("用户删除")
public Map<String,Object> deleteUser(@RequestParam(required = true) Long id){
······
5.整合数据库授权
这里就可以把对应角色的权限告诉Shiro让他来判断已经登录的角色是否有权限访问了
修改UserRealm类的doGetAuthorizationInfo方法:
/**
* @author Chen
* @create 2019-04-30 15:27
*/
public class UserRealm extends AuthorizingRealm {
@Setter
private UserService userService;
@Setter
private RoleService roleService;
@Setter
private PermissionService permissionService;
@Override
public String getName() {
return "UserRealm";
}
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) principalCollection.getPrimaryPrincipal();
List<String> permissions = new ArrayList<String>();
List<String> roles = new ArrayList<String>();
if ("admin".equals(user.getUsername())){
//让超级管理员拥有所有权限
permissions.add("*:*");
//查询所有角色
roles = roleService.getRoleByUserId(user.getId());
}else {
//根据用户ID查询该用户具有的角色
roles = roleService.getRoleByUserId(user.getId());
//根据用户ID查询该用户具有的权限
permissions = permissionService.getAllPermissionsByUserId(user.getId());
}
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(permissions);
info.addRoles(roles);
return info;
}
/**
* 认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//从token中获取登录的用户名, 查询数据库返回用户信息
String username = (String) token.getPrincipal();
User user = userService.getUserByUsername(username);
if (user == null) {
return null;
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
return info;
}
}
不要忘了在Spring-shiro.xml注入对应的Service
这里根据用户ID查询角色信息以及权限信息用到了多表查询,顺便复习了一下,需要的点击下方链接:
在把对应的sql语句放上来把:
<select id="getAllPermissionsByUserId" parameterType="Long" resultType="String">
select resource from user u
join user_role ur on u.id = ur.user_id
join role_permission rp on ur.role_id = rp.role_id
join permission p on rp.permission_id = p.id
where u.id = #{id}
</select>
<mapper namespace="cn.edu.neusoft.dao.RoleDao">
<select id="getRoleByUserId" resultType="String" parameterType="Long">
select sn from role r
join user_role u on r.id = u.role_id where user_id = #{id}
</select>
到这里我们的授权功能就完成啦。