SaaS-HRM(7)服务状态和基于JWT的API鉴权

 

1、服务状态

1.1 什么是服务状态

       有状态和无状态服务是两种不同的服务架构,两者的不同之处在于对于服务状态的处理。服务状态是服务请求所需的数据,它可以是一个变量或者一个数据结构。无状态服务不会记录服务状态,不同请求之间也是没有任何关系;而有状态服务则反之。对服务器程序来说,究竟是有状态服务,还是无状态服务,其判断依据——两个来自相同发起者的请求在服务器端是否具备上下文关系。

1.2 无状态服务

       无状态服务,服务器端所能够处理的数据全部来自于请求所携带的信息,无状态服务对于客户端的单次请求的处理,不依赖于其他请求,处理一次请求的信息都包含在该请求里。最典型的就是通过cookie保存token的方式传输请求数据。也可以理解为Cookie是通过客户端保持状态的解决方案。

我们前面讲的JWT生成token,然后传给客户端的方式就是无状态的。

1.3 有状态服务

       有状态服务则相反,服务会存储请求上下文相关的数据信息,先后的请求是可以有关联的。例如,在Web 应用中,经常会使用Session 来维系登录用户的上下文信息。虽然http 协议是无状态的,但是借助Session,可以使http 服务转换为有状态服务。

2、基于JWT的API鉴权

2.1 基于拦截器的token

       如果我们每个方法都去写一段代码,冗余度太高,不利于维护,那如何做使我们的代码看起来更清爽呢?我们可以将这段代码放入拦截器去实现。

2.1.1 Spring中的拦截器

       Spring为我们提供了org.springframework.web.servlet.handler.HandlerInterceptorAdapter这个适配器,继承此类,可以非常方便的实现自己的拦截器。他有三个方法:分别实现预处理、后处理(调用了Service并返回ModelAndView,但未进行页面渲染)、返回处理(已经渲染了页面)。
1.在preHandle中,可以进行编码、安全控制等处理;

2.在postHandle中,有机会修改ModelAndView;

3.在afterCompletion中,可以根据ex是否为null判断是否发生了异常,进行日志记录。

2.1.2 自定义拦截器JwtInterceptor

在ihrmcommon工程新增interceptor包,然后创建JetInterceptor:

package com.zdw.ihrm.common.interceptor;

import com.zdw.ihrm.common.entity.ResultCode;
import com.zdw.ihrm.common.exception.CommonException;
import com.zdw.ihrm.common.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义拦截器,进行token鉴权
 *   preHandle:进入到控制器方法之前执行z的方法,此处只要用到这个方法就行了
 *      返回值:true:可以执行执行控制器的方法,false:拦截,不会执行控制器的方法
 *   postHandle:执行控制器方法之后,执行该方法
 *   afterCompletion:响应结束之前执行该方法
 *
 *   该拦截器的作用:
 *   1、得到请求头中的token,判断用户是否登录
 *   2、判断用户是否具有当前接口(api)的访问权限
 */
@Component
public class JwtInterceptor extends HandlerInterceptorAdapter {
    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取token信息
        //1.1 得到请求头
        String authorization = request.getHeader("Authorization");//得到前后端约定的请求头Authorization
        if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")){//这也是前后端约定的
            //1.2获取到token
            String token = authorization.replace("Bearer ","");
            //1.3 解析token,得到Claims对象
            Claims claims = jwtUtil.parseJwtToken(token);
            if(claims!=null){
                //1.4 绑定claims到request请求域中,我们的控制器对象就可以从request获取到claims中
                request.setAttribute("user_claims",claims);
                return true;
            }
        }
        throw new CommonException(ResultCode.UNAUTHENTICATED);//抛出还没登录的异常
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }
}

2.1.3 添加拦截器配置

 在ihrm_system模块下,和启动类同级的目录下创建拦截器配置类;InterceptorConfig:

package com.zdw.ihrm.system;

import com.zdw.ihrm.common.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Autowired
    private JwtInterceptor jwtInterceptor;
    //添加拦截器配置
    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        //1.添加自定义拦截器
        registry.addInterceptor(jwtInterceptor).
                addPathPatterns("/**").//2.指定拦截器的url地址
                excludePathPatterns("/sys/login","/frame/register/**");//3.指定不拦截的url地址
    }
}

2.1.4 修改SystemApplication

 添加No Session的配置:

    //解决no session
    @Bean
    public OpenEntityManagerInViewFilter openEntityManagerInViewFilter() {
        return new OpenEntityManagerInViewFilter();
    }

2.1.5 修改BaseController

在BaseController中,添加Claims属性,然后从request中取出token相关信息,同时也要处理公司id和公司名称的值;

package com.zdw.ihrm.common.controller;

import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.ModelAttribute;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class BaseController {
    protected HttpServletRequest request;
    protected HttpServletResponse response;
    protected Claims claims;
    @ModelAttribute//这个注解的作用:在实例化子类对象的时候,会先执行该方法
    public void setReqAndResp(HttpServletRequest request, HttpServletResponse response) {
        this.request = request;
        this.response = response;
        Object obj = request.getAttribute("user_claims");
        if(obj!=null){
            this.claims = (Claims) obj;
        }
    }
    //企业id,(暂时使用1,以后会动态获取)
    public String parseCompanyId() {
        if(claims!=null){
            return (String)claims.get("companyId");
        }
        return "1";
    }
    public String parseCompanyName() {
        if(claims!=null){
            return (String)claims.get("companyName");
        }
        return "湖南教育股份有限公司";
    }
}

2.1.6 修改获取信息的方法

修改UserController中的profile方法:

    //获取用户信息的方法
    @RequestMapping(value = "/profile",method = RequestMethod.POST)
    public Result profile(HttpServletRequest request) throws Exception {
        //请求中获取key为Authorization的头信息
        /*String authorization = request.getHeader("Authorization");
        if(StringUtils.isEmpty(authorization)){
            throw new CommonException(ResultCode.UNAUTHENTICATED);//说明没有登录
        }
        //前后端约定头信息内容以 Bearer+空格+token 形式组成
        String token = authorization.replace("Bearer ", "");//把Bearer 替换掉,得到的就是token
        //解析token
        Claims claims = jwtUtil.parseJwtToken(token);
        if(claims==null){
            throw new CommonException(ResultCode.UNAUTHENTICATED);//说明没有登录
        }*/
        //上面注释的这部分都通过拦截器JwtInterceptor和父类BaseController处理了
        String userId = claims.getId();//得到用户id
        User user = userService.findtById(userId);//会一起查询到用户的roles权限集合
        /**
         * 根据user的level来判断查询的权限
         * saasAdmin:saas管理员:拥有所有的权限
         * coAdmin:企业管理员:只有企业相关的所有权限,不能查看saas平台相关的操作,比如企业管理(企业信息的增上改查)
         * user:普通用户:只有分配的对应的角色的权限
         */
        String level = user.getLevel();
        ProfileResult profileResult = null;
        if("user".equals(level)){//普通用户
            profileResult = new ProfileResult(user);
        }else{
            Map<String,Object> map = new HashMap<>();
            if("coAdmin".equals(level)){
                map.put("enVisible","1");//说明可以查询到企业可以看到的所有权限
            }
            List<Permission> list = permissionService.findAll(map);
            profileResult = new ProfileResult(user,list);
        }
        return new Result(ResultCode.SUCCESS,profileResult);
    }

2.2 基于拦截器的api鉴权

       前端我们已经通过权限控制了按钮的显示与隐藏,但是如果别人通过页面元素看到了请求后台的的url地址,那照样可以把请求发送到服务端。所以,我们需要在后端也对客户端请求的api鉴权,只有当拥有相关api的权限时,才允许请求,否则会被拦截。但是我们怎么获取到用户的api呢?所以应该是在用户登录的时候,就把用户的相关权限都返回给客户端,当客户端请求api的时候,我们就能获取到其权限,然后跟它请求的api对应的方法所要求的权限是否相等。

2.2.1 修改用户登录方法

修改UserController中的login方法,登录成功,把用户权限以字符串的形式返回给客户端;

    @RequestMapping(value = "/login",method = RequestMethod.POST)
    public Result login(@RequestBody Map<String,String> loginMap) throws Exception {
        String mobile = loginMap.get("mobile");
        String password = loginMap.get("password");
        //根据mobile查询用户信息
        User user = userService.findByMobile(mobile);
        if(user ==null || !password.equals(user.getPassword())){
            throw  new CommonException(ResultCode.MOBILEORPASSWORDERROR);//提示用户名或密码错误
        }else{
            //登录成功,签发token并把token响应给客户端
            Map<String,Object> map = new HashMap<>();
            map.put("companyId",user.getCompanyId());
            map.put("companyName",user.getCompanyName());
            //-------------------------------------------
            //把用户对应的权限code,以逗号拼接成字符串返回给客户端
            StringBuilder stringBuilder = new StringBuilder();
            for (Role role : user.getRoles()) {
                Set<Permission> permissions = role.getPermissions();
                for (Permission permission : permissions) {
                    stringBuilder.append(permission.getCode()).append(",");
                }
            }
            map.put("apis",stringBuilder.toString());//可访问的api权限,这样拦截器中就可以从Claims中拿到权限
            //-------------------------------------------
            String token = jwtUtil.createJwtToken(user.getId(), user.getUsername(), map);
            return new Result(ResultCode.SUCCESS,token);
        }
    }

2.2.2 修改删除用户的方法

修改UserController中deleteById方法,在@RequestMapping注解加上name属性:name="USER_DELETE"  注意,这个值要跟我们给删除按钮分配的权限一样。(如果其他的按钮要做api权限控制,也要这样去做)

    //根据id删除用户
    @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE,name = "USER_DELETE")
    public Result deleteById(@PathVariable("id") String id){
        userService.deleteById(id);
        return new Result(ResultCode.SUCCESS);
    }

2.2.3 修改拦截器

在拦截器JwtInterceptor中的preHandler方法中,添加判断api权限的代码:

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取token信息
        //1.1 得到请求头
        String authorization = request.getHeader("Authorization");//得到前后端约定的请求头Authorization
        if(!StringUtils.isEmpty(authorization) && authorization.startsWith("Bearer")){//这也是前后端约定的
            //1.2获取到token
            String token = authorization.replace("Bearer ","");
            //1.3 解析token,得到Claims对象
            Claims claims = jwtUtil.parseJwtToken(token);
            if(claims!=null){
                //------------------------------------------
                //通过claims获取到当前用户的可访问API权限字符串
                String apis = (String)claims.get("apis");
                //强制转换handler,这个handler就是我们接下来要执行的控制器方法
                HandlerMethod handlerMethod =(HandlerMethod)handler;
                //获取接口上的reqeustmapping注解
                RequestMapping annotation = handlerMethod.getMethodAnnotation(RequestMapping.class);
                //获取注解的name属性
                String name = annotation.name();
                if(apis.contains(name)){//说明用户的权限包含该api权限
                    //1.4 绑定claims到request请求域中,我们的控制器对象就可以从request获取到claims中
                    request.setAttribute("user_claims",claims);
                    return true;
                }else{
                    throw new CommonException(ResultCode.UNAUTHORISE);//抛出权限不足的异常
                }
                //------------------------------------------
            }
        }
        throw new CommonException(ResultCode.UNAUTHENTICATED);//抛出还没登录的异常
    }

2.2.4 测试

我们首先要把module-employees模块的index.vue视图中,控制删除按钮显示与隐藏的判断去掉,改成如下:

<el-button  @click="handleDelete(scope.row)" type="text" size="small">删除</el-button>

这样的话,我们前端就算没有删除员工的权限,也可以看到删除按钮,这样方便测试。

然后给人事经理角色分配的权限中不要包含删除员工的权限,如下:

此时,以人事经理角色的用户登录,可以看到删除按钮,但是点击删除操作时,会提示权限不足,这就说明我们的拦截器已经进行了api鉴权了。

注意:当启动ihrm_company工程的时候,会报错如下:

解决办法:在启动类CompanyApplication中配置JwtUtil:

    @Bean//把我们的JwtUtil交给spring管理
    public JwtUtil jwtUtil(){
        return new JwtUtil();
    }

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值