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();
}