精华内容
下载资源
问答
  • java的细粒度权限和shiro权限校验 Spring + Struts + hibernate
  • Spring AOP 实现功能权限校验功能

    万次阅读 2016-06-23 17:11:24
    实现功能权限校验的功能有多种方法,其一使用拦截器拦截请求,其二是使用AOP抛异常。 首先用拦截器实现未登录时跳转到登录界面的功能。注意这里没有使用AOP切入,而是用拦截器拦截,因为AOP一般切入的是service层...

    实现功能权限校验的功能有多种方法,其一使用拦截器拦截请求,其二是使用AOP抛异常。
    首先用拦截器实现未登录时跳转到登录界面的功能。注意这里没有使用AOP切入,而是用拦截器拦截,因为AOP一般切入的是service层方法,而拦截器是拦截控制器层的请求,它本身也是一个处理器,可以直接中断请求的传递并返回视图,而AOP则不可以。

    1.使用拦截器实现未登录时跳转到登录界面的功能

    1.1 拦截器SecurityInterceptor

    package com.jykj.demo.filter;
    
    import java.io.PrintWriter;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.springframework.web.servlet.HandlerInterceptor;
    import org.springframework.web.servlet.ModelAndView;
    
    import com.alibaba.fastjson.JSON;
    import com.jykj.demo.util.Helper;
    import com.jykj.demo.util.Result;
    
    public class SecurityInterceptor implements HandlerInterceptor{
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
                throws Exception {
            System.out.println("SecurityInterceptor:"+request.getContextPath()+","+request.getRequestURI()+","+request.getMethod());
            HttpSession session = request.getSession();
            if (session.getAttribute(Helper.SESSION_USER) == null) {
                System.out.println("AuthorizationException:未登录!"+request.getMethod());
                if("POST".equalsIgnoreCase(request.getMethod())){
                    response.setContentType("text/html; charset=utf-8");  
                    PrintWriter out = response.getWriter();   
                    out.write(JSON.toJSONString(new Result(false,"未登录!")));
                    out.flush();
                    out.close();
                }else{
                    response.sendRedirect(request.getContextPath()+"/login"); 
                }
                return false;
            } else {
                return true;
            }
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
                ModelAndView modelAndView) throws Exception {
            // TODO Auto-generated method stub
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
                throws Exception {
            // TODO Auto-generated method stub
    
        }
    
    }
    

    1.2.spring-mvc.xml(拦截器配置部分)

    <!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
        <mvc:resources mapping="/resources/**" location="/resources/" />
    <mvc:interceptors>
            <mvc:interceptor>
                <mvc:mapping path="/*"/>  <!-- 拦截/  /test  /login  等等单层结构的请求  --> 
                <mvc:mapping path="/**/*.aspx"/><!-- 拦截后缀为.aspx的请求 -->
                <mvc:mapping path="/**/*.do"/><!-- 拦截后缀为 .do的请求 -->
                <mvc:exclude-mapping path="/login"/>
                <mvc:exclude-mapping path="/signIn"/>
                <mvc:exclude-mapping path="/register"/>
                <bean class="com.jykj.demo.filter.SecurityInterceptor">
                </bean>
            </mvc:interceptor>
        </mvc:interceptors>

    这里需要特别说明:拦截器拦截的路径最好是带有后缀名的,否则一些静态的资源文件不好控制,也就是说请求最好有一个统一的格式如 .do 等等,这样匹配与过滤速度会非常快。如果不这样,例如 用 /** 来拦截所有的请求,则页面渲染速度会非常慢,因为资源文件也被拦截了。

    2.使用AOP实现功能权限校验

    对于功能权限校验也可以类似地用拦截器来实现,只不过会拦截所有的请求,对不需要权限校验的请求没有很好的过滤功能,所以采用AOP指定拦截需要校验的方法的方式来实现之。

    2.1 切面类 PermissionAspect

    package com.jykj.demo.filter;
    
    import java.io.IOException;
    import java.lang.reflect.Method;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.beans.factory.annotation.Autowired;
    
    import com.jykj.demo.annotation.ValidatePermission;
    import com.jykj.demo.exception.AccessDeniedException;
    import com.jykj.demo.service.SysUserRolePermService;
    
    /**
     * 事件日志 切面,凡是带有 @ValidatePermission 以及@ResponseBody注解 控制器 都要进行 功能权限检查,
     * 若无权限,则抛出AccessDeniedException 异常,该异常将请求转发至一个控制器,然后将异常结果返回
     * @author Administrator
     *
     */
    public class PermissionAspect {
        @Autowired
        SysUserRolePermService sysUserRolePermService;
    
        public void doBefore(JoinPoint jp) throws IOException{
            System.out.println(
                    "log PermissionAspect Before method: " + jp.getTarget().getClass().getName() + "." + jp.getSignature().getName());
            Method soruceMethod = getSourceMethod(jp);
            if(soruceMethod!=null){
                ValidatePermission oper = soruceMethod.getAnnotation(ValidatePermission.class);
                if (oper != null) {
                    int fIdx = oper.idx();
                    Object[] args = jp.getArgs();
                    if (fIdx>= 0 &&fIdx<args.length){
                        int functionId = (Integer) args[fIdx];
                        String rs = sysUserRolePermService.permissionValidate(functionId);
                        System.out.println("permissionValidate:"+rs);
                        if(rs.trim().isEmpty()){
                            return ;//正常
                        }
                    }
                }
            }
            throw new AccessDeniedException("您无权操作!");
        }
        private Method getSourceMethod(JoinPoint jp){
            Method proxyMethod = ((MethodSignature) jp.getSignature()).getMethod();
            try {
                return jp.getTarget().getClass().getMethod(proxyMethod.getName(), proxyMethod.getParameterTypes());
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (SecurityException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

    2.2自定义注解ValidatePermission

    package com.jykj.demo.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @Descrption该注解是标签型注解,被此注解标注的方法需要进行权限校验
     */
    @Target(value = ElementType.METHOD)
    @Retention(value = RetentionPolicy.RUNTIME)
    @Documented
    public @interface ValidatePermission {
        /**
         * @Description功能Id的参数索引位置  默认为0,表示功能id在第一个参数的位置上,-1则表示未提供,无法进行校验
         */
        int idx() default 0;
    }
    

    说明: AOP切入的是方法,不是某个控制器请求,所以不能直接返回视图来中断该方法的请求,但可以通过抛异常的方式达到中断方法执行的目的,所以在before通知中,如果通过验证直接return返回继续执行连接点方法,否则抛出一个自定义异常AccessDeniedException来中断连接点方法的执行。该异常的捕获可以通过系统的异常处理器(可以看做控制器)来捕获并跳转到一个视图或者一个请求。这样就达到拦截请求的目的。所以需要配置异常处理器。

    2.3 spring-mvc.xml(异常处理器配置,以及aop配置)

    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
            <!-- <property name="defaultErrorView" value="rediret:/error"></property>   -->
            <property name="exceptionMappings">
                <props>
                    <!--<prop key="com.jykj.demo.exception.AuthorizationException">redirect:/login</prop>-->
                    <prop key="com.jykj.demo.exception.AccessDeniedException">forward:/accessDenied</prop>
                </props>
            </property>
        </bean>
    <bean id="aspectPermission" class="com.jykj.demo.filter.PermissionAspect" />
        <!-- 对带有@ValidatePermission和ResponseBody注解的controller包及其子包所有方法执行功能权限校验  --> 
        <aop:config proxy-target-class="true">  
            <aop:aspect ref="aspectPermission">  
                <aop:pointcut id="pc"  
                    expression="@annotation(com.jykj.demo.annotation.ValidatePermission) 
                    and @annotation(org.springframework.web.bind.annotation.ResponseBody) 
                    and execution(* com.jykj.demo.controller..*.*(..)) " />  
                <aop:before pointcut-ref="pc" method="doBefore"/>  
            </aop:aspect>  
        </aop:config>

    2.4 注解需要进行功能校验的控制器请求

    @RequestMapping(value = "/moduleAccess.do", method = RequestMethod.POST, produces="text/html;charset=utf-8")
        @ResponseBody
        @ValidatePermission
        public String moduleAccess(int fid,String action,FrmModule module) {
            System.out.println("fid:"+fid+",action:"+action);
            int rs = -1;
            try{
                if(Helper.F_ACTION_CREATE.equals(action)){
                    rs = moduleService.access(module,Helper.DB_ACTION_INSERT);
                    //module.setModuleid(rs);
                    module = moduleService.selectByPrimaryKey(rs);
                }else if(Helper.F_ACTION_EDIT.equals(action)){
                    rs = moduleService.access(module,Helper.DB_ACTION_UPDATE);
                    module = moduleService.selectByPrimaryKey(module.getModuleid());
                }else if(Helper.F_ACTION_REMOVE.equals(action)){
                    rs = moduleService.access(module,Helper.DB_ACTION_DELETE);
                }else{
                    return JSON.toJSONString(new Result(false,"请求参数错误:action"));
                }
            }catch(Exception e){
                e.printStackTrace();
                return JSON.toJSONString(new Result(false,"操作失败,出现异常,请联系管理员!"));
            }
            if(rs<0){
                return JSON.toJSONString(new Result(false,"操作失败,请联系管理员!"));
            }
            return  JSON.toJSONString(new Result(true,module));
        }

    2.5 异常处理器将请求转发到的控制器请求 forward:/accessDenied

    @RequestMapping(value = "/accessDenied",produces = "text/html;charset=UTF-8")
    @ResponseBody
    public String accessDenied(){
        return JSON.toJSONString(new Result(false,"您没有权限对此进行操作!"));
    }

    2.6 请求校验不通过时 由上述的控制器返回 结果本身

    如下所示:

    {"info":"您没有权限对此进行操作!","success":false}

    2.7 功能校验service 示例

    /**
         * 校验当前用户在某个模块的某个功能的权限
         * @param functionId
         * @return 空字符串表示 有权限 ,否则是错误信息
         * @throws Exception 
         */
        public String permissionValidate(int functionId){
            Object o =  request.getSession().getAttribute(Helper.SESSION_USER);
            //if(o==null)  throw new AuthorizationException(); 
            SysUser loginUser= (SysUser)o;
            if(loginUser.getUserid() == 1) return "";
            try{
                return mapper.permissionValidate(loginUser.getUserid(),functionId);
            }catch(Exception ex){
                ex.printStackTrace();
                return "数据库操作出现异常!";
            }
        }

    说明: 这里仅仅是对带有@ValidatePermission和@ResponseBody注解的controller包及其子包所有方法进行切入,这样肯定是不够通用的,应该是对带有@ValidatePermission的方法进行切入,在切面类中通过判断该方法是否有@ResponseBody注解来抛出不一样的异常,若带有@ResponseBody注解则抛出上述的异常返回json字符串,
    否则,应该抛出另一个自定义异常然后将请求重定向到一个合法的视图如error.jsp .

    通过客户端发送 /moduleAccess.do 请求,该请求对应的方法同时具有@ValidatePermission和@ResponseBody,并且有功能Id参数fid,这样AOP可以切入该方法,执行doBefore通知,通过功能参数fid,对它结合用户id进行权限校验,若校验通过直接返回,程序继续执行,否则抛出自定义异常AccessDeniedException,该异常由系统捕获(需要配置异常处理器)并发出请求 forward:/accessDenied ,然后对应的控制器 /accessDenied 处理该请求返回一个包含校验失败信息的json给客户端。这样发送 /moduleAccess.do 请求,如果校验失败,转发到了/accessDenied请求,否则正常执行。绕了这么一个大圈子才实现它。

    展开全文
  • 这两天在写项目的全局权限校验,用 Zuul 作为服务网关,在 Zuul 的前置过滤器里做的校验。 权限校验或者身份验证就不得不提 Token,目前 Token 的验证方式有很多种,有生成 Token 后将 Token 存储在 Redis 或数据库...

    这两天在写项目的全局权限校验,用 Zuul 作为服务网关,在 Zuul 的前置过滤器里做的校验。

    权限校验或者身份验证就不得不提 Token,目前 Token 的验证方式有很多种,有生成 Token 后将 Token 存储在 Redis 或数据库的,也有很多用 JWT(JSON Web Token)的。

    说实话这方面我的经验不多,又着急赶项目,所以就先用个简单的方案。

    登录成功后将 Token 返回给前端,同时将 Token 存在 Redis 里。每次请求接口都从 Cookie 或 Header 中取出 Token,在从 Redis 中取出存储的 Token,比对是否一致。

    我知道这方案不是最完美的,还有安全性问题,容易被劫持。但目前的策略是先把项目功能做完,上线之后再慢慢优化,不在一个功能点上扣的太细,保证项目进度不至于太慢。

    项目地址:https://github.com/cachecats/coderiver

    本文将分四部分介绍

    1. 登录逻辑
    2. AuthFilter 前置过滤器校验逻辑
    3. 工具类
    4. 演示验证

    一、登录逻辑

    登录成功后,将生成的 Token 存储在 Redis 中。用 String 类型的 key, value 格式存储,key是 TOKEN_userId,如果用户的 userId 是 222222,那键就是 TOKEN_222222;值是生成的 Token。

    只贴出登录的 Serive 代码

    @Override
    public UserInfoDTO loginByEmail(String email, String password) {
    
        if (StringUtils.isEmpty(email) || StringUtils.isEmpty(password)) {
            throw new UserException(ResultEnum.EMAIL_PASSWORD_EMPTY);
        }
    
        UserInfo user = userRepository.findUserInfoByEmail(email);
        if (user == null) {
            throw new UserException(ResultEnum.EMAIL_NOT_EXIST);
        }
        if (!user.getPassword().equals(password)) {
            throw new UserException(ResultEnum.PASSWORD_ERROR);
        }
    
        //生成 token 并保存在 Redis 中
        String token = KeyUtils.genUniqueKey();
        //将token存储在 Redis 中。键是 TOKEN_用户id, 值是token
        redisUtils.setString(String.format(RedisConsts.TOKEN_TEMPLATE, user.getId()), token, 2l, TimeUnit.HOURS);
    
        UserInfoDTO dto = new UserInfoDTO();
        BeanUtils.copyProperties(user, dto);
        dto.setToken(token);
    
        return dto;
    }
    

    二、AuthFilter 前置过滤器

    AuthFilter 继承自 ZuulFilter,必须实现 ZuulFilter 的四个方法。

    filterType(): Filter 的类型,前置过滤器返回 PRE_TYPE

    filterOrder(): Filter 的顺序,值越小越先执行。这里的写法是 PRE_DECORATION_FILTER_ORDER - 1, 也是官方建议的写法。

    shouldFilter(): 是否应该过滤。返回 true 表示过滤,false 不过滤。可以在这个方法里判断哪些接口不需要过滤,本例排除了注册和登录接口,除了这两个接口,其他的都需要过滤。

    run(): 过滤器的具体逻辑

    为了方便前端,考虑到要给 pc、app、小程序等不同平台提供服务,token 设置在 cookie 和 header 任选一均可,会先从 cookie 中取,cookie 中没有再从 header 中取。

    package com.solo.coderiver.gateway.filter;
    
    import com.google.gson.Gson;
    import com.netflix.zuul.ZuulFilter;
    import com.netflix.zuul.context.RequestContext;
    import com.netflix.zuul.exception.ZuulException;
    import com.solo.coderiver.gateway.VO.ResultVO;
    import com.solo.coderiver.gateway.consts.RedisConsts;
    import com.solo.coderiver.gateway.utils.CookieUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.util.StringUtils;
    
    import javax.servlet.http.Cookie;
    import javax.servlet.http.HttpServletRequest;
    
    import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_DECORATION_FILTER_ORDER;
    import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
    
    /**
     * 权限验证 Filter
     * 注册和登录接口不过滤
     *
     * 验证权限需要前端在 Cookie 或 Header 中(二选一即可)设置用户的 userId 和 token
     * 因为 token 是存在 Redis 中的,Redis 的键由 userId 构成,值是 token
     * 在两个地方都没有找打 userId 或 token其中之一,就会返回 401 无权限,并给与文字提示
     */
    @Slf4j
    @Component
    public class AuthFilter extends ZuulFilter {
    
        @Autowired
        StringRedisTemplate stringRedisTemplate;
    
        //排除过滤的 uri 地址
        private static final String LOGIN_URI = "/user/user/login";
        private static final String REGISTER_URI = "/user/user/register";
    
        //无权限时的提示语
        private static final String INVALID_TOKEN = "invalid token";
        private static final String INVALID_USERID = "invalid userId";
    
        @Override
        public String filterType() {
            return PRE_TYPE;
        }
    
        @Override
        public int filterOrder() {
            return PRE_DECORATION_FILTER_ORDER - 1;
        }
    
        @Override
        public boolean shouldFilter() {
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            log.info("uri:{}", request.getRequestURI());
            //注册和登录接口不拦截,其他接口都要拦截校验 token
            if (LOGIN_URI.equals(request.getRequestURI()) ||
                    REGISTER_URI.equals(request.getRequestURI())) {
                return false;
            }
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            RequestContext requestContext = RequestContext.getCurrentContext();
            HttpServletRequest request = requestContext.getRequest();
    
            //先从 cookie 中取 token,cookie 中取失败再从 header 中取,两重校验
            //通过工具类从 Cookie 中取出 token
            Cookie tokenCookie = CookieUtils.getCookieByName(request, "token");
            if (tokenCookie == null || StringUtils.isEmpty(tokenCookie.getValue())) {
                readTokenFromHeader(requestContext, request);
            } else {
                verifyToken(requestContext, request, tokenCookie.getValue());
            }
    
            return null;
        }
    
        /**
         * 从 header 中读取 token 并校验
         */
        private void readTokenFromHeader(RequestContext requestContext, HttpServletRequest request) {
            //从 header 中读取
            String headerToken = request.getHeader("token");
            if (StringUtils.isEmpty(headerToken)) {
                setUnauthorizedResponse(requestContext, INVALID_TOKEN);
            } else {
                verifyToken(requestContext, request, headerToken);
            }
        }
    
        /**
         * 从Redis中校验token
         */
        private void verifyToken(RequestContext requestContext, HttpServletRequest request, String token) {
            //需要从cookie或header 中取出 userId 来校验 token 的有效性,因为每个用户对应一个token,在Redis中是以 TOKEN_userId 为键的
            Cookie userIdCookie = CookieUtils.getCookieByName(request, "userId");
            if (userIdCookie == null || StringUtils.isEmpty(userIdCookie.getValue())) {
                //从header中取userId
                String userId = request.getHeader("userId");
                if (StringUtils.isEmpty(userId)) {
                    setUnauthorizedResponse(requestContext, INVALID_USERID);
                } else {
                    String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userId));
                    if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
                        setUnauthorizedResponse(requestContext, INVALID_TOKEN);
                    }
                }
            } else {
                String redisToken = stringRedisTemplate.opsForValue().get(String.format(RedisConsts.TOKEN_TEMPLATE, userIdCookie.getValue()));
                if (StringUtils.isEmpty(redisToken) || !redisToken.equals(token)) {
                    setUnauthorizedResponse(requestContext, INVALID_TOKEN);
                }
            }
        }
    
    
        /**
         * 设置 401 无权限状态
         */
        private void setUnauthorizedResponse(RequestContext requestContext, String msg) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
    
            ResultVO vo = new ResultVO();
            vo.setCode(401);
            vo.setMsg(msg);
            Gson gson = new Gson();
            String result = gson.toJson(vo);
    
            requestContext.setResponseBody(result);
        }
    }
    

    三、工具类

    MD5 工具类

    package com.solo.coderiver.user.utils;
    
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    
    /**
     * 生成 MD5 的工具类
     */
    public class MD5Utils {
    
        public static String getMd5(String plainText) {
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                md.update(plainText.getBytes());
                byte b[] = md.digest();
    
                int i;
    
                StringBuffer buf = new StringBuffer("");
                for (int offset = 0; offset < b.length; offset++) {
                    i = b[offset];
                    if (i < 0)
                        i += 256;
                    if (i < 16)
                        buf.append("0");
                    buf.append(Integer.toHexString(i));
                }
                //32位加密
                return buf.toString();
                // 16位的加密
                //return buf.toString().substring(8, 24);
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * 加密解密算法 执行一次加密,两次解密
         */
        public static String convertMD5(String inStr){
    
            char[] a = inStr.toCharArray();
            for (int i = 0; i < a.length; i++){
                a[i] = (char) (a[i] ^ 't');
            }
            String s = new String(a);
            return s;
    
        }
    }
    

    生成 key 的工具类

    package com.solo.coderiver.user.utils;
    
    import java.util.Random;
    
    public class KeyUtils {
    
        /**
         * 产生独一无二的key
         */
        public static synchronized String genUniqueKey(){
            Random random = new Random();
            int number = random.nextInt(900000) + 100000;
            String key = System.currentTimeMillis() + String.valueOf(number);
            return MD5Utils.getMd5(key);
        }
    }
    

    四、演示验证

    在 8084 端口启动 api_gateway 项目,同时启动 user 项目。

    用 postman 通过网关访问登录接口,因为过滤器对登录和注册接口排除了,所以不会校验这两个接口的 token。

    可以看到,访问地址 http://localhost:8084/user/user/login 登录成功并返回了用户信息和 token。

    此时应该把 token 存入 Redis 中了,用户的 id 是 111111 ,所以键是 TOKEN_111111,值是刚生成的 token 值

    再来随便请求一个其他的接口,应该走过滤器。

    header 中不传 token 和 userId,返回 401

    只传 token 不传 userId,返回401并提示 invalid userId

    token 和 userId 都传,但 token 不对,返回401,并提示 invalid token

    同时传正确的 token 和 userId,请求成功

    以上就是简单的 Token 校验,如果有更好的方案欢迎在评论区交流


    代码出自开源项目 CodeRiver,致力于打造全平台型全栈精品开源项目。

    coderiver 中文名 河码,是一个为程序员和设计师提供项目协作的平台。无论你是前端、后端、移动端开发人员,或是设计师、产品经理,都可以在平台上发布项目,与志同道合的小伙伴一起协作完成项目。

    coderiver河码 类似程序员客栈,但主要目的是方便各细分领域人才之间技术交流,共同成长,多人协作完成项目。暂不涉及金钱交易。

    计划做成包含 pc端(Vue、React)、移动H5(Vue、React)、ReactNative混合开发、Android原生、微信小程序、java后端的全平台型全栈项目,欢迎关注。

    项目地址:https://github.com/cachecats/coderiver


    您的鼓励是我前行最大的动力,欢迎点赞,欢迎送小星星✨ ~

    展开全文
  • 自己实现注解式权限校验(SpringBoot)

    千次阅读 2019-12-20 10:47:29
    权限校验是很多情况都会用到的,结合Java注解和拦截器,直接在Controller层的方法上添加一个注解,可以无侵入式的进行权限校验。 一.Java注解 1.RequestMapping 我们打开一个最常用的Spring注解 可以看到,...

    自己实现注解式权限校验(SpringBoot)

    权限校验是很多情况都会用到的,结合Java注解和拦截器,直接在Controller层的方法上添加一个注解,可以无侵入式的进行权限校验。

    一.Java注解

    1.RequestMapping

    我们打开一个最常用的Spring注解

    在这里插入图片描述
    可以看到,RequestMapping注解上,还有几个注解,分别代表

    Target:注解目标(如:可以在方法、类、参数中使用)

    Retention:是注解的注解,称为元注解。 分为3类 :

    • RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
    • RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
    • RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

    Documented:表明这个注释是由 javadoc记录的。

    Mapping:Spring的注解,不做过多说明

    二.自定义一个判断权限的注解

    1.Permission.java

    在了解了注解的组成后,我们可以尝试着,自己编写一个注解。用来判断权限。

    注意:此注解使用了Spring的AliasFor注解,改注解成对出现,表示双方互为别名属性。(既设置了name,则value同样有该值),使用AliasFor注解后,必须用Spring的AnnotationUtils工具获取注解才可以达到上述效果。

    import org.springframework.core.annotation.AliasFor;
    
    import java.lang.annotation.*;
    
    /**
     * @author litong
     * @date 2019/11/29 16:18
     */
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Documented
    public @interface Permission {
    
    	/**
    	 * 权限
    	 */
    	@AliasFor("value")
    	PermissionEnum[] name() default {};
    
    	/**
    	 * 权限
    	 */
    	@AliasFor("name")
    	PermissionEnum[] value() default {};
    
    }
    
    

    2.PermissionEnum.java

    权限的枚举,使用枚举可以更好的说明参数,易读性高,且可以减少报错几率。

    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    /**
     * @author litong
     * @date 2019/11/29 16:20
     */
    @Getter
    @AllArgsConstructor
    public enum PermissionEnum {
       /**
        * 用户管理权限
        */
       USER(1, "用户管理权限"),
       /**
        * 教师管理权限
        */
       TEACHER(2, "教师管理权限"),
    
       /**
        * 无需校验,
        */
       NO(-99999, "无需权限"),
       ;
       /**
        * 权限编码
        */
       private Integer code;
       /**
        * 权限名称
        */
       private String msg;
    }
    

    三.自定义拦截器

    注:此拦截器,一般还会有未登录拦截,Token解析,用户注入、权限校验等功能,现只展示其中之一,权限校验。

    1.AuthenticationInterceptor.java

    /**
     * @Author: litong
     * @Date: 2019-09-20 11:50
     * @Description: 拦截器
     */
    @Slf4j
    public class AuthenticationInterceptor implements HandlerInterceptor {
        /**
    	 * 在业务处理器处理请求之前被调用
    	 *
    	 * @param request
    	 * @param response
    	 * @param handler
    	 * @return
    	 * @throws Exception
    	 */
    	@Override
    	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    		// 如果不是映射到方法直接通过
    		if (!(handler instanceof HandlerMethod)) {
    			return true;
    		}
             // 获取方法中的注解
             HandlerMethod handlerMethod = (HandlerMethod) handler;
    		Method method = handlerMethod.getMethod();
            // 省略判断是否需要登录的方法.....
            // 省略Token解析的方法.....
            // 此处根据自己的系统架构,通过Token或Cookie等获取用户信息。
            UserInfo userInfo = userService.getUserByToken(token);
            
            // 获取类注解
    		Permission permissionClass = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), Permission.class);
    		// 获取方法注解
    		Permission permissionMethod = AnnotationUtils.findAnnotation(method,Permission.class);
    		// 判断是否需要权限校验
    		if (permissionClass == null && permissionMethod == null) {
    			// 不需要校验权限,直接放行
    			return true;
    		}
            // 获取该方法注解,优先级:方法注解>类注解
    		PermissionEnum[] permissionEnums;
    		if (permissionClass != null && permissionMethod == null) {
    			// 类注解不为空,方法注解为空,使用类注解
    			permissionEnums = permissionClass.name();
    		} else if (permissionClass == null) {
    			// 类注解为空,使用方法注解
    			permissionEnums = permissionMethod.name();
    		} else {
    			// 都不为空,使用方法注解
    			permissionEnums = permissionMethod.name();
    		}
            // 校验该用户是否有改权限
            // 校验方法可自行实现,拿到permissionEnums中的参数进行比较
            if(userService.checkPermissionForUser(userInfo,permissionEnums)){
                // 拥有权限
                return true;
            } else {
                // 抛出自定义异常,可在全局异常捕获后自行处理。
                throw new AuthTokenException(CheckConstants.PERMISSION_ERROR);
            }
    
        }
        
        /**
    	 * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
    	 *
    	 * @param httpServletRequest
    	 * @param httpServletResponse
    	 * @param o
    	 * @param modelAndView
    	 * @throws Exception
    	 */
    	@Override
    	public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    
    	}
    
    	/**
    	 * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
    	 *
    	 * @param httpServletRequest
    	 * @param httpServletResponse
    	 * @param o
    	 * @param e
    	 * @throws Exception
    	 */
    	@Override
    	public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    	}
    }
    

    四.使用

    1.类(该类中,所有方法校验权限)

    /**
     * @author litong
     * @date 2019/9/20 13:26
     */
    @RestController
    @RequestMapping("/auth")
    @AllArgsConstructor
    @Slf4j
    @Permission({PermissionEnum.USER})
    public class AuthController {
        
    }
    

    2.方法(此方法校验权限,优先级大于类上的注解)

    @PostMapping("/get")
    @Permission({PermissionEnum.USER})
    public R get() {
    }
    
    展开全文
  • 使用AOP实现权限拦截校验

    千次阅读 2019-04-14 11:14:51
    aop的好处就在于它可以只让你写一次代码,然后这些代码就可以用于容器...* 被该注释修饰的方法都会经过切面拦截校验权限 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ...

    aop的好处就在于它可以只让你写一次代码,然后这些代码就可以用于容器当中的所有对象,权限拦截校验是一个表现形式。

    那么,我们要如何实现这个功能?

    首先,定义一个注解:

    /*
    * 被该注释修饰的方法都会经过切面拦截校验权限
    */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface Permission {
        PermissionEnum value();
        String msg() default "";
    }

    这里有一个PermissionEnum枚举类,当然可以直接使用String 代替,不过不推荐,其源码如下:

    public enum PermissionEnum {
    
        LOGIN("login"), // 登录权限
        CREATE_ADMIN("createAdmin"), // 创建其他管理员的权限
        DASHBOARD_VIEW("dashboardView"), // 查看仪表盘数据的权限
        USER_DATA_VIEW("userDataView"), // 查看用户数据的权限
        ;
        private String permission;
    
        PermissionEnum(String permission) {
            this.permission = permission;
    
        }
    
        public String getPermission() {
            return permission;
        }
    }

    这里,我们控制的权限颗粒度是方法,所以必须使用一个标识符来标志出每个方法,我们只要将Permission注解加到方法之上,就能被切面所拦截,并进行权限校验:

    @Permission(PermissionEnum.USER_DATA_VIEW)
        public List<User> findAll(Integer page,Integer length){
            Pageable pageable = PageRequest.of(page,length);
            Page<User> userPage = userRepository.findAll(pageable);
            return userPage.stream().collect(Collectors.toList());
    
        }

    上面是一个方法示例:

    接下来就是重点了,切入点的编写:

    @Before("pointCut()")
        public void before(JoinPoint joinPoint){
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature = (MethodSignature) signature;
            Method method = methodSignature.getMethod();
            var a = method.getAnnotation(Permission.class);
    
            String msg = null;
            if ("".equals(a.msg())){
                msg = "没有"+a.value().getPermission()+"权限";
            }else{
                msg = a.msg();
            }
            var permission = adminPermissionService.getCurrentAdminPermission();
    
            try {
                String methodName = a.value().getPermission();
                methodName = methodName.substring(0,1).toUpperCase()+methodName.substring(1,methodName.length());
                Method method1 = permission.getClass().getMethod("get"+methodName);
                Boolean ret = (Boolean) method1.invoke(permission);
                if (ret == null){
                    ErrorUtils.error(msg);
                }
    
                if (!ret){
                    ErrorUtils.error(msg);
                }
            } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                throw new RuntimeException(e.getMessage());
            }
    
        }

    代码虽然有点长,但做的事并不多,无非就是获取当前登录管理员的权限,然后判断该其切点的权限要求当前管理是否满足,满足就继续运行,否则抛出一个权限错误的异常。

    当然,这个权限拦截校验方案是写死的,也就是说你无法根据实际的情况需要添加或者删除的相应权限,因为这是由系统的权限控制方案所限定的,我们权限拦截的目标是方法,不像其他诸如URL的拦截方案,想要修改方法,即拦截目标,就必须修改源码,所以也就没有必要做成高度可自定义化的权限方案。

    展开全文
  • 前面学习了如何自定义一个注解:java如何优雅的自定义一个注解 下面来实战演示一下如何使用自定义注解做一些实际的功能。比如校验对象属性是否为空。 一、自定义一个NotNull注解 我们自定义一个NotNull注解,里面有...
  • Java注解实现权限管理

    千次阅读 2020-07-02 22:21:09
    关于java注解介绍请参见 Java自定义注解实现权限管理 基础实现 在每个controller方法中添加用户校验代码,这种可以控制到方法级,但是每个方法都要维护这段重复逻辑。 @RequestMapping(value = "/task/progress", ...
  • 上级给了一个权限校验小程序的后端demo,是通过自定义注解、jwt来实现校验token的。但是我就是没看明白这个拦截器的部分,请问一下Authorize是加到哪里的注解?是加到小程序发起的请求里吗? 自定义注解Authorize: ...
  • 而spring的面向切面的特效可以帮助我们很好的实现动态的权限校验。这里我们就用到的spring的aop。接下来就带领大家用aop和注解来快速的实现权限校验 一,在pom.xml里引入aop的类库。 &lt;!--aop切面的使用--&...
  • 自己参考网上手写的代码,只能算是1.0版本吧,加密、解密、签名、校验都有,并附有部分备注。有的方法封装的不是特别好,等后几个版本再优化吧,这个版本比较适合刚刚接触,想自己实现的童鞋吧,一起学习。
  • java web简单权限管理设计

    万次阅读 多人点赞 2015-03-19 23:23:05
    推荐最新技术springboot版权限管理(java后台通用权限管理系统(springboot)),采用最新技术架构,功能强大! 注:由于该项目比较老,所以没有采用maven管理,建议下载springboot权限管理系统,对学习和使用会更有...
  • 通过SpringBoot自定义注解实现AOP角色权限校验之前,首先先要了解一下注解的基本知识: Annotation是Java重要的组成部分,从J2SE 5.0时代就已经存在了。在我们的代码中,我们随处可以看到许多注解,例如@Autowired...
  • 其实就是使用过滤器,在逻辑层加上一个过滤器来实现登录状态的校验,然后再将信息跳转到指定的界面或者Servlet。 我们先来分析下具体的使用流程: 假如用户处于登录状态则用户可以访问指定的网页,没有登录的用户...
  • java生成MD5校验码及算法实现

    千次阅读 2012-04-12 14:53:19
    Java中,java.security.MessageDigest (rt.jar中)已经定义了 MD5 的计算,所以我们只需要简单地调用即可得到 MD5 的128 位整数。然后将此 128 位计 16 个字节转换成 16 进制表示即可。     下面是一个可...
  • 所有被GetMapping注解修饰的方法会织入advice @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") private void logAdvicePointcut() {} // Before表示logAdvice将...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 65,057
精华内容 26,022
关键字:

java实现方法的权限校验

java 订阅