最早期,我们通过从前台传递来 username(用户名) password(密码) role_status(角色状态)
传递这些信息到 Controller 层,之后 controller 会通过 if 判断来校验角色信息
不同的角色 controller 的响应对应不同的 service 和 view
不同的角色:
- 获取的数据不同
- 展示的页面不同
- 具备的操作权限不同
这种实现形式具有很多的缺点:
- 代码耦合度非常高
- 扩展性很差(新增角色时操作繁琐)
- 维护难度很高
以角色为学生、教师为例子,大致流程图如下:
经过一段时期的发展,对于权限体系,我们可以采取加上 Filter 过滤器,来过滤所有的请求
这种方式在数据库中维护的资源是所有的请求 URI(统一资源标识符) ,即 RequestMapping 中的字符串
(在此阶段中也产生了一种形式:URL 结尾为 .action 和 .do 结尾,只拦截 .do 结尾的请求进行数据库的匹配)
此时,我们会遇到如以下的一些问题:
再发展时,就对细粒度的 URI 进行了一定的归纳,形成了统一的字符串来标识一类资源权限
可以通过注解及 AOP 切面的形式,对不同的 controller 方法,根据权限需求,绑定统一的字符串
从而达到 用户对应角色,角色对应资源 的模式
此时就和如今的 RBAC 体系类似了。
在不使用类似 JWT 等框架的前提下,可以使用 UUID 来模拟生成随机的 token
模拟流程图如下:
用户进行登陆,需要输入用户名以及密码
在用户名、密码校验通过之后,在后台通过 UUID 生成随机的 token
将生成的随机 token 与用户名存入 Redis 缓存中 (使用 Hash 的方式,Key 值存放用户名,Value 值存放生成的 token)
在用户登陆成功后,会在其用户信息中加入 token ,每次发起请求时,都会在请求头中带上此 token
当用户发起请求时,token 随请求传入后台
后台首先经过 token Filter 过滤器,判断其 token 是否合法及是否包含 token
如果 token 不合法或为空,则返回给前台 401 权限认证失败
如果 token 合法,则从 Redis 缓存中取出对应此 token 的用户名,根据用户名判断其角色及角色所对应的权限 (Redis 中不存在该 token 则返回权限认证失败)
通过定义自定义注解,并在注解中加入 value 值标识对应的权限字段,来控制在不同的 controller 中需要的权限资源信息
首先,查找到注解修饰的方法
其次,判断 token 对应的用户名对应的角色的权限中,是否包含有注解中所声明的权限信息
若没有对应的权限,则返回权限认证失败
若存在对应的权限信息,则进行访问该资源,并将数据返回至前台
SpringBoot 配置文件 application.yml
token:
# header 值
header: Authorization
# token 前缀
prefix: "MELODY BEARER "
# 白名单,不过滤token
exclude: /auth/temp,/auth/login
TokenFilter.java
@Component
public class TokenFilter implements Filter {
@Value("${token.header}")
private String header;
@Value("${token.prefix}")
private String prefix;
@Value("${token.exclude}")
private String exclude;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
List<String> excludeList = Arrays.asList(exclude.split(","));
HttpServletRequest request = (HttpServletRequest) servletRequest;
String token = request.getHeader(header);
if(excludeList.contains(request.getRequestURI())) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
if(token == null || token.equals("") || !token.startsWith(prefix)) {
System.err.println("TOKEN ERROR");
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
} else {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}
Controller
@RestController
@RequestMapping("/auth")
public class AuthController {
@Value("${token.prefix}")
private String prefix;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private MultiThreadQueryService multiThreadQueryService;
@AuthCheck(value = "/auth/test")
@GetMapping("/test")
public ResJson test() {
System.err.println("test");
return ResJson.yes();
}
@AuthCheck(value = "/auth/temp")
@GetMapping("/temp")
public ResJson temp() {
System.err.println("temp");
return ResJson.yes();
}
@PostMapping("/login")
public ResJson login(@RequestBody User usr) {
// 模拟用户登陆正确的用户名与密码
if(usr.getNickname().equals("admin") && usr.getPassword().equals("123456")) {
String token = prefix + UUID.randomUUID();
redisTemplate.opsForHash().put("token", usr.getNickname(), token);
Stack stack = new Stack<>();
stack.push(token);
stack.push(usr);
return ResJson.yes(stack);
}
return ResJson.no(ResCode.FAIL);
}
}
自定义注解 AuthCheck
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AuthCheck {
public String value() default "";
}
AOP 切面
在切面中存在有两种方法
1 .细粒度的根据请求的 URI 来进行判断权限的方式 (way1部分)
2. 根据统一权限字符串判断权限的方式 (way2部分)
@Aspect
@Component
public class AuthAspect {
@Value("${token.header}")
private String header;
@Autowired
private HttpServletRequest request;
@Autowired
private RedisTemplate redisTemplate;
@Pointcut("@annotation(com.melody.auth.annotation.AuthCheck)")
public void authPointCut(){}
@Around("authPointCut()")
public Object authCheck(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
String username = "";
try{
// 判断 TOKEN
if (redisTemplate.opsForHash().values("token").stream().anyMatch(str -> str.equals(request.getHeader(header)))) {
Map<String, String> map = redisTemplate.opsForHash().entries("token");
for(Map.Entry<String, String> entry : map.entrySet()) {
if(entry.getValue().equals(request.getHeader(header))) {
username = entry.getKey();
}
}
}
if(username == null || username.equals("")){
throw new AuthException(ResCode.TOKEN_NOT_EXIST);
} else {
// 模拟根据用户名获取用户对应角色的有权限访问的(way1:资源列表)/(way2:权限字段资源列表)
List<String> resourceList = new ArrayList<>();
resourceList.add("/auth/test");
resourceList.add("/auth/login");
/****************/
// way1:根据URI来判断权限,此时数据库中存所有的URI,扩展性很差
// boolean flag = resourceList.stream().anyMatch(str -> str.equals(request.getRequestURI()));
/****************/
/****************/ //从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
//获取切入点所在的方法
Method method = signature.getMethod();
AuthCheck ac = method.getAnnotation(AuthCheck.class);
boolean flag = false;
if(ac != null) {
String auth = ac.value();
// way2:数据库中存放权限字段,根据注解的value确定请求所需权限判断是否有权限进行访问
flag = resourceList.stream().anyMatch(str -> str.equals(auth));
}
/****************/
if(!flag) {
throw new AuthException(ResCode.BANED_REQUEST); // 自定义异常
}
}
} catch(AuthException e) {
System.err.println(e.getResCode().getCode() + ":" + e.getResCode().getMsg());
return ResJson.no(e.getResCode());
}
Object res = proceedingJoinPoint.proceed();
return res;
}
}
自定义异常 AuthException
@Getter
public class AuthException extends RuntimeException{
private ResCode resCode;
public AuthException(ResCode resCode) {
this.resCode = resCode;
}
}
自定义 Json 字符串 ResJson
@Data
public class ResJson<T> implements Serializable {
private int code;
private String msg;
private T data;
public static ResJson yes() {
return yes("");
}
public static ResJson yes(Object o) {
return new ResJson(ResCode.SUCCESS, o);
}
public static ResJson no(ResCode resCode) {
return new ResJson(resCode);
}
public static ResJson no(ResCode resCode, Object o) {
return new ResJson(resCode, o);
}
public ResJson(){}
public ResJson(ResCode resCode) {
setResCode(resCode);
}
public ResJson(ResCode resCode, T data) {
setResCode(resCode);
this.data = data;
}
public void setResCode(ResCode resCode) {
this.code = resCode.getCode();
this.msg = resCode.getMsg();
}
@Override
public String toString() {
return "ResJson{" +
"\"code\":" + code +
",\"msg\":" + msg + '\'' +
",\"data\":" + data +
'}';
}
}
自定义状态码 ResCode
package com.melody.auth.model;
public enum ResCode {
SUCCESS(200, "获取数据成功"),
FAIL(400, "参数或语法错误"),
TOKEN_NOT_EXIST(401, "TOKEN不存在,认证失败"),
BANED_REQUEST(403, "无权访问该资源"),
FATAL("未知致命错误"),
;
private int code;
private String msg;
ResCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
ResCode(String msg) {
this.code = -999;
this.msg = msg;
}
public int getCode() {
return this.code;
}
public String getMsg() {
return this.msg;
}
}