JavaWeb 权限管理设计与实现

  • 序言
  • 项目准备
  • 项目结构
  • Shiro配置
  • 表单提交与ajax请求对于权限成功、失败的处理
  • 前端页面按钮权限的隐藏与显示


序言

在开发过程中,权限是个很重要的模块,我们在做权限管理的时候采用的按角色分配权限,首先要设计好用户–角色–资源–权限这四者间的关系,接着要考虑前后台权限的控制,然后要确定好实现方式,现在主流的权限管理一个是Spring security,一个是shiro。前者功能强大,但是配置复杂,不易上手,后者功能简单,配置也很简单,所以此次记录下shiro的使用。

项目准备

1、spring boot 2.2.6.RELEASE
2、mybatis plus
3、mysql
4、shiro
5、freemarker
前端框架采用的是layui

项目结构

java菜单权限控制 javaweb权限控制_spring boot


pom.xml

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <junit.version>4.12</junit.version>
    <spring.boot.version>2.2.6.RELEASE</spring.boot.version>
    <apache.commons.lang3.version>3.10</apache.commons.lang3.version>
    <mysql.java.version>8.0.16</mysql.java.version>
    <mybatis-plus.version>3.3.1</mybatis-plus.version>
    <fastjson.version>1.2.68</fastjson.version>
    <druid.spring.boot.version>1.1.20</druid.spring.boot.version>
    <lombok.version>1.18.12</lombok.version>
    <shiro.version>1.5.2</shiro.version>
    <shiro-freemarker-tags.versoin>1.0.1</shiro-freemarker-tags.versoin>
  </properties>
<dependencies>
        <dependency>
            <groupId>com.yml.easy</groupId>
            <artifactId>easy-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>net.mingsoft</groupId>
            <artifactId>shiro-freemarker-tags</artifactId>
        </dependency>
    </dependencies>

application.yml

spring:
  application:
    name: so-easy
  freemarker:
    enabled: true
    template-loader-path: classpath:/templates/
    suffix: .html
    charset: utf-8
    content-type: text/html
    cache: false
  datasource:
    druid:
      url: jdbc:mysql://localhost:3306/so-easy?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
      username: root
      password: root
      driver-class-name: com.mysql.cj.jdbc.Driver

Shiro配置

其实配置shiro很简单,最主要的就是要两个类,EasyUserRealmShiroConfig,因为在实际运用中,项目中都有自己的密码验证实现,所以,这里又写了一个密码验证类EasyCredentialsMatcher
EasyUserRealm.java

package com.yml.easy.config.shiro;

import com.alibaba.fastjson.JSONObject;
import com.yml.easy.entity.SysUserEntity;
import com.yml.easy.service.SysUserService;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
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 org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * @author: YML
 * @create: 2020-04-18 14:30
 */
@Component
public class EasyUserRealm extends AuthorizingRealm {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Resource
    private SysUserService sysUserService;

    /**
     * 权限认证
     * @author YML
     * @date 2020/4/18 16:14
     * @param principals    principals
     * @return org.apache.shiro.authz.AuthorizationInfo
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SysUserEntity user = (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        // 权限
        Set<String> permissions = new HashSet<>();
        //查询角色
        //查询权限(实际是从数据库查询)
        permissions.addAll(Arrays.asList("admin","dev"));
        authorizationInfo.setStringPermissions(permissions);
        return authorizationInfo;
    }

    /**
     * 登录验证
     * @author YML
     * @date 2020/4/18 14:32
     * @param token token
     * @return org.apache.shiro.authc.AuthenticationInfo
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info("执行登录认证:{}", JSONObject.toJSONString(token));
        String username = (String) token.getPrincipal();
        //获取数据库用户信息SysUserEntity为自定义的数据库映射 bean
        SysUserEntity userEntity = sysUserService.queryUserByLoginOrPhone(username).getData();
        if (userEntity == null) {
            // 账号不存在
            throw new UnknownAccountException("账户不存在");
        }
        //判断状态
        if (userEntity.getUserStatus()!=1){
            // 账号被锁定
            throw new LockedAccountException("用户不可用");
        }
        String salt = userEntity.getUserSlat();
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userEntity,
                userEntity.getUserPass(),
                ByteSource.Util.bytes(salt),
                getName());

        return authenticationInfo;
    }

    /**
     * 设置自定义密码验证
     * @author YML
     * @date 2020/4/18 16:14
     * @param credentialsMatcher    credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {

        super.setCredentialsMatcher(new EasyCredentialsMatcher());
    }
}

ShiroConfig

package com.yml.easy.config.shiro;

import com.jagregory.shiro.freemarker.ShiroTags;
import freemarker.template.TemplateException;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;

import javax.annotation.Resource;
import java.io.IOException;

/**
 * shiro配置
 *
 * @author: YML
 * @create: 2020-04-18 14:32
 */
@Configuration
public class ShiroConfig {

    @Resource
    private EasyUserRealm easyUserRealm;

    /**
     * 配置URL过滤器
     * @author      YML
     * @date        2019/11/3 20:43
     * @return      org.apache.shiro.spring.web.config.ShiroFilterChainDefinition
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();

        chainDefinition.addPathDefinition("/layui/**","anon");
        chainDefinition.addPathDefinition("/css/**", "anon");
        chainDefinition.addPathDefinition("/js/**","anon");
        chainDefinition.addPathDefinition("/images/**", "anon");
        chainDefinition.addPathDefinition("/json/**", "anon");
        chainDefinition.addPathDefinition("/plugins/**", "anon");

        // all other paths require a logged in user
        chainDefinition.addPathDefinition("/","anon");
        chainDefinition.addPathDefinition("/login","anon");
        chainDefinition.addPathDefinition("/logout","logout");
        chainDefinition.addPathDefinition("/sysUser/login","anon");
        chainDefinition.addPathDefinition("/**", "user");
        return chainDefinition;
    }

    /**
     * 配置security并设置userReaml,避免xxxx required a bean named 'authorizer' that could not be found.的报错
     * @author      YML
     * @date        2019/11/3 20:44
     * @return      org.apache.shiro.mgt.SessionsSecurityManager
     */
    @Bean
    public SessionsSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(easyUserRealm);
        return securityManager;
    }

    /**
     * 开启Shiro的注解,需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @author      YML
     * @date        2019/11/3 20:45
     * @return      org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() throws IOException, TemplateException {
        FreeMarkerConfigurer freeMarkerConfigurer = new FreeMarkerConfigurer();
        freeMarkerConfigurer.setTemplateLoaderPath("classpath:templates/");
        freemarker.template.Configuration configuration = freeMarkerConfigurer.createConfiguration();
        configuration.setDefaultEncoding("UTF-8");
        //这里可以添加其他共享变量 比如sso登录地址
        configuration.setSharedVariable("shiro", new ShiroTags());
        freeMarkerConfigurer.setConfiguration(configuration);
        return freeMarkerConfigurer;
    }
}

对于shiroFilterChainDefinition()方法中对Url进行过滤时,第二个参数代表着过滤条件,具体可见如下

java菜单权限控制 javaweb权限控制_apache_02


EasyCredentialsMatcher

package com.yml.easy.config.shiro;

import com.yml.easy.util.EncryptionUtils;
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.authc.credential.CredentialsMatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * shiro认证密码比较
 *
 * @author: YML
 * @create: 2020-04-18 15:05
 */
public class EasyCredentialsMatcher implements CredentialsMatcher {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 密码校验
     * @author YML
     * @date 2020/4/18 15:05
     * @param authToken     登录信息
     * @param info      用户信息
     * @return boolean
     */
    @Override
    public boolean doCredentialsMatch(AuthenticationToken authToken, AuthenticationInfo info) {
        logger.info("密码校验。。。");
        UsernamePasswordToken token = (UsernamePasswordToken) authToken;
        String tokenPass = String.valueOf(token.getPassword());
        SimpleAuthenticationInfo authenticationInfo = (SimpleAuthenticationInfo) info;
        String password = authenticationInfo.getCredentials().toString();
        String slat = new String(authenticationInfo.getCredentialsSalt().getBytes());
        return EncryptionUtils.md5Check(password,tokenPass,slat);
    }
}

然后在写个controller

@RestController
@RequestMapping("/sysUser/")
public class SysUserController {

    @Resource
    private SysUserService sysUserService;

    /**
     * 用户登录
     * @author YML
     * @date 2020/4/18 15:38
     * @param entity    entity
     * @param request   request
     * @return com.yml.easy.meta.CallResult<java.util.Map<java.lang.String,java.lang.Object>>
     */
    @RequestMapping("login")
    public CallResult<Map<String,Object>> login(SysUserEntity entity, HttpServletRequest request){

        CallResult<Map<String,Object>> callResult = CallResult.newInstance();
        if (null==entity || ParamUtils.isEmpty(entity.getUserLogin()) || ParamUtils.isEmpty(entity.getUserPass())){
            callResult.setBaseResponse(BaseResponse.LOSE_USERNAME_PASSWORD);
            return callResult;
        }
        Subject sub = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(entity.getUserLogin(), entity.getUserPass());
        try {
            sub.login(token);
            return callResult;
        } catch (UnknownAccountException e) {
            return CallResult.newInstance(BaseResponse.USER_NOT_EXIST);
        }
        catch (LockedAccountException e) {
            return CallResult.newInstance(BaseResponse.NO_LOGIN_PERMISSION);
        }catch (IncorrectCredentialsException ice) {
            return CallResult.newInstance(BaseResponse.PASSWORD_ERROR);
        }
    }

}

至此,后台的配置就已经完成了,对于后台的请求方法权限,我们可以通过注解来进行,shiro常用的注解如下:

java菜单权限控制 javaweb权限控制_ci_03


按照当前的配置,常用的就是@RequiresPermissions,如

@RequestMapping("login")
    @RequiresPermissions({"admin"})
    public void addUser(){
    }

表单提交与ajax请求对于权限成功、失败的处理

通过spring的全局异常拦截进行处理

package com.yml.easy.handler;

import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;

/**
 * 异常处理机制
 *
 * @author: YML
 * @create: 2020-04-20 20:38
 */
@ControllerAdvice
public class EasyExceptionHandler {

    @ExceptionHandler(AuthorizationException.class)
    public Object authorizationExceptionHandler(AuthorizationException e, HttpServletRequest request) {
    //根据请求类型进行转发
        if (isAjax(request)){
            return "redirect:/err/authorizationExceptionJson";
        }else{
            return "redirect:/err/authorizationExceptionPage";
        }
    }


    /**
     * 判断是否是Ajax请求
     * @author YML
     * @date 2020/4/20 21:07
     * @param request   request
     * @return boolean
     */
    public boolean isAjax(HttpServletRequest request) {
        return (request.getHeader("X-Requested-With") != null &&
                "XMLHttpRequest".equals(request.getHeader("X-Requested-With").toString()));
    }

}

前端页面按钮权限的隐藏与显示

java菜单权限控制 javaweb权限控制_apache_04