跟我学Springboot开发后端管理系统8:Matrxi-Web权限设计实现

2020/05/08

上篇文章讲述了Matrix-web整体实现的权限控制的思路。现在来回顾一下:

  • 首先,用户需要登录,填用户名、密码,后端接收到登录请求,进行用户、密码的校验,校验成功后则根据用户名生成Token,并返回给浏览器。
  • 浏览器收到Token后,会存储在本地的LocalStorge里。
  • 后续浏览器发起请求时都携带该Token,请求达到后端后,会在Filter进行判断,首选判断是否为白名单url(比如登录接口url),如果是则放行;否则进入Token验证。如果有Token且解析成功,则放行,否则,返回无权限访问。
  • Filter判断后,请求达到具体的Controller层,如果在Controller层上加上了权限判断的注解,则生成代理类。代理类在执行具体方法前会根据Token判断权限。
    • 取出用户的Token并解析得到该请求的userId,根据userId在从存储层获取用户的权限点。权限控制是RBAC这种方式实现的。
  • 获取到用户权限点后,获取权限判断的注解的权限信息,看用户权限点是否包含权限注解的权限信息,如果包含,则权限校验通过,否则则请求返回无权限。

本篇文章主要讲述在Matrix-Web中是如何实现的,主要讲解一些代码细节

用户登录成功,生成Token

用户登录接口是没有做权限控制的,是任何人都可以访问。请求需要携带用户名、密码,后端服务校验用户名、密码正确后,生成Token。登陆接口如下:

    @PostMapping("/login")
    public RespDTO login(@RequestParam String username, @RequestParam String password) {
        QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", username);
        SysUser user = sysUserService.getOne(queryWrapper);
        if (user == null) {
            //异步存储登陆日志
            saveSysLoginLog(username, null, false);
            throw new AriesException(USER_NOT_EXIST);
        }
        if (!user.getPassword().equals(MD5Utils.encrypt(password))) {
            saveSysLoginLog(username, null, false);
            throw new AriesException(PWD_ERROR);
        }
        //登录成功
        String jwt;
        Map<String, String> result = new HashMap<>(1);
        try {
            jwt = JWTUtils.createJWT(user.getId() + "", user.getUserId(), 599999999L);
            result.put("token", jwt);
            log.info("login success,{}", jwt);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //异步存储登陆日志
        saveSysLoginLog(username, user.getRealname(), true);
        return RespDTO.onSuc(result);
    }

在Matrix-web中生成Jwt的是采用开源的jjwt,在工程的pom文件引入以下的依赖:

<dependency>
  <groupId>io.jsonwebtoken</groupId>      <artifactId>jjwt</artifactId>
 <version>${jjwt.version}</version>
</dependency>

Matrix-Web项目中封装好了JWTUtils用于生成和解析JWT,具体生成步骤和解析步骤,请查看每一步的代码注释,在这里就不再重复。


public class JWTUtils {

//生成Token
public static String createJWT(String id, String subject, long ttlMillis) throws Exception {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        Map<String,Object> claims = new HashMap<String,Object>();//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
//        claims.put("uid", "DSSFAWDWADAS...");
//        claims.put("user_name", "admin");
//        claims.put("nick_name","DASDA121");
        SecretKey key = generalKey();//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        //下面就是在为payload添加各种标准声明和私有声明了
        JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
                .setClaims(claims)          //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(id)                  //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)           //iat: jwt的签发时间
                .setSubject(subject)        //sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key);//设置签名使用的签名算法和签名使用的秘钥
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);     //设置过期时间
        }
        return builder.compact();
 }

//解析Token
public static Claims parseJWT(String jwt) throws Exception{
    SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
    Claims claims = Jwts.parser()  //得到DefaultJwtParser
            .setSigningKey(key)         //设置签名的秘钥
            .parseClaimsJws(jwt).getBody();//设置需要解析的jwt
    return claims;
}

用户登录,服务端判断用户名、密码,如果用户名、密码正确,则生成Token返回给浏览器,浏览器会存储在js-cookie里。

对请求的Token校验

后续的所有请求从js-cookie中获取Token,并将Token中设置在Http请求头中。Matrix-Web的前端采用axios网络请求框架,可以再请求发出前进行拦截设置Token。前端代码如下:


// request拦截器
service.interceptors.request.use(
  config => {
    var token = getToken()
    if (token) {
      config.headers['requestId'] = guid()
      config.headers['Authorization'] = token // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    // config.headers['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'
    return config
  },
  error => {
    // Do something with request error
    console.log('error',error) // for debug
    Promise.reject(error)
  }
)

后端HandlerInterceptor初步验证Token

当前端的请求达到Matrix-Web后端服务器的时候,我们Spring MVC的HandlerInterceptor初步校验Token 是否存在。实现类SecurityInterceptor实现了HandlerInterceptor接口,并在preHandle发方法中获取了token,如果Token不存在,则返回无权限访问。具体代码实现如下:


@Component
public class SecurityInterceptor implements HandlerInterceptor {

    LogUtils LOG = new LogUtils(SecurityInterceptor.class);

    private static final String ERROR_MSG = "{\"code\":\"1\",\"msg\":\"you have no permission to access\"}";


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        //如果用户是非登录用户,则拒绝用户请求

        String method = request.getMethod();
        if (ApiConstants.HTTP_METHOD_OPTIONS.equals(method)) {
            return true;
        }
        String token = UserUtils.getCurrentToken();
        LOG.info("requst uri:" + request.getRequestURI() + ",request token:" + token);
        if (StringUtils.isEmpty(token)) {
            writeNoPermission(response);
        }
        return true;
    }



    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object o, Exception e) throws Exception {

    }


    private void writeNoPermission(ServletResponse servletResponse) {
        try {
            servletResponse.getWriter().write(ERROR_MSG);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

需要将上面的SecurityInterceptor注册到Spring MVC的WebMvcConfigurerAdapter中,SecurityInterceptor的作用范围需要去掉登录、注册、druid监控、swagger相关的接口,具体实现如下:


@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
    /**
     * 定义排除拦截路径
     */
    public static String[] EXCLUDE_PATH_PATTERN = {
            //文件上传和下载
            "/file/**",
            //h5端的api,建议生产中将前端h5和后端h5使用的api分拆成两个服务,
              //druid监控请求
            "/druid/**",

            //用户注册和登陆
            "/user/register", "/user/login",
            //错误资源
            "/error",
            //swagger在线api文档资源
            "/swagger-resources","/v2/api-docs","/swagger-ui.html","/webjars/**"
    };

    /**
     * 注册自定义拦截器,添加拦截路径和排除拦截路径
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
       registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/**").excludePathPatterns(EXCLUDE_PATH_PATTERN);

    }
 }

这样通过Spring Mvc的HandlerInterceptor就可以实现初步的判断请求是否携带了Token,哪些请求是白名单请求,不需要验证Token的。

权限判断

当请求通过Spring MVC的HandlerInterceptor接口,请求会进入到具体的Controller层。Matrix-web模仿了 Spring security的权限判断模式,使用注解Aop,在含有自定义注解@HasPermission的方法的类,自动生成aroud类型的切面,在执行具体代码逻辑之前会进行权限的判断。

自定义注解HasPermission

写一个自定义注解,作为aop的切点,有hasRole和hasPermission属性,代码如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HasPermission {

    String value() default "";

    String hasRole() default "";

    String hasPermission() default "";

}

aop实现

写一个切面, 切点为注解@HasPermission,Around类型通知,在方法之前判断权限。判断权限的方法为checkPermission(hasPermission)。代码如下:


@Aspect
@Component
@Slf4j
public class PermissionAspect implements Ordered {


    @Pointcut("@annotation(io.github.forezp.permission.HasPermission)")
    public void permissionPointCut() {

    }

    @Around("permissionPointCut()")
    public Object before(ProceedingJoinPoint point) throws Throwable {

        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();

        Annotation[] methodAnnotations = method.getDeclaredAnnotations();
        for (Annotation annotation : methodAnnotations) {
            if (annotation instanceof HasPermission) {
                HasPermission hasPermission = (HasPermission) annotation;
                if (!checkPermission(hasPermission)) {
                    throw new AriesException(NO_PERMISSION);
                }
            }
        }
   }

在checkPermission方法中,首先会根据当前请求所对应的Token获取用户id,然后根据用户id获取用户对应的角色集和权限集。然后和注解@HasPermission的属性hasRole或者hasPermission做对比匹配,如果角色集和权限集不包含注解上面的hasRole或者hasPermission,则当前请求的用户无权限访问,否则有权限。注解代码实现逻辑请参看源码。

怎么使用

在Matrix-Web管理后台中创建角色需要ROLE_ADMIN角色,在创建角色的接口上加上注解@HasPermission(hasRole = “ROLE_ADMIN”)。代码如下:

@RestController
@RequestMapping("/user")
@Slf4j
public class SysUserController {

 @PostMapping("/roles")
    @HasPermission(hasRole = "ROLE_ADMIN")
    public RespDTO userSetRoles(@RequestParam String userId, @RequestParam String roleIds) {
        if (StringUtils.isEmpty(userId) || StringUtils.isEmpty(roleIds)) {
            throw new AriesException(ERROR_ARGS);
        }

        sysUserService.setUserRoles(userId, roleIds);

        return RespDTO.onSuc(null);
    }
}

当请求调用这个接口时,首先会执行 aop的逻辑,会判断请求的当前用户是否具有@HasPermission注解hasRole或hasPermission权限,如果有,则执行正常的逻辑,如果没有,则返回无权限操作。

总结

本篇文章和上篇文章比较详细的介绍了matrix-web的权限设计和代码实现逻辑。

源码下载

https://github.com/forezp/matrix-web

本文为原创文章,转载请标明出处。
本文链接:https://www.fangzhipeng.com/springboot/2020/05/08/permission-done.html
本文出自方志朋的博客


(转载本站文章请注明作者和出处 方志朋-forezp

宝剑锋从磨砺出,梅花香自苦寒来,用心分享,一起成长,做有温度的攻城狮!

Post Directory