restful api 权限设计 – 疾速搭建权限我的项目
当初很多网站都进行了前后端拆散,后端提供 rest api,前端调用接口获取数据渲染。这种架构下如何爱护好后端所提供的 rest api 使得更加器重。
认证 - 申请携带的认证信息是否校验通过,鉴权 - 认证通过的用户领有指定 api 的权限能力拜访此 api。然而不仅于此,什么样的认证策略, jwt, basic,digest,oauth 还是多反对, 权限配置是写死代码还是动静配置,云原生越来越火用的框架是 quarkus 不是 spring 生态,http 实现不是 servlet 而是 jax-rs 标准咋办。
在上篇 restful api 权限设计 – 初探咱们大抵说到了要爱护咱们 restful api 的认证鉴权所需的方向点。多说无益,当初就一步一步来实战下基于 springboot sureness 来疾速搭建一个残缺性能的权限认证我的项目。
这里为了关照到刚入门的同学,图文展现了每一步操作。有根底可间接略过。
初始化一个 springboot web 工程
在 IDEA 如下操作:
提供一些模仿的 restful api
新建一个 controller, 在外面实现一些简略的 restful api 供内部测试调用
/**
* simulate api controller, for testing
* @author tomsun28
* @date 17:35 2019-05-12
*/
@RestController
public class SimulateController {
/** access success message **/
public static final String SUCCESS_ACCESS_RESOURCE = "access this resource success";
@GetMapping("/api/v1/source1")
public ResponseEntity<String> api1Mock1() {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
@PutMapping("/api/v1/source1")
public ResponseEntity<String> api1Mock3() {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
@DeleteMapping("/api/v1/source1")
public ResponseEntity<String> api1Mock4() {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
@GetMapping("/api/v1/source2")
public ResponseEntity<String> api1Mock5() {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
@GetMapping("/api/v1/source2/{var1}/{var2}")
public ResponseEntity<String> api1Mock6(@PathVariable String var1, @PathVariable Integer var2) {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
@PostMapping("/api/v2/source3/{var1}")
public ResponseEntity<String> api1Mock7(@PathVariable String var1) {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
@GetMapping("/api/v1/source3")
public ResponseEntity<String> api1Mock11(HttpServletRequest request) {return ResponseEntity.ok(SUCCESS_ACCESS_RESOURCE);
}
}
我的项目中退出 sureness 依赖
在我的项目的 pom.xml 退出 sureness 的 maven 依赖坐标
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>sureness-core</artifactId>
<version>0.4.3</version>
</dependency>
如下:
应用默认配置来配置 sureness
新建一个配置类,创立对应的 sureness 默认配置 bean
sureness 默认配置应用了文件数据源 sureness.yml
作为账户权限数据源
默认配置反对了 jwt, basic auth, digest auth
认证
@Configuration
public class SurenessConfiguration {
/**
* sureness default config bean
* @return default config bean
*/
@Bean
public DefaultSurenessConfig surenessConfig() {return new DefaultSurenessConfig();
}
}
配置默认文本配置数据源
认证鉴权当然也须要咱们本人的配置数据: 账户数据,角色权限数据等
这些配置数据可能来自文本,关系数据库,非关系数据库
咱们这里应用默认的文本模式配置 – sureness.yml, 在 resource 资源目录下创立 sureness.yml 文件
在 sureness.yml 文件里配置咱们的角色权限数据和账户数据,如下:
## -- sureness.yml 文本数据源 -- ##
# 加载到匹配字典的资源, 也就是须要被爱护的, 设置了所反对角色拜访的资源
# 没有配置的资源也默认被认证爱护, 但不鉴权
# eg: /api/v1/source1===get===[role2] 示意 /api/v2/host===post 这条资源反对 role2 这一种角色拜访
# eg: /api/v1/source2===get===[] 示意 /api/v1/source2===get 这条资源反对所有角色或无角色拜访 前提是认证胜利
resourceRole:
- /api/v1/source1===get===[role2]
- /api/v1/source1===delete===[role3]
- /api/v1/source1===put===[role1,role2]
- /api/v1/source2===get===[]
- /api/v1/source2/*/*===get===[role2]
- /api/v2/source3/*===get===[role2]
# 须要被过滤爱护的资源, 不认证鉴权间接拜访
# /api/v1/source3===get 示意 /api/v1/source3===get 能够被任何人拜访 无需登录认证鉴权
excludedResource:
- /api/v1/account/auth===post
- /api/v1/source3===get
- /**/*.html===get
- /**/*.js===get
- /**/*.css===get
- /**/*.ico===get
# 用户账户信息
# 上面有 admin root tom 三个账户
# eg: admin 领有 [role1,role2] 角色, 明文明码为 admin, 加盐明码为 0192023A7BBD73250516F069DF18B500
# eg: root 领有[role1], 明码为明文 23456
# eg: tom 领有[role3], 明码为明文 32113
account:
- appId: admin
# 如果填写了加密盐 --salt, 则 credential 为 MD5(password+salt)的 32 位后果
# 没有盐认为不加密,credential 为明文
# 若明码加盐 则 digest 认证不反对
credential: 0192023A7BBD73250516F069DF18B500
salt: 123
role: [role1,role2]
- appId: root
credential: 23456
role: [role1]
- appId: tom
credential: 32113
role: [role3]
增加过滤器拦挡所有申请, 对所有申请进行认证鉴权
新建一个 filter, 拦挡所有申请,用 sureness 对所有申请进行认证鉴权。认证鉴权失败的申请 sureness 会抛出对应的异样,咱们捕捉响应的异样进行解决返回 response 即可。
@Order(1)
@WebFilter(filterName = "SurenessFilterExample", urlPatterns = "/*", asyncSupported = true)
public class SurenessFilterExample implements Filter {
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
try {SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(servletRequest);
// 认证鉴权胜利则会返回带用户信息的 subject 能够将 subject 信息绑定到以后线程上下文 holder 供前面应用
if (subject != null) {SurenessContextHolder.bindSubject(subject);
}
} catch (ProcessorNotFoundException | UnknownAccountException | UnsupportedSubjectException e4) {
// 账户创立相干异样
responseWrite(ResponseEntity
.status(HttpStatus.BAD_REQUEST).body(e4.getMessage()), servletResponse);
return;
} catch (DisabledAccountException | ExcessiveAttemptsException e2) {
// 账户禁用相干异样
responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED).body(e2.getMessage()), servletResponse);
return;
} catch (IncorrectCredentialsException | ExpiredCredentialsException e3) {
// 认证失败相干异样
responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED).body(e3.getMessage()), servletResponse);
return;
} catch (NeedDigestInfoException e5) {
// digest 认证须要重试异样
responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.header("WWW-Authenticate", e5.getAuthenticate()).build(), servletResponse);
return;
} catch (UnauthorizedException e6) {
// 鉴权失败相干异样,即无权拜访此 api
responseWrite(ResponseEntity
.status(HttpStatus.FORBIDDEN).body(e6.getMessage()), servletResponse);
return;
} catch (RuntimeException e) {
// 其余异样
responseWrite(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(),
servletResponse);
return;
}
try {
// 若未抛出异样 则认证鉴权胜利 持续上面申请流程
filterChain.doFilter(servletRequest, servletResponse);
} finally {SurenessContextHolder.clear();
}
}
/**
* write response json data
* @param content content
* @param response response
*/
private static void responseWrite(ResponseEntity content, ServletResponse response) {response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
((HttpServletResponse)response).setStatus(content.getStatusCodeValue());
content.getHeaders().forEach((key, value) ->
((HttpServletResponse) response).addHeader(key, value.get(0)));
try (PrintWriter printWriter = response.getWriter()) {if (content.getBody() != null) {if (content.getBody() instanceof String) {printWriter.write(content.getBody().toString());
} else {ObjectMapper objectMapper = new ObjectMapper();
printWriter.write(objectMapper.writeValueAsString(content.getBody()));
}
} else {printWriter.flush();
}
} catch (IOException e) {}}
}
像下面一样,
- 若认证鉴权胜利,
checkIn
会返回蕴含用户信息的SubjectSum
对象 - 若两头认证鉴权失败,
checkIn
会抛出不同类型的认证鉴权异样, 用户需依据这些异样来持续前面的流程(返回相应的申请响应)
为了使 filter 在 springboot 失效 须要在 boot 启动类加注解 @ServletComponentScan
@SpringBootApplication
@ServletComponentScan
public class BootstrapApplication {public static void main(String[] args) {SpringApplication.run(BootstrapApplication.class, args);
}
}
验证测试
通过下面的步骤 咱们的一个残缺性能认证鉴权我的项目就搭建实现了,有同学想 就这几步骤 它的残缺性能体现在哪里啊 能反对啥。
这个搭好的认证鉴权我的项目基于 rbac 权限模型,反对 baisc 认证,digest 认证, jwt 认证。能细粒度的管制用户对后盾提供的 restful api 的拜访权限,即哪些用户能拜访哪些 api。咱们这里来测试一下。
IDEA 上启动工程项目。
basic 认证测试
认证胜利:
明码谬误:
账户不存在:
digest 认证测试
留神如果明码配置了加密盐,则无奈应用 digest 认证
jwt 认证测试
jwt 认证首先你得领有一个签发的 jwt,创立如下 api 接口提供 jwt 签发 - /api/v1/account/auth
@RestController()
public class AccountController {
private static final String APP_ID = "appId";
/**
* account data provider
*/
private SurenessAccountProvider accountProvider = new DocumentAccountProvider();
/**
* login, this provider a get jwt api, convenient to test other api with jwt
* @param requestBody request
* @return response
*
*/
@PostMapping("/api/v1/account/auth")
public ResponseEntity<Object> login(@RequestBody Map<String,String> requestBody) {if (requestBody == null || !requestBody.containsKey(APP_ID)
|| !requestBody.containsKey("password")) {return ResponseEntity.badRequest().build();}
String appId = requestBody.get("appId");
String password = requestBody.get("password");
SurenessAccount account = accountProvider.loadAccount(appId);
if (account == null || account.isDisabledAccount() || account.isExcessiveAttempts()) {return ResponseEntity.status(HttpStatus.FORBIDDEN).build();}
if (account.getPassword() != null) {if (account.getSalt() != null) {password = Md5Util.md5(password + account.getSalt());
}
if (!account.getPassword().equals(password)) {return ResponseEntity.status(HttpStatus.FORBIDDEN).build();}
}
// Get the roles the user has - rbac
List<String> roles = account.getOwnRoles();
long refreshPeriodTime = 36000L;
// issue jwt
String jwt = JsonWebTokenUtil.issueJwt(UUID.randomUUID().toString(), appId,
"token-server", refreshPeriodTime >> 1, roles,
null, Boolean.FALSE);
Map<String, String> body = Collections.singletonMap("token", jwt);
return ResponseEntity.ok().body(body);
}
}
申请 api 接口登录认证获取 jwt
携带应用获取的 jwt 值申请 api 接口
鉴权测试
通过下面的 sureness.yml 文件配置的用户 - 角色 - 资源,咱们能够关联上面几个典型测试点
/api/v1/source3===get
资源能够被任何间接拜访,不须要认证鉴权api/v1/source2===get
资源持所有角色或无角色拜访 前提是认证胜利- 用户 admin 能拜访
/api/v1/source1===get
资源, 而用户 root,tom 无权限 - 用户 tom 能访
/api/v1/source1===delete
资源,而用户 admin.root 无权限
测试如下:
其余
这次图文一步一步的详细描述了构建一个简略但残缺的认证鉴权我的项目的流程,当然外面的受权账户等信息是写在配置文件外面的,理论的我的项目是会把这些数据写在数据库中。万变不离其宗,无论是写配置文件还是数据库,它只是作为数据源提供数据,基于 sureness 咱们也能轻松疾速构建基于数据库的认证鉴权我的项目,反对动静刷新等各种性能,这个就下次再写咯。
若等不及下次文章,能够间接去看基于数据库的认证鉴权 DEMO 仓库地址:https://github.com/tomsun28/s…
源代码仓库
这篇文章的残缺 DEMO 代码仓库地址:https://github.com/tomsun28/s…