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 */@RestControllerpublic 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
认证
@Configurationpublic 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],明码为明文32113account: - 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@ServletComponentScanpublic 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...