RESTful API Permission Design – Quickly set up authentication authentication projects

Many websites now have a front-end separation where the back-end provides the REST API and the front-end calls the interface for data rendering. Protecting the REST API provided by the back end in this architecture brings more attention. Authentication – whether the authentication information carried by the request has been verified. Authentication – the user who has passed the authentication has the authority of the specified API to access this API. However, not only this, what kind of authentication policy, JWT, BASIC, Digest, OAuth or more support, permissions configuration is written dead code or dynamic configuration, cloud native more and more popular framework is Quarkus rather than Spring ecology, The HTTP implementation is not Servlet but JAX-RS specification.

In the last article, RESTful API Permission Design – A Tentative Discussion, we talked about the direction points needed to protect our RESTful API authentication credentials. More said is useless, now step by step to actual practice based on SpringBoot Sureness to quickly build a fully functional authority authentication project.


Here, for the benefit of the students who are just beginning, each step is illustrated. There is a basis can be skipped directly.

Initialize a SpringBoot Web project

Operation in IDEA is as follows:

Provide some mock RESTful APIs

Create a new Controller and implement some simple RESTful APIs for external tests to call

/** * 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); }}

Add sureness dependencies to your project

Add Sureness’s Maven dependent coordinates to the project’s pom.xml

< the dependency > < groupId > com. Usthe. Sureness < / groupId > < artifactId > sureness - core < / artifactId > < version > 0.4.3 < / version > </dependency>

As follows:

Configure sureness using the default configuration

The sureness default configuration uses the file data source sureness.yml as the account permissions. The default configuration data source supports JWT, Basic Auth, Digest Auth authentication

@Configuration public class SurenessConfiguration { /** * sureness default config bean * @return default config bean */ @Bean public DefaultSurenessConfig surenessConfig() { return new DefaultSurenessConfig(); }}

Configure the default text configuration data source

Authentication of course requires our own configuration data: account data, role permissions data, etc. Configuration data may come from text, relational databases, For non-relational database, we use the default text configuration -sureness. yml, create sureness.yml file in resource directory, and configure our role permissions data and account data in sureness.yml file, as follows:

## -- sureness.yml text data source -- ## ## resources loaded into the matching dictionary, that is, resources that need to be protected, are set to be accessed by the supported roles ## Resources that are not configured are also protected by default, but are not protected # eg: / API /v1/source1===get== [role2] = / API /v2/host=== = POST / API /v1/source2=== =[] means/API /v1/source2=== =get This resource supports access to all or no roles provided that the resource is authenticated successfully: - /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 means that/API /v1/source3===get can be accessed by anyone without login. - /api/v1/account/auth===post - /api/v1/source3===get - /**/*.html===get - /**/*.js===get - /**/*.css===get - /**/*. Ico ===get # admin root Tom # eg Admin has [role1, role2] role, clear the password for the admin, salt password for 0192023 # a7bbd73250516f069df18b500 eg: root/role1, password is proclaimed in 23456 # eg: Tom has [ROLE3], password in plaintext 32113 account: -appid: If password = "password+salt", the credential is MD5(MD5). If password = "password+salt", the credential is MD5(). 0192023A7BBD73250516F069DF18B500 salt: 123 role: [role1,role2] - appId: root credential: 23456 role: [role1] - appId: tom credential: 32113 role: [role3]

Add filters to block all requests and authenticate all requests

Create a new filter that intercepts all requests and authenticates all requests with sureness. Sureness will throw the corresponding exception for the request with authentication authentication failure, and we can capture the exception of the response and process it and return 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); A Subject with user information can be bound to the current thread context holder for later use if (Subject! = null) { SurenessContextHolder.bindSubject(subject); }} the catch (ProcessorNotFoundException | UnknownAccountException | UnsupportedSubjectException e4) {/ / account creation associated abnormalities responseWrite(ResponseEntity .status(HttpStatus.BAD_REQUEST).body(e4.getMessage()), servletResponse); return; } the catch (DisabledAccountException | ExcessiveAttemptsException e2) {/ / account disabled related to abnormal responseWrite (ResponseEntity .status(HttpStatus.UNAUTHORIZED).body(e2.getMessage()), servletResponse); return; } the catch (IncorrectCredentialsException | ExpiredCredentialsException e3) {/ / authentication failure related to abnormal responseWrite (ResponseEntity .status(HttpStatus.UNAUTHORIZED).body(e3.getMessage()), servletResponse); return; } catch (NeedDigestInfoException E5) {// DigestAuthentication needs to retry the exception responseWrite(responseEntity. Status (HTTPStatus. Unauthorized) .header("WWW-Authenticate", e5.getAuthenticate()).build(), servletResponse); return; } catch (UnauthorizedException e6) {// UnauthorizedException e6) {// UnauthorizedException e6) { ResponseWrite (responseEntity. Status (HttpStatus.forbidden).body(e6.GetMessage ()), ServletResponse); return; } catch (RuntimeException E) {responseWrite(responseEntity. Status (HttpStatus.internal_server_error).build(), servletResponse); return; } try {// If no exception is thrown, the authentication is successful and the following request process continues 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) {} } }

As above,

  1. If the certification is successful,checkInIt will return information about the userSubjectSumobject
  2. If the intermediate certification fails,checkInDifferent types of authentication exceptions are thrown, based on which the user must continue the process (return the corresponding request response).

In order for Filter to work with SpringBoot, annotate the boot boot class with @ServletComponentScan

@SpringBootApplication @ServletComponentScan public class BootstrapApplication { public static void main(String[] args) { SpringApplication.run(BootstrapApplication.class, args); }}

All done. Verify the test

Through the above steps we have a complete function of the authentication project is completed, there are students want to these steps of its complete function reflected in where ah can support what.

This project is based on the RBAC authorization model and supports BAISC authentication, Digest authentication, JWT authentication. The ability to fine-grained control users’ access to the RESTful APIs provided in the background, i.e. which users have access to which APIs. So let’s test that out here.

Initiate engineering projects on IDEA.

BASIC Certification Test

Successful certification:

Password error:

Account does not exist:

Digest Certification Test

Note that if the password is configured with encrypted salt, you cannot use digest authentication

JWT certification testing

First you need to have a JWT issuing JWT. Create the following API to provide JWT issuing – / 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); }}

Request API interface login authentication to obtain JWT

Carry a request API interface using the obtained JWT value



Authentication test

With the user-role-resource configuration in the sureness.yml file above, we can associate the following typical test points

  1. /api/v1/source3===getResources can be accessed directly by anyone without authentication
  2. api/v1/source2===getThe authentication is successful if the resource is accessed with all or no roles
  3. User admin can access it/api/v1/source1===getResource, and user root, Tom, has no permissions
  4. User Tom can visit/api/v1/source1===deleteResource, and the user admin.root has no permission

The test is as follows:

/ API /v1/source3===get resource can be accessed directly by anyone, no authentication required

API /v1/source2===get resource access with all roles or no roles if authentication is successful

User admin can access it/api/v1/source1===getResource, and user root, Tom, has no permissions



User Tom can visit/api/v1/source1===deleteResource, and the user admin.root has no permission



other

This step by step description describes the process of building a simple but complete authentication project. Of course, the authorized account information is written in the configuration file, but the actual project will write the data in the database. Whether writing configuration files or databases, it just serves as a data source to provide data. Based on Sureness, we can also easily and quickly build a database-based authentication project, support dynamic refresh and other functions, this will be written next time.

If can’t wait the next article, can directly go to the certification authentication based on database DEMO warehouse address: https://github.com/tomsun28/s…


Source code repository

This article complete DEMO code warehouse address: https://github.com/tomsun28/a…