授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。
:::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>
到这里我们的授权功能就完成啦。