前后端分离的项目中,服务端对于每个请求都是一模一样的,对于每个用户的请求识别就需要用到一个统一的方式(jwt),然后在访问每个接口的时候先对这个jwt进行识别,查出这个用户的权限级别,检查是否拥有访问这个接口的权限,如果没有,那么将直接返回一个错误信息,表示权限不足,反之放过,继续往下执行。
jwt就是一个包含用户信息的加密字符串:参考:JWT的生成及验证过程
因为对于每个接口都需要进行权限校验,那么采用spring的aop最好不过了。设置每个controller都需要经过的aop切面,在切面中获取出controller中的HttpServletRequest,这个request知道了就可以获取出访问的所有headers之类的参数了。接下来就进行权限验证。
面向对象的特点之一就是继承,所以在项目中controller都需要继承一个BaseController,这样的好处之一是在controller层中可以复用一些BaseController的方法,比如获取远端ip地址、获取用户权限等常用函数。好处之二就是在权限校验切面中调用BaseController controller = (BaseController) joinPoint.getTarget();这个方法的时候可以统一转成一个对象。
BaseController一般是这样的:
public class BaseController {
private static final Logger log = Logger.getLogger(BaseController.class);
protected static final ThreadLocal<HttpServletRequest> requests = new ThreadLocal();
protected static final ThreadLocal<HttpServletResponse> responses = new ThreadLocal();
public BaseController() {
}
@ModelAttribute
public void init(HttpServletRequest request, HttpServletResponse response) {
requests.set(request);
responses.set(response);
}
public HttpServletRequest getRequest() {
return (HttpServletRequest)requests.get();
}
public HttpServletResponse getResponse() {
return (HttpServletResponse)responses.get();
}
public void setSessionValue(String key, Object value) {
this.getRequest().getSession().setAttribute(key, value);
}
public Object getSessionValue(String key) {
return this.getRequest().getSession().getAttribute(key);
}
public String getRemoteAddr() {
return this.getRequest().getRemoteAddr();
}
public void set(String key, Object value) {
this.getRequest().setAttribute(key, value);
}
public Object get(String key) {
return this.getRequest().getAttribute(key);
}
public Object getClaimsValue(String key) {
Claims claims = (Claims)this.getRequest().getAttribute("claims");
return claims != null && claims.get(key) != null ? claims.get(key) : null;
}
}
那么在权限切面中首先需要获取出请求的用户信息jwt:用户信息的jwt字符串通常是通过请求头的Authorization来传递的,因为这个用户信息我们会经常用到,那么解析出来之后放到request的session中,注意是request的session,不是request.getSession()这个session,对于每个用户信息的生命周期只是这个request。getsession的这个session将会是全局的session,当然,前后端分离的项目中sessionid会变,所以这个不起作用的。
BaseController controller = (BaseController)joinPoint.getTarget();
String token = controller.getRequest().getHeader("Authorization");
if (token == null) {
return joinPoint.proceed();
} else {
Claims claims = null;
try {
claims = TokenUtil.parse(token);
} catch (ExpiredJwtException var6) {
return ResultModel.builder().code("JWT EXPIRE").build();
} catch (Exception var7) {
return ResultModel.builder().code("JWT WRONG").build();
}
controller.set("claims", claims);
return joinPoint.proceed();
}
当权限不足的时候,返回内容的结构应该和controller层返回结构是一样的,统一采用一种对象:采用的是对象的构造器模式。
public class ResultModel {
public static final String SUCCESS = "success";
public static final String FAILED = "failed";
public static final String ERROR = "error";
private Object data;
private String code;
private String token;
private int total;
public ResultModel() {
}
}
获取了用户的信息,那么我们就可以检验这个用户是否有权限访问这个接口啦,项目会有一张表来保存每个接口对每个权限的开放状态。如:
我用到了3张表,是因为满足数据库开发的3大范式,其实完全可以设置成一个表,表中存url和可以访问的权限(用‘,’分开)比如:
为了规范,还是建议用多张表!
切面就检查某个权限是否能访问某个接口:
public class AccessAspect {
@Autowired
private AuthorDao authorDao;
public Object checkAccess(ProceedingJoinPoint joinPoint) throws Throwable {
BaseController controller = (BaseController) joinPoint.getTarget();
HttpServletRequest request = controller.getRequest();
Integer roleId = (Integer) controller.getClaimsValue("roleId");
roleId = roleId == null ? VISITOR : roleId;
String api = request.getRequestURI().replaceAll(request.getContextPath(), "");
while (api.contains("//")) {
api = api.replace("//","/");
}
String methodName = request.getMethod();
if ("OPTIONS".equals(methodName)) {
return joinPoint.proceed();
}
String url = methodName + ":" + api;
Author record = new Author(roleId, url);
if (authorDao.verify(record)) {
return joinPoint.proceed();
} else {
return ResultModel.builder().code(ILLEGAL_ACCESS_ERROR).build();
}
}
}
权限到就是一个查询对应表是否有记录:
@Repository
public class AuthorDaoImpl extends SqlSessionDaoSupport implements AuthorDao {
private String getNamespace(){
return "org.kelab.enterprise.dao.AuthorDao";
}
@Override
public boolean verify(Author record) {
return (Integer)getSqlSession().selectOne(getNamespace() + ".verify",record) > 0;
}
}
sql代码:
<mapper namespace="org.kelab.enterprise.dao.AuthorDao">
<select id="verify" resultType="java.lang.Integer" parameterType="org.kelab.enterprise.entity.Author">
select count(*)
from request_url re, role_request ro
where re.id = ro.url_id
and re.url = #{url,jdbcType=VARCHAR}
and ro.role_id = #{roleId,jdbcType=INTEGER}
</select>
</mapper>