Springboot + vue前后端分离后台管理系统(十三) -- 访问权限控制

前言

角色菜单可以控制侧边栏的显示,再细一点的粒度就是权限了,可以控制页面或接口是否可以访问。

实现

配置

  • ShiroConfig.java 开启权限注解切点扫描
@Configuration
public class ShiroConfig {

    /**
     * 注入ShiroRealm,自定义的realm 后面的认证和授权全在这里编写
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm() {
        return new ShiroRealm();
    }

    /**
     * 创建SecurityManager
     * @return
     */
    @Bean
    public DefaultWebSecurityManager defaultWebSecurityManager(ShiroRealm shiroRealm) {
        /**
         * securityManager对象,shiroRealm进行托管
         */
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置支持的AuthenticationToken
        shiroRealm.setAuthenticationTokenClass(BearerToken.class);
        securityManager.setRealm(shiroRealm);

        /**
         * 禁用session
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);

        return securityManager;
    }

    /**
     * 过滤器配置
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setLoginUrl("/login");
        // 添加自定义过滤器
        Map<String, Filter> filterMap = new HashMap<>(16);
        filterMap.put("tokenFilter", new BearerTokenFilter());
        factoryBean.setFilters(filterMap);

        /**
         * 自定义拦截规则
         */
        Map<String, String> filterRuleMap = new HashMap<>(16);
        // 对swagger相关url请求不进行拦截
        filterRuleMap.put("/swagger-ui.html", "anon");
        filterRuleMap.put("/doc.html/**", "anon");
        filterRuleMap.put("/swagger-resources/**", "anon");
        filterRuleMap.put("/v2/**", "anon");
        filterRuleMap.put("/webjars/**", "anon");
        filterRuleMap.put("/favicon.ico", "anon");

        // 其余请求都要经过BearerTokenFilter自定义拦截器
        filterRuleMap.put("/**", "tokenFilter");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);

        return factoryBean;
    }


    /**
     * 开启shiro 权限相关注解的切点
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}
  • 接口添加权限注解 @RequiresPermissions()
	@RequiresPermissions("sys:role:view")
    @GetMapping(value = "roles/list")
    @ApiOperation(value = "角色列表", notes = "接口描述")
    public ResResult listRole(){
        return this.roleService.listRole();
    }
  • ShiroRealm返回当前用户权限
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;

    @Autowired
    private MenuService menuService;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 授权方法
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

        String username = JwtUtil.getUsername(UserUtil.getTokenWithoutPrefix(principalCollection.toString()));
        User user = this.userService.getOne(new QueryWrapper<User>().eq("username", username));
        if (user != null) {
            // 获取当前用户拥有的权限
            List<String> perms = this.menuService.findPremByUser(user.getId());
            for (String perm : perms) {
                authorizationInfo.addStringPermission(perm);
            }
        }

        return authorizationInfo;
    }

    /**
     * 认证方法
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 获取身份信息
        BearerToken bearerToken = (BearerToken) authenticationToken;
        // 去掉token前缀
        String token = bearerToken.getToken();
        if (StrUtil.isEmpty(token)) {
            throw new AuthenticationException("token is empty");
        }
        token = token.replace(Constant.TOKEN_PREFIX, "").trim();
        // jwt解析token 获取用户名
        String username = JwtUtil.getUsername(token);
        if (StrUtil.isEmpty(username)) {
            throw new AuthenticationException("token invalid");
        }
        // 查询数据库用户是否存在
        QueryWrapper queryWrapper = new QueryWrapper<User>();
        queryWrapper.eq("username", username);
        User user = userService.getOne(queryWrapper);
        if (user == null) {
            throw new AuthenticationException("User didn't existed!");
        }
        //验证token是否合法
        if (!JwtUtil.verify(token, username, user.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }
        // 验证token 是否过期
        String cacheToken = (String) redisUtil.get(Constant.LOGIN_PREFIX + user.getId());
        if (StrUtil.isEmpty(cacheToken) || !StrUtil.equals(cacheToken, token)) {
            throw new AuthenticationException("cacheToken is empty or token incorrect");
        }
        //验证通过刷新token 时间
        redisUtil.expire(Constant.LOGIN_PREFIX + user.getId(), Constant.TOKEN_EXPIRE);

        return new SimpleAuthenticationInfo(bearerToken.getToken(), bearerToken.getToken(), "shiroRealm");

    }
}
  • 全局异常捕获

@RequiresPermissions()修饰的接口权限不够的话会抛出UnauthorizedException异常,捕获一下返回403给前端,让前端处理

	/**
     * 功能描述: 异常统一返回, 统一返回500状态
     */
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(UnauthorizedException.class)
    public ResResult handleUnauthorizedException(Exception e) {
        log.error("权限异常:{}", e);
        return ResResult.failure(ResResultCode.FORBIDDEN);
    }

页面

静态路由新增403和404页面

export const constantRoutes = [
  {
    path: '/',
    redirect: '/login',
    hidden: true
  },
  {
    path: '/login',
    component: () => import('@/views/login/index'),
    hidden: true
  },
  {
    path: '/errorPage',
    name: 'errorPage',
    component: Layout,
    hidden: true,
    children: [{
      path: '/404',
      component: () => import('@/views/404'),
    },
    {
      path: '/403',
      component: () => import('@/views/403'),
    }]
  }

]

request.js全局响应处理403和404

import axios from 'axios'
import router from '../router'
import store from '@/store'
import { getToken } from '@/utils/auth'
import IMessage from './IMessage'

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
})

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers['Authorization'] = getToken()
    }
    return config
  },
  error => {
    // do something with request error
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
  */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const res = response.data
    // if the custom code is not 200, it is judged as an error.
    if (res.code !== 200) {
      IMessage.error(res.msg || '网络通讯异常,请稍后再试!')
      return Promise.reject(new Error(res.msg || 'Error'))
    } else {
      return res
    }
  },
  error => {
    if (error.response && error.response.status === 401) {
      IMessage.error('登录失效,请重新登录')
      router.replace({
        path: '/login',
        query: { redirect: router.currentRoute.fullPath }
      })
    }
    else if (error.response && error.response.status === 403) {
      router.replace({
        path: '/403'
      })
    }
    else {
      IMessage.error('网络通讯异常,请稍后再试!')
    }

    return Promise.reject(error)
  }
)

export default service

效果

  • 403访问权限不足

  • 404访问路由不存在

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值