📜 ⬆️ ⬇️

6 ways: how to add security for Rest service in Java

In this article I will try to describe several ways, and more precisely 6, how to add security for the rest of the Java service.

Our team was tasked with finding all possible ways to add security to the rest service. Analyze the pros and cons and choose the most suitable for our project. When I began to look for such an article in Google, I did not find anything suitable, but there were only fragments and I had to collect this information bit by bit. So I think this article will be useful to other Java developers writing back-end. I will not argue that one of these methods is better or worse, it all depends on the task and a specific project. Therefore, which of the six methods is most suitable for your project is up to you. I will try to describe the principle of each approach and give a small example using Java and Spring Security.

Method One : Basic Authentication


Basic Authentication - the user or rest client specifies his login and password to access the rest service. Login and password are transmitted over the network as plain text encoded plain Base64 and can be easily decoded by any user. When using this method, the https protocol must be used for data transmission.
')
The configuration is very simple, so security.xml will look like for our Spring Security

<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <http auto-config="true"> <intercept-url pattern="/rest/**" access="ROLE_USER" /> <logout/> </http> <authentication-manager> <authentication-provider> <user-service > <user name="user" password="pass" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager> </beans:beans> 

This is our rest controller:

 @RequestMapping("/rest/api") @RestController public class RestController { @RequestMapping public Object getInfo() { return //some response MyClass; } } 

And finally, a rest-client based on the RestTemplate sprint. Add the word Basic to the header, then the login and password without spaces, separated by a colon and encoded with Base64.

  RestTemplate restTemplate = new RestTemplate(); String url = "http://localhost:8080/rest/api"; HttpHeaders headers = new HttpHeaders(); headers.add("Authorization", "Basic QWxhZGRpbupvcRVuIHNlc2FtZQ=="); //here is some login and pass like this login:pass HttpEntity<String> request = new HttpEntity<String>(headers); MyClass myclass = restTemplate.exchange(url, HttpMethod.GET, request, MyClass.class).getBody(); 

Method Two : Digest authentication


Digest authentication - This is almost the same as the first method, only the login and password are transmitted in encrypted form, and not as plain text. Login and password are encrypted with MD5 algorithm and it is quite difficult to decrypt it. With this approach, you can use an unprotected http connection and are not afraid that the password will be intercepted by intruders. The implementation of the server side remains the same. It is necessary to change the client a little so that he knows how to send the password in encrypted form. This is where http Apache client comes to the rescue.
In order not to engage in copy-paste, I will provide a link to a project on Github with the implementation of such a client .

Method Three : Token Authentication


The essence of this method is that the user using his credentials logs into the application and receives a token to access the rest service. Access to the service that issues tokens must necessarily be made via an https connection, access to the rest service can be done via the usual http. The token must contain a login, password, it may also contain expiration time and user roles, as well as any information necessary for your application. After the token is ready and for example all its parameters are separated by a colon or another symbol convenient for you, or serialized as a json or xml object, it must be encrypted before being given to the user. Note that only the rest service needs to know how to decrypt this token. After the token comes to the restart service, it decrypts it and receives all the necessary data for authentication and, if necessary, the client restoring authorization. The implementation will be radically different from the previous two.

Our security.xml will now look like this:

 <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd"> <beans:bean id="restAuthenticationEntryPoint" class="com.example.rest.security.RestAuthenticationEntryPoint"/> <http pattern="/rest/**" entry-point-ref="restAuthenticationEntryPoint" use-expressions="true" auto-config="false" create-session="stateless" > <custom-filter ref="authenticationTokenProcessingFilter" position="FORM_LOGIN_FILTER" /> <intercept-url pattern="/rest/**" access="isAuthenticated()" /> <logout /> </http> <beans:bean class="com.example.rest.security.CustomTokenAuthenticationFilter" id="authenticationTokenProcessingFilter"> <beans:constructor-arg type="java.lang.String" value="/rest/**"/> <beans:constructor-arg type="org.springframework.security.authentication.AuthenticationManager" ref="authManager"> </beans:constructor-arg> </beans:bean> <http auto-config="true"> <intercept-url pattern="/token/**" access="ROLE_USER" /> </http> <authentication-manager alias="authManager" erase-credentials="false"> <authentication-provider> <user-service > <user name="user" password="pass" authorities="ROLE_USER" /> </user-service> </authentication-provider> </authentication-manager> </beans:beans> 

The RestAuthenticationEntryPoint bin will look something like this:

 public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" ); } } 

Filter CustomTokenAuthenticationFilter, which will check the validity of the token, the right and so on. and ultimately decide whether this client is allowed to work with our rest service or not will look something like this, but you can implement it differently.

 public class CustomTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter { private AuthenticationManager authenticationManager; @Autowired private CryptService cryptService; //service which can decrypt token public CustomTokenAuthenticationFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(defaultFilterProcessesUrl); this.authenticationManager = authenticationManager; super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(new NoOpAuthenticationManager()); setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler()); } public final String HEADER_SECURITY_TOKEN = "My-Rest-Token"; @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { String token = request.getHeader(HEADER_SECURITY_TOKEN); Authentication userAuthenticationToken = parseToken(token); if (userAuthenticationToken == null) { throw new AuthenticationServiceException("here we throw some exception or text"); } return userAuthenticationToken; } @Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { super.successfulAuthentication(request, response, chain, authResult); chain.doFilter(request, response); } // This method makes some validation depend on your application logic private Authentication parseToken(String tokenString) { try { String encryptedToken = cryptService.decrypt(tokenString); Token token = new ObjectMapper().readValue(encryptedToken, Token.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(token.getUsername(), token.getPassword())); } catch (Exception e) { return null; } return null; } } 

What we have in the end. The user logs in to the application receives an encrypted token, which can be used by the SprT RestTemplate or another rest client adding it to the header, for example, our custom header My-Rest-Token. On the server side, the filter receives a value from this header, decrypts the token, parses it, or parses it into its components and decides whether or not to give access to the client.

Method Four : Digital Signature (public / private key pair)


The idea of ​​this approach is to use a public key cryptosystem . The bottom line is that anyone can access the restart service and get a random character set, or rather an encrypted response from the server and only the owner of the private key can decrypt it.
And so in order.
  1. When a new user is registered on the server, a pair of keys for this user is generated - public and private
  2. Private is sent to the user and only he can decrypt the message (the key must be sent via a secure channel so that no one can intercept it)
  3. At each rest request, the client sends his login so that the service can encrypt the message with the necessary public key.
  4. Service encrypts and sends message
  5. The client accepts it and decrypts with its key.

To implement this approach, you need to write two filters: one on the server side and the other on the client side. The filter on the rest side of the server will encrypt the rispons using the key of the client that made the request. The client's rest filter will decrypt the rispons from the rest of the service using its private key.

You can make this approach even more secure if you generate a key pair on the client side using javascript libraries such as forge . This approach makes it possible not to send a private key over the network at all, but to immediately generate it on the client side, which significantly reduces the risk of compromising this key. The public key is sent to the server for future use when encrypting messages. The sending channel can be unprotected, since there is nothing to worry about if the public key is intercepted (for details, see the link above the public-key cryptosystem).

Method Five : Certificate Authentication


You can set up your server in such a way that if the client does not provide the required certificate when requested, the server will not receive a response from the server, it will be more accurate to get the answer that the certificate is missing or not suitable. You can read more about certificates here . Certificates are of two types:

Below are a few steps on how to create a Self signed certificate using the keytool utility.

generate client and server keys
keytool -genkey -keystore keystore_client -alias clientKey
keytool -genkey -keystore keystore_server -alias serverKey

generate client and server certificates
keytool -export -alias clientKey -rfc -keystore keystore_client> client.cert
keytool -export -alias serverKey -rfc -keystore keystore_server> server.cert

import certificates to the corresponding truststores
keytool -import -alias clientCert -file client.cert -keystore truststore_server
keytool -import -alias serverCert -file server.cert -keystore truststore_client

Now the received certificates should be added to the configuration of our server. In this case, Tomcat is used.
 <Connector port="8443" protocol="org.apache.coyote.http11.Http11Protocol"       maxThreads="150" SSLEnabled="true" scheme="https" secure="true"       keystoreFile="${catalina.home}/conf/cert/keystore_server" keystorePass="changeit"       truststoreFile="${catalina.home}/conf/cert/truststore_server" truststorePass="changeit"       clientAuth="true" sslProtocol="TLS" /> 

Below is the rest client using the Http Apache Client, which is able to provide a rest certificate and perform all the necessary handshakes to get a response from the server.

 public class CertificateAuthenticationServiceImpl implements CertificateAuthenticationService { private static final String keyStorePass = "changeit"; private static final String trustedStorePass = "changeit"; private static final File keyStore = new File(new CertificateAuthenticationServiceImpl().getClass().getResource("/authCertificate/keystore_client").getPath()); private static final File trustedStore = new File(new CertificateAuthenticationServiceImpl().getClass().getResource("/authCertificate/truststore_client").getPath()); private static final String certificateType = "jks"; public String httpGet(URL url) { String resp = null; try { final HttpParams httpParams = new BasicHttpParams(); final KeyStore keystore = KeyStore.getInstance(certificateType); keystore.load(new FileInputStream(keyStore), keyStorePass.toCharArray()); final KeyStore truststore = KeyStore.getInstance(certificateType); truststore.load(new FileInputStream(trustedStore), trustedStorePass.toCharArray()); final SchemeRegistry schemeRegistry = new SchemeRegistry(); schemeRegistry.register(new Scheme(url.toURI().getScheme(), new SSLSocketFactory(keystore, keyStorePass, truststore), url.getPort())); final DefaultHttpClient httpClient = new DefaultHttpClient(new ThreadSafeClientConnManager(httpParams, schemeRegistry), httpParams); try { HttpGet httpget = new HttpGet(url.toString()); CloseableHttpResponse response = httpClient.execute(httpget); try { HttpEntity entity = response.getEntity(); if (entity != null) { resp = EntityUtils.toString(entity); } EntityUtils.consume(entity); } finally { response.close(); } } finally { httpClient.close(); } } catch (Exception e) { throw new RuntimeException(e); } return resp; } } 

Method Six : OAuth2 authorization


Well, the snack I left the most difficult to understand and implement the method. But it is very flexible and well suited for large portals. Again, I will not do copy-paste to read what OAuth is and how it works, go here .
Spring security provides us with the OAuthTemplate class, which makes life much easier for us.
I got all the ideas for implementing my OAuth implementation from this great article; there is even a working draft that can be downloaded.

Conclusion


Well, I hope I managed to clarify the big picture a bit and help you with your own projects. Of course, these are not all ways to protect your rest service, but there are solutions for every taste. Examples of implementations are not common solutions, but are given to fully understand the picture. You can write your own implementation, depending on the needs of the project.

Before choosing security for your project, clearly decide what you want, weigh the pros and cons. You should not complicate the system if the project does not require it.

I hope your applications will be safe and secure.

Source: https://habr.com/ru/post/245415/


All Articles