

跟我学Springboot开发后端管理系统8:Matrxi-Web权限设计实现
source link: https://www.fangzhipeng.com/springboot/2020/05/08/permission-done.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

跟我学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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK