上篇文章讲述了Matrix-web整体实现的权限控制的思路。现在来回顾一下:
首先,用户需要登录,填用户名、密码,后端接收到登录请求,进行用户、密码的校验,校验成功后则根据用户名生成Token,并返回给浏览器。
浏览器收到Token后,会存储在本地的LocalStorge里。
后续浏览器发起请求时都携带该Token,请求达到后端后,会在Filter进行判断,首选判断是否为白名单url(比如登录接口url),如果是则放行;否则进入Token验证。如果有Token且解析成功,则放行,否则,返回无权限访问。
Filter判断后,请求达到具体的Controller层,如果在Controller层上加上了权限判断的注解,则生成代理类。代理类在执行具体方法前会根据Token判断权限。
获取到用户权限点后,获取权限判断的注解的权限信息,看用户权限点是否包含权限注解的权限信息,如果包含,则权限校验通过,否则则请求返回无权限。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7hlz5kP4-1590667445450)(https://ws3.sinaimg.cn/large/813fb3c3gy1g73tw6y311j20fu0cemxg.jpg)]
本篇文章主要讲述在Matrix-Web中是如何实现的,主要讲解一些代码细节。
用户登录接口是没有做权限控制的,是任何人都可以访问。请求需要携带用户名、密码,后端服务校验用户名、密码正确后,生成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.jsonwebtokengroupId> <artifactId>jjwtartifactId>
<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里。
后续的所有请求从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)
}
)
当前端的请求达到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类型的切面,在执行具体代码逻辑之前会进行权限的判断。
写一个自定义注解,作为aop的切点,有hasRole和hasPermission属性,代码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface HasPermission {
String value() default "";
String hasRole() default "";
String hasPermission() default "";
}
写一个切面, 切点为注解@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