The post How I got Tier-1 Exceptional Talent Visa and how can you get it too. appeared first on Little Big Extra.
]]>Recently, I was granted the Tier-1 Exceptional Talent visa by Home Office UK and many people have asked me how I got this visa and if they qualify for it. Please note that I am not an immigration expert and this is just a personal experience and not professional advice of any sort. As I have been working in the Digital sector for over a decade, I applied through Tech nation and I am just sharing my experience for stage-1 of this visa which is applying to Tech Nation.
If you know about Tier-1 Exceptional talent visa, you might be familiar that unlike any other visa program this is the one of most coveted yet most desirable visa available. On this visa, you can start your own company, work for any employer, renew this visa as many times as you want and will also be eligible for Indefinite Leave to Remain in the UK after 3/5 years depending on which visa (Talent/Promise) has been given.
However, after the HSMP/Tier-1 programme was closed back in 2011(I believe) the visa routes have become narrower and challenging. Being in the UK for nearly a decade has helped me to get deep insight into UK Business, Technology and culture. I wanted to contribute to the UK economy by the knowledge I have gained during these years and hence I thought of applying for this visa.
The first thing which you will need to do is go to – and have a read in detail and see if you are a good fit. I am sure by the name of this visa type and the list of documentation needed you may feel a bit overwhelmed and might get a thought that this visa isn’t for you or you won’t qualify for it. My strong advice is to read the documentation at least 2-3 times and let it sink in. This way you might be able to identify the gaps between what is required and what you have to offer and what needs to be worked upon.
All the documentation you need for this visa is available on web site (check for latest documentation), you will need to keep in mind that this is a subjective visa and documents which may have worked for one person may not work for others. It is difficult to even advise someone on what exactly the documents are required. However, you will need at least
You can refer to this ( updated as of 11th March -2019, look for latest documentation if available) for more details.
Ok, I have decided to go ahead and apply for this visa, how shall I put the application forward. How do I write my recommendation letters and statement of purpose?
There is no substitute for self-help. Reading and understanding the tech nation document will help you more than anything.
That is what I assume 99% of the people will feel and I also felt the same when I first heard about this visa. I would stress the importance of reading the documentation again and getting a complete understanding.
Calm down if you feel down overwhelmed, come back after few days again to the Tech-Nation website read all the criterion carefully once again.
Some of the tips which I would like to share are as follows
The post How I got Tier-1 Exceptional Talent Visa and how can you get it too. appeared first on Little Big Extra.
]]>The post Five points guide to become a Docker Guru for beginners appeared first on Little Big Extra.
]]>When it comes to Docker many people ask about the courses for learning and mastering containerization and deployment using Docker. Based on my experience of using Docker I feel the best resources to learn Docker are available at . Here I will list out 5 basic steps which will help all, who wish to learn and master Docker
1. Installing Docker on your Operating system/local machine
The first baby step starts with installing Docker on your local machine. Make sure the docker recommendations for memory (at least 4GB of RAM ) are hardware requirements are met. Docker is available for both and .
Docker Community Edition is open source version which can be used free of cost and has most of the docker features minus support.
When the installation finishes, Docker starts automatically, and you should see a Whale Icon in the notification area. which means Docker is running, and accessible from a terminal. Open a command-line terminal and Run docker version to check the version. Run docker run hello-world to verify that Docker can pull and run images from DockerHub. You may be asked for your DockerHub credentials for pulling Images.
2. Familiarizing yourself with docker pull and docker run commands
Next step would be to familiarize yourself with docker pull and docker run commands. docker pull gets the images from docker registry and docker run runs the downloaded image. The running image is also called Docker Container.
If you just use docker run command, then it will first pull the image and then start running the image. While using docker run make sure that you familiarize yourself, especially with the various flag which can be used. There are many flags which can be used but some of the handy ones are as listed below
At this point, I will strongly advise downloading . Kitematic’s one-click install gets Docker running on your Mac and lets you control your app containers from a graphical user interface (GUI). If you missed any flags with your run command, you can fix them using Kitematic’s UI. Also, another feature which I like about kitematic is that it is easy to remove unused images, get into the bash shell of Docker container, see the logs etc.
3. Creating Images using Dockerfile and pushing images to the registry using docker push
Docker file is the building block of Docker images and containers. Dockerfiles use a simple DSL which allows you to automate the steps to create an image. A Docker image consists of read-only layers each of which represents a Dockerfile instruction. The layers are stacked and each one is a delta of the changes from the previous layer.
Once your application is ready and now you want to package it into a Docker Image you will need to use Docker file DSL instructions. If all is well, at this point and all instructions of DockerFile has been completed and a docker image has been created then do a sanity check using docker run command to verify if things are working.
Use docker push to share your images to the registry or to a self-hosted one so they can be used across various environments and by various users/team members.
4. Docker Compose and Persistent Data Storage
Considering that now you are master with the all the above commands and able to create docker images, the next would be to use multiple docker images. With Compose, you use a YAML file to . You can configure as many containers as you want, how they should be built and connected, and where data should be stored. When the YAML file is complete, you can run a single command to build, run, and configure all of the containers.
Since you have now multiple Docker containers running and interacting with each other, now you want to persist the data too. Docker Volumes are your saviour for this cause, you can use the –v or –mount flag to map data from Docker Containers to disks. Since Docker Containers are ephermal, once they are killed any data stored within them will also be lost, hence it is important to use the docker volumes so data can be persisted.
5. Docker Swarm
So now you have multiple containers running using docker-compose, the next step is to ensure availability and high performance for your application by distributing it over the number of Docker hosts inside a cluster. With Docker Swarm, you can control a lot of things,
If you are using Docker Swarm make sure that you familiarize yourself with Quorum of Nodes and Master/Worker configuration. The managers in Docker Swarm need to define a quorum of managers which, in simple terms, means that the number of available manager nodes should be always greater or equal to (n+1)/2, where n is the number of manager nodes. So if you have 3 manager nodes, 2 should be always up, and if you have 5 manager nodes, 3 should be up. Also, it is a good idea to have managers running in different geographic locations.
Hopefully, the above points will help in giving insight into Docker world for novices. I have documented my docker journey over here Docker Archives – Little Big Extra.
The best way to learn is by doing it.
The post Five points guide to become a Docker Guru for beginners appeared first on Little Big Extra.
]]>The post Configuring Kibana and ElasticSearch for Log Analysis with Fluentd on Docker Swarm appeared first on Little Big Extra.
]]>In my previous post, I talked about how to configure fluentd for logging for multiple Docker containers. The post explained how to create a single file for each micro service irrespective of its multiple instances it could have.
However, Log files have limitations it is not easy to extract analysis or find any trends.
Elastic Search and Splunk have become very popular in recent years as they give you allow you to events in real-time, visualise trends and search through logs.
Elastic Search is an open source search engine based on Apache Lucene.It is an extremely fast search engine and is commonly used for log analytics, full-text search and much more.
Along with Kibana, which is a visualisation tool, Elasticsearch can be used for real-time analytics. With Kibana you can create intuitive charts and reports, filters, aggregations and trends based on data.
Since this post is continuation of previous post, I will show you how to modify the fluent.conf for elastic search changes
All we need to do is that we need to add another “store” block like below
<store> @type elasticsearch host elasticsearch port 9200 logstash_format true logstash_prefix logstash logstash_dateformat %Y%m%d include_tag_key true tag_key @log_name flush_interval 1s </store>
In the above config, we are telling that elastic search is running on port 9200 and the host is elasticsearch (which is docker container name). Also we have defined the general Date format and flush_interval has been set to 1s which tells fluentd to send records to elasticsearch after every 1sec.
This is how the complete configuration will look like
<source> @type forward port 24224 bind 0.0.0.0 </source> <match tutum> @type copy <store> @type file path /fluentd/log/tutum.*.log time_slice_format %Y%m%d time_slice_wait 10m time_format %Y%m%dT%H%M%S%z compress gzip utc format json </store> <store> @type elasticsearch host elasticsearch port 9200 logstash_format true logstash_prefix logstash logstash_dateformat %Y%m%d include_tag_key true tag_key @log_name flush_interval 1s </store> </match> <match visualizer> @type copy <store> @type file path /fluentd/log/visualizer.*.log time_slice_format %Y%m%d time_slice_wait 10m time_format %Y%m%dT%H%M%S%z compress gzip utc format json </store> <store> @type elasticsearch host elasticsearch port 9200 logstash_format true logstash_prefix logstash logstash_dateformat %Y%m%d include_tag_key true tag_key @log_name flush_interval 1s </store> </match>
So the next step is to create a custom image of fluentd which has the above configuration file.
Save above file as fluent.conf in a folder named conf and then create a file called DockerFile at the same level as conf folder
# fluentd/Dockerfile FROM fluent/fluentd:v0.12-debian RUN ["gem", "install", "fluent-plugin-elasticsearch", "--no-rdoc", "--no-ri", "--version", "1.9.2"] RUN rm /fluentd/etc/fluent.conf COPY ./conf/fluent.conf /fluentd/etc
In this Docker file as you can see we are replacing the fluent.conf in the base image with the version of ours and also installing elastic search plugin
Now let us create a Docker image by
run "docker build -t ##YourREPOname##/myfluentd:latest ."and then push it to the docker hub repository
"docker push ##YourREPOname##/myfluentd"
This is how the YAML configuration for ElasticSearch looks like
elasticsearch: image: elasticsearch ports: - "9200:9200" networks: - net environment: - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" logging: driver: "json-file" options: max-size: 10M max-file: 1 deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated replicas: 1 placement: constraints: [node.role == manager] update_config: delay: 2s resources: limits: memory: 1000M volumes: - ./esdata:/usr/share/elasticsearch/data
Things to note here for this elastic search configuration file
This is how the YAML configuration for Kibana looks like
kibana: image: kibana ports: - "5601:5601" networks: - net logging: driver: "json-file" options: max-size: 10M max-file: 1 deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated replicas: 1 placement: constraints: [node.role == manager] update_config: delay: 2s
Things to note here for this kibana configuration file
So now we need a complete docker-compose file which will have whoami service with multiple instances, docker visualiser service along with elastic, kibana and fluentd services.
This is how the complete files looks like
version: "3" services: whoami: image: tutum/hello-world networks: - net ports: - "80:80" logging: driver: "fluentd"# Logging Driver options: tag: tutum # TAG deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated replicas: 4 placement: constraints: [node.role == worker] update_config: delay: 2s vizualizer: image: dockersamples/visualizer volumes: - /var/run/docker.sock:/var/run/docker.sock ports: - "8080:8080" networks: - net logging: driver: "fluentd" options: tag: visualizer #TAG deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated # one container per manager node replicas: 1 update_config: delay: 2s placement: constraints: [node.role == manager] fluentd: image: abhishekgaloda/myfluentd volumes: - ./Logs:/fluentd/log ports: - "24224:24224" - "24224:24224/udp" networks: - net deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated replicas: 1 placement: constraints: [node.role == manager] update_config: delay: 2s elasticsearch: image: elasticsearch ports: - "9200:9200" networks: - net environment: - bootstrap.memory_lock=true - "ES_JAVA_OPTS=-Xms512m -Xmx512m" logging: driver: "json-file" options: max-size: 10M max-file: 1 deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated replicas: 1 placement: constraints: [node.role == manager] update_config: delay: 2s resources: limits: memory: 1000M volumes: - ./esdata:/usr/share/elasticsearch/data kibana: image: kibana ports: - "5601:5601" networks: - net logging: driver: "json-file" options: max-size: 10M max-file: 1 deploy: restart_policy: condition: on-failure delay: 20s max_attempts: 3 window: 120s mode: replicated replicas: 1 placement: constraints: [node.role == manager] update_config: delay: 2s networks: net:
You can run above services on docker swarm by using below command, make sure you save the file by the name docker-swarm.yml
docker stack deploy -c docker-swarm.yml test
Once you make sure that all services are up and running using
docker service ls
you can access Kibana on
http://##domain#Or#IP##:5601/
Once You see something similar like below click on Create Button and then Discover on top left, you should see some bars indicating logs
Now if you click on http://##domain#Or#IP##/hello the whomai container will generate some logs which should appear on Kibana provided the right time has been chosen and auto-refresh has been enabled. See the screenshot below.
Kibana offers a lot of ways to create the visualization like charts, graphs etc for log analysis which can be explored further.
Drop in suggestions or comments for any feedback.
Follow this Video for demonstration
The post Configuring Kibana and ElasticSearch for Log Analysis with Fluentd on Docker Swarm appeared first on Little Big Extra.
]]>The post How to add Subject Alt Names or multiple domains in a key-store and self signed certificate appeared first on Little Big Extra.
]]>In this article, we will see how to add multiple domains also known as the Subject alt name in the JKS file. If you are getting SSL handshake exception and your application is complaining about
****javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names present
then basically it means that your JKS file is missing the required domain on which you are trying to access the application.
#For MAC use below command cp /etc/ssl/openssl.cnf . #For RHELinux(Centos7) cp /etc/pki/tls/openssl.cnf . #For Windows you will need to install openssl and then find out where the corresponding file is and use the below defined Method -2
echo '[ subject_alt_name ]' >> openssl.cnf
echo 'subjectAltName = DNS:example.mydomain1.com, DNS:example.mydomain2.com, DNS:example.mydomain3.com, DNS: localhost'>> openssl.cnf
openssl req -x509 -nodes -newkey rsa:2048 -config openssl.cnf -extensions subject_alt_name -keyout private.key -out self-signed.pem -subj '/C=gb/ST=edinburgh/L=edinburgh/O=mygroup/OU=servicing/CN=www.example.com/emailAddress=postmaster@example.com' -days 365
openssl x509 -in self-signed.pem -text -noout
Certificate: Subject: C=gb, ST=edinburgh, L=edinburgh, O=mygroup, OU=servicing, CN=www.example.com/emailAddress=postmaster@example.com Subject Public Key Info: Public Key Algorithm: rsaEncryption Public-Key: (2048 bit) Modulus: 45:17:ea:d5:87: 30:17:e1:50:4a:c7:67:9b:5f:35:c3:0b:0e:f2:83: 32:19....... Exponent: 65537 (0x10001) X509v3 extensions: X509v3 Subject Alternative Name: DNS:example.mydomain1.com, DNS:example.mydomain2.com, DNS:example.mydomain3.com, DNS:localhost Signature Algorithm: sha256WithRSAEncryption 8c:7d:85:5e:37:d2:e7:09:f5:3e:ce:73:d4:d5:3e:5a:ee:e2:
openssl pkcs12 -export -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -export -in self-signed.pem -inkey private.key -name myalias -out keystore.p12
keytool -importkeystore -destkeystore keystore.jks -deststoretype PKCS12 -srcstoretype PKCS12 -srckeystore keystore.p12
keytool -list -v -keystore keystore.jks
Signature algorithm name: SHA256withRSA Subject Public Key Algorithm: 2048-bit RSA key Version: 3 Extensions: SubjectAlternativeName [ DNSName: example.mydomain1.com DNSName: example.mydomain2.com DNSName: example.mydomain3.com DNSName: localhost ]
keytool -export -keystore keystore.jks -alias myalias -file selfsigned.crt
sudo keytool -importcert -file selfsigned.crt -alias myalias -keystore /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/lib/security/cacerts
The post How to add Subject Alt Names or multiple domains in a key-store and self signed certificate appeared first on Little Big Extra.
]]>The post How to fix javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names present appeared first on Little Big Extra.
]]>In this article, we will focus on how to resolve the SSLHandshakeException and possible cause behind it. If you are getting below error, let’s find out how to resolve it.
javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names present at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1959) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:328) at sun.security.ssl.Handshaker.fatalSE(Handshaker.java:322) at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1614)
The reason, we get above error is that CN(Common name) defined in your certificate is not matching with the domain the application is running on.
For e.g, In your certificate, the CN name is defined as www.example.com but you may be running the application say a URL which is like http://localhost:8080/api
There are 2 easy ways to fix the above error
Use this Command to check what is the CN name defined in your certificate
keytool -printcert -v -file certifcate.crt #Where certificate.crt is the name of your certificate
You should get some response like this
Owner: EMAILADDRESS=postmaster@example.com, CN=www.mydomain.com, OU=organisation, O=my group, L=edinburgh, ST=edinburgh, C=gb Issuer: EMAILADDRESS=postmaster@example.com, CN=www.mydomain.comm, OU=organisation, O= my group, L=edinburgh, ST=edinburgh, C=gb Serial number: dcc3d4ffe7a016f2 Valid from: Tue Jun 26 12:41:05 BST 2018 until: Wed Jun 26 12:41:05 BST 2019 Certificate fingerprints: MD5: 32:FE:3A:35:D6:7F:C0:4A:0D:95:99:10:9A:71:1D:DC SHA1: 10:FF:59:F0:72:83:40:B8:5D:6C:D1:64:33:90:22:17:2B:0E:37:A0 SHA256: C3:1D:34:BA:9D:C4:00:66:9E:C8:91:29:1B:0B:96:5F:D2:00:17:95:DB:72:6E:2C:7B:1E:9B:20:5E:08:1F:60 Signature algorithm name: SHA256withRSA
Pay close attention to the first line in above output CN=www.mydomain.com, if you are running the application from the above domain, you should not encounter the above error.
So to fix the above error simply use one of the below approaches
OR
The post How to fix javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names present appeared first on Little Big Extra.
]]>The post How to fix PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException appeared first on Little Big Extra.
]]>
In the last article, we were trying to enable communication over https between 2 applications using the self-signed certificate. Once the certificate was added and when we ran the application some users would have got the below error.
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:8100/customer/": sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
In this article, we will focus on how to fix the above error.
The reason, we get above error is that JDK is bundled with a lot of trusted Certificate Authority(CA) certificates into a file called ‘cacerts’ but this file has no clue of our self-signed certificate. In other words, the cacerts file doesn’t have our self-signed certificate imported and thus doesn’t treat it as a trusted entity and hence it gives the above error.
To fix the above error, all we need is to import the self-signed certificate into the cacerts file.
/Library/Java/JavaVirtualMachines/
{{JDK_version}}/Contents/Home/jre/lib/security
{{Installation_directory}}/{{JDK_version}}/jre/lib/security
##To generate certificate from keystore(.jks file) #### keytool -export -keystore keystore.jks -alias selfsigned -file selfsigned.crt
Above step will generate a file called selfsigned.crt
#### Now add the certificate to JRE/lib/security/cacerts (trustore) ##### keytool -importcert -file selfsigned.crt -alias selfsigned -keystore {{cacerts path}}
for e.g
keytool -importcert -file selfsigned.nextgen.crt -alias selfsigned.nextgen -keystore /Library/Java/JavaVirtualMachines/jdk1.8.0_171.jdk/Contents/Home/jre/lib/security/cacerts
That’s all, restart your application and it should work fine. If it still doesn’t work and get an SSL handshake exception. It probably means you are using different domain then registered in the certificate.
The post How to fix PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException appeared first on Little Big Extra.
]]>The post How to enable communication over https between 2 spring boot applications using self signed certificate appeared first on Little Big Extra.
]]>
In this tutorial, we will try to cover how we can enable HTTPS communication over 2 Spring boot applications. HTTPS was developed for exchanging confidential information in a secured manner by making use of encryption using public and private keys in order to prevent unauthorized access.
In the production environment, you will need to install a certificate issued by a certificate authority (CA) which will validate identity and then and issue certificates. CA certificates contain a public key corresponding to a private key. The CA owns the private key and uses it to sign the certificates it issues. Certificates help prevent the use of fake public keys for impersonation.
However, CA-signed certificates might not be available in the lower environments like DEV or for local testing, in this case, you might want to establish that your API’s are able to talk over HTTPS and this is where you can make use of the self-signed certificate.
We will use a self-signed certificate, to generate one we will need OpenSSL installed on our machine. In MacOs and Linux machines it is preinstalled, however, in windows you have to install it. Keytool can also be used to generate the self-signed certificates
openssl req -x509 -nodes -newkey rsa:2048 -keyout private.key -out selfsigned.pem -subj '/C=gb/ST=XX/L=XX/O=XX/OU=XX/CN=localhost/emailAddress=postmaster@example.com' -days 365
Also please note that above command also defines the country, state, location, organization name for simplification only XX has been added and the validity for above certificate is for a year which is controlled by ‘-days 365’. Feel free to change as per your needs.
openssl x509 -in selfsigned.pem -text -noout
If the certificate has been generated successfully, then basically above command should generate something like this, basically listing out the expiry date, the signature algorithm etc.
Certificate: Data: Version: 1 (0x0) Serial Number: 10095699467019317110 (0x8c1b212d0ac96f76) Signature Algorithm: sha256WithRSAEncryption Issuer: C=gb, ST=XX, L=XX, O=XX, OU=XX, CN=loclhost/emailAddress=postmaster@example.com Validity Not Before: Jun 27 10:52:32 2018 GMT Not After : Jun 27 10:52:32 2019 GMT Subject: C=gb, ST=XX, L=XX, O=XX, OU=XX, CN=loclhost/emailAddress=postmaster@example.com Subject Public Key Info:
openssl pkcs12 -export -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -export -in selfsigned.pem -inkey private.key -name selfsigned -out keystore.p12
keytool -importkeystore -srcstoretype PKCS12 -srckeystore keystore.p12 -destkeystore keystore.jks -deststoretype pkcs12
Add the following configuration to your spring boot application.yaml file and copy the above-generated keystore.jks file in src/main/resources folder
server: ssl: key-store-type: PKCS12 key-store: 'classpath:keystore.jks' key-store-password: changeit key-alias: selfsigned
Now restart your application and see if you can see something in the logs ” Tomcat started on port(s): 8080 (https) with context path.. ”
This confirms that application will work only on https and not on http.
Considering the above changes went well and both the Spring boot application has started and run on different ports with HTTPS enabled (server.port: port_number property can be used to configure different port numbers)
To call the other spring Boot application all we need is change the URL to use ‘https’
You can use RestTemplate to make a call
@Autowired RestTemplate restTemplate; void someRandomFunction(){ restTemplate.getForObject("https://localhost:8090/SpringBootApp1", Response.class) }
If you get the below exception
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:8100/customer/session/1b63e4b8-a91b-4742-b70a-8f113271212": sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
If you get SSL Handshake exception
****javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No subject alternative names present at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
The post How to enable communication over https between 2 spring boot applications using self signed certificate appeared first on Little Big Extra.
]]>The post How to maintain Session Persistence (Sticky Session) in Docker Swarm appeared first on Little Big Extra.
]]>Stateless services are in vogue and rightfully so as they are easy to scale up and are loosely coupled. However, it is practically impossible to stay away from stateful services completely. For example, say you might need a login application where user session details need to be maintained across several pages.
Session state can be maintained either using
or a combination of both.
Maintaining a user session is relatively easy if you are using a typical monolithic architecture where your application is installed on a couple of servers and you can change the configuration in servers to facilitate session replication using some cache mechanism or session stickiness using a load balancer/reverse proxy.
However, In the case of Microservices, where the scale can be as large from 10 to 10000’s instances the session replication might slow up things as each and every service need to look up at the centralised cache to get session information.
The other approach Session Stickiness where each following request should keep going to the same server( Docker container) and hence preserving the session will be looked at in this article.
Load balancer typically works on Layer 7 OSI model, the application layer (HTTP protocol at this layer) and then distributes the data across multiple machines, but Docker ingress routing mesh works at level 4 in OSI layer.
Someone in StackOverflow has summarized the solution for above problem as- To implement sticky sessions, you would need to implement a reverse proxy inside of docker that supports sticky sessions and communicates directly to the containers by their container id (rather than doing a DNS lookup on the service name which would again go to the round robin load balancer). Implementing that load balancer would also require you to implement your own service discovery tool so that it knows which containers are available.
So I tried implementing the reverse proxy with Nginx and it worked with multiple containers on a single machine but when deployed on Docker Swarm it doesn’t work probably because I was using the service discovery by name and as suggested above, I should use containerId to communicate and not container names.
Read about the Jwilder Nginx proxy which works for everyone and it worked on my local but when deployed on Swarm it won’t generate anything any container IP’s inside the
upstream{server}
Desperate enough by this time I was going through all possible solutions people have to offer about on the internet (stack overflow, Docker community forums..) and one gentleman has mentioned something about Traefik. Eyes glittered when I read that it works on SWARM and here I go.
Even though I was very comfortable with Nginx and assumed that learning will again be an overhead. It wasn’t the case Traefik is simple to learn and easy to understand and good thing is that you need not fiddle with any of the conf files.
I have tested the configuration with Docker compose version 3 which is the latest and deployed using Docker stack deploy
To start off you need to create a docker-compose.yml (version 3) and add the load balancer Traefik Image. This is how it looks like
loadbalancer: image: traefik command: --docker \ --docker.swarmmode \ --docker.watch \ --web \ --loglevel=DEBUG ports: - 80:80 - 9090:8080 volumes: - /var/run/docker.sock:/var/run/docker.sock deploy: restart_policy: condition: any mode: replicated replicas: 1 update_config: delay: 2s placement: constraints: [node.role == manager] networks: - net
Few things to note here
placement: constraints: [node.role == manager]specifies that traefik run only on manager node.
To add a Docker Image which will hold session stickyness we need to add something like this
whoami: image: tutum/hello-world networks: - net ports: - "80" deploy: restart_policy: condition: any mode: replicated replicas: 5 placement: constraints: [node.role == worker] update_config: delay: 2s labels: - "traefik.docker.network=test_net" - "traefik.port=80" - "traefik.frontend.rule=PathPrefix:/hello;" - "traefik.backend.loadbalancer.sticky=true"
This is a hello world image which displays the container name its running on. We are defining in this file to have 5 replicas of this container. The important section where traefik does the magic is in “labels”
- "traefik.docker.network=test_net"Tells on which network this image will run on. Please note that the network name is test_net, where test is the stack name. In the load balancer service we just gave net as name.
- "traefik.port=80"This Helloworld is running on docker port 80 so lets map the traefik port to 80
- "traefik.frontend.rule=PathPrefix:/hello"All URLs starting with {domainname}/hello/ will be redirected to this container/application
- "traefik.backend.loadbalancer.sticky=true"The magic happens here, where we are telling to make sessions sticky.
Try to use the below file as it is and see if it works, if it does then fiddle with it and make your changes accordingly.
You will need to create a file called docker-compose.yml on your Docker manager node and run this command
docker stack deploy -c docker-compose.yml testwher the “test” is the namespace.
version: "3" services: whoami: image: tutum/hello-world networks: - net ports: - "80" deploy: restart_policy: condition: any mode: replicated replicas: 5 placement: constraints: [node.role == worker] update_config: delay: 2s labels: - "traefik.docker.network=test_net" - "traefik.port=80" - "traefik.frontend.rule=PathPrefix:/hello;" - "traefik.backend.loadbalancer.sticky=true" loadbalancer: image: traefik command: --docker \ --docker.swarmmode \ --docker.watch \ --web \ --loglevel=DEBUG ports: - 80:80 - 9090:8080 volumes: - /var/run/docker.sock:/var/run/docker.sock deploy: restart_policy: condition: any mode: replicated replicas: 1 update_config: delay: 2s placement: constraints: [node.role == manager] networks: - net networks: net:
Now you can test this service by http://{Your-Domain-name}/hello and http://{Your-Domain-name}:9090 should show us a Traefik dashboard.
Though there are 5 replicas of above “whoami” service, it should always display the same container ID. If it does congratulations your session peristence is working.
This is how the dashboard of Traefik looks like
In case you don’t have a swarm node and just want to test it on your localhost machine. You can use the following docker-compose file. To run successfully create a directory called test( required for namespace, as we have given our network name as test_net
- "traefik.docker.network=test_net", change the directory name if you have different network) and run
docker-compose up -d
version: "3" services: whoami: image: tutum/hello-world networks: - net ports: - "80" labels: - "traefik.backend.loadbalancer.sticky=true" - "traefik.docker.network=test_net" - "traefik.port=80" - "traefik.frontend.rule=PathPrefix:/hello" loadbalancer: image: traefik command: --docker \ --docker.watch \ --web \ --loglevel=DEBUG ports: - 80:80 - 25581:8080 volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - net networks: net:
Docker-Compose should create the required services and whoami service should be available on http://localhost/hello.
Scale say this service to 5
docker-compose scale whoami=5and test
Follow this Video to see things in action
The post How to maintain Session Persistence (Sticky Session) in Docker Swarm appeared first on Little Big Extra.
]]>The post Part-2: Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>
In the last part, I had demonstrated how we can use spring-social to authorize user using Facebook, Google and LinkedIn API’s. If you have not read the last part, I would request you to have a look and then come back to this part.
In this part, I am going to use spring security to allow only logged in users or authenticated users to navigate to secure pages, any user attempting to go to secure pages will be redirected to the Login page for authentication.
Once the user is authenticated, we will save his details to in-memory DB and then the user can log out and log in again.
Spring Security framework provides both authentication and authorization feature for applications. It also helps in preventing attacks like session fixation, clickjacking, cross-site request forgery, etc and good thing is that it can be customized easily to fit in different use cases.
In this tutorial, we will add spring security with spring social API to register users and then add the log on and log out functionality.
The first step to start will be adding maven dependencies for Spring-security and also for thymeleaf-extras-spring security for using spring-security tags for displaying logged in user and roles etc on pages.
Add the following dependencies to the POM.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency>
When logging on to social networking platforms, we didn’t need any form as we fetch the user details through the API. However, to register a user we will need a simple form to capture their details. We will create a view called registration.html under src/main/resources/templates/registration.html.
This form will have server-side validation and minimal CSS/JS
Source code of Registration page is as below
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Login</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width" /> <base href="/" /> <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"></link> </head> <body> <div class="container" style="width:80%"> <h1>Registration Page</h1> <br /> <form action="#" th:action="@{/registration}" th:object="${userBean}" method="post" > <div class="form-group" > <label for="email" class="control-label col-sm-2">Email*</label>:: <input type="text" th:field="*{email}" placeholder="Enter email"/> <div style="width:33%" th:if="${#fields.hasErrors('email')}" th:errors="*{email}" class="alert alert-danger">Email Error</div> </div> <div class="form-group"> <label for="firstName" class="control-label col-sm-2">First Name*</label>:: <input type="text" th:field="*{firstName}" /> <div style="width:33%" th:if="${#fields.hasErrors('firstName')}" th:errors="*{firstName}" class="alert alert-danger">FirstName Error</div> </div> <div class="form-group"> <label for="lastName" class="control-label col-sm-2">Last Name*</label>:: <input type="text" th:field="*{lastName}" /> <div style="width:33%" th:if="${#fields.hasErrors('lastName')}" th:errors="*{lastName}" class="alert alert-danger">LastName Error</div> </div> <div class="form-group"> <label for="password" class="control-label col-sm-2">Password*</label>:: <input type="text" th:field="*{password}" /> <div style="width:33%" th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="alert alert-danger">Password Error</div> </div> <div class="form-group"> <label for="passwordConfirm" class="control-label col-sm-2">Confirm Password*</label>:: <input type="text" th:field="*{passwordConfirm}" /> <div style="width:33%" th:if="${#fields.hasErrors('passwordConfirm')}" th:errors="*{passwordConfirm}" class="alert alert-danger">Password Error</div> </div> <div class="form-group"> <label for="title" class="control-label col-sm-2">Title</label>:: <select th:field="*{title}"> <option value="Mr" th:text="Mr"></option> <option value="Mrs" th:text="Mrs"></option> </select> <div th:if="${#fields.hasErrors('title')}" th:errors="*{title}">Title Error</div> </div> <div class="form-group"> <label for="country" class="control-label col-sm-2">Country</label>:: <select th:field="*{country}"> <option value="India" th:text="India"></option> <option value="UK" th:text="UK"></option> <option value="US" th:text="US"></option> <option value="Japan" th:text="Japan"></option> </select> <div th:if="${#fields.hasErrors('country')}" th:errors="*{country}" class="alert alert-danger">Country Error</div> </div> <input type="hidden" name="provider" value="registration" /> <div class="form-group"> <button type="submit" class="btn btn-primary">Register</button> </div> </form> </div> </body> </html>
To serve this Page, we will modify the existing Login controller
@GetMapping("/registration") public String showRegistration(UserBean userBean) { return "registration"; }
Now let’s modify the login page and add the registration link on the home page, also we will add username and password fields so once the user is registered they can also log in.
This HTML fragment will add a username/password field along with an error message, in case of the user not found.
Also, let’s add another code fragment which is to display the Logged In user, the role assigned and a Logout button
<div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <b><span sec:authentication="name"></span></b> | Roles: <b><span sec:authentication="authorities"></span></b> <form action="#" th:action="@{/logout}" method="post"> <button type="submit" class="btn btn-danger btn-sm" <span class="glyphicon glyphicon-log-out"></span> Log out</button> </form> </div>
We will also need to csrf token to our existing forms for Google, Facebook and LinkedIn
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
This is how the Complete login.html looks like.
Source code of login.html
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Login</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width" /> <base href="/" /> <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"></link> </head> <body> <div class="container"> <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <b><span sec:authentication="name"></span></b> | Roles: <b><span sec:authentication="authorities"></span></b> <form action="#" th:action="@{/logout}" method="post"> <button type="submit" class="btn btn-danger btn-sm"> <span class="glyphicon glyphicon-log-out"></span> Log out</button> </form> </div> <br/> <h1>Login Using</h1> <form th:action="@{/login}" method="post" style="display: inline"> <label for="username">Email </label> : <input type="text" id="username" name="username" autofocus="autofocus" placeholder="Enter email"/> <br /> <label for="password">Password</label>: <input type="password" id="password" name="password" /> <br /> <p th:if="${loginError}" class="alert alert-danger">Wrong email or password combination</p> <button type="submit" class="btn btn-primary"> <span class="fa fa-user"></span>Login </button> </form> <form action="/connect/google" method="POST" style="display: inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="hidden" name="scope" value="profile email" /> <button type="submit" class="btn btn-danger"> Google <span class="fa fa-google-plus"></span> </button> </form> <form action="/connect/facebook" method="POST" style="display: inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="hidden" name="scope" value="public_profile,email" /> <button type="submit" class="btn btn-primary"> Facebook <span class="fa fa-facebook"></span> </button> </form> <form action="/connect/linkedin" method="POST" style="display: inline"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="hidden" name="scope" value="r_basicprofile,r_emailaddress" /> <button type="submit" class="btn btn-primary"> LinkedIn <span class="fa fa-linkedin"></span> </button> </form> <br /> <h3> <p class="bg-important"> <a href="/registration" th:href="@{/registration}">Create Account</a> </p> </h3> </div> </body> </html>
Once the user is authenticated, we will need to save their details in a database. So we will use the annotations like @Entity and @Table to create an in-memory database(HSQLDB) and tables if they don’t exist. The DB details can be easily configured in application.properties,also the DB type can also be changed easily.
We will use the hibernate validator annotations @NotNull,@Size to make sure that input fields are validated on the server side before they can be, you can add client-side javascript if that suits.
package com.login.model; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Transient; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.Email; @Entity(name = "user") @Table(name = "user") public class UserBean implements Serializable{ private static final long serialVersionUID = 1L; @NotNull(message = "Email cannot be empty") @Email(message = "Email Format is not valid") @Size(min = 3, max = 30, message = "Email can not be empty") @Id private String email; @NotNull(message = "First Name cannot be empty") @Size(min = 3, max = 30, message = "First Name cannot be less than 3 characters") private String firstName; @NotNull(message = "Last Name cannot be empty") @Size(min = 3, max = 30, message = "Last Name cannot be less than 3 characters") private String lastName; private String title; private String country; private String password; @Transient private String passwordConfirm; private String provider; private String image; ......getter/setter methods here ... }
We will be using to retrieve user details and save them.
Now we will create a new interface called UserRepository in package com.login.repository this interface will extend the JPARespository<T, ID> where T is UserBean in our Case and ID is the email(primary key).
We will define the abstract method findByEmail by passing email which has been defined as a primary key in UserBean class. To get the UserBean for a particular method all we need to do is inject the UserRepository and call findByemail method()
package com.login.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.login.model.UserBean; public interface UserRepository extends JpaRepository<UserBean, String> { UserBean findByEmail(String email); }
The key advantage of using Spring Data is that it makes virtually our code DAO implementation-free, only above interface will be able to save and retrieve user details.
This is the crux of this tutorial, we will use the spring security to define which URL’s can be accessed only by insecurely and which one can only be accessed by login user.
We will add a class called SecurityConfig to allow users to
package com.login.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; //@formatter:off @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsService userDetailsService; @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/css/**", "/connect/**").permitAll() .antMatchers("/secure/**") .authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/secure/user") .failureUrl("/login-error") .permitAll() .and() .logout() .permitAll(); } @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } } // @formatter:on
As you might have noticed that we have added a bean for BCryptPasswordEncoder, this bean will be used for encrypting the password as hash, which is one of the safest technique to store passwords and decrypting hash is very tough ( Maybe not as tough as mining a BitCoin )
Also, there is another method configureglobal(AuthenticationManagerBuilder auth) which basically defines that we have defined our own custom implementation of UserDetailsService, we will talk about it later.
Since now we want our user.html to be only presented for logged in users we will move user.html from src/main/resources/templates to src/main/resources/templates/secure/user.html
Also, we will add the thymleaf authentication tags to display logged in Username and role.
This is how the complete user.html looks like, make sure it is in src/main/resources/templates/secure/user.html
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Login</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width" /> <meta name="ctx" th:content="${#httpServletRequest.getContextPath()}" /> <base href="/" /> <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"></link> </head> <body> <div class="container"> <h1>Secure Page</h1> <div th:fragment="logout" class="logout" sec:authorize="isAuthenticated()"> Logged in user: <b><span sec:authentication="name"></span></b> | Roles: <b><span sec:authentication="authorities"></span></b> <form action="#" th:action="@{/logout}" method="post"> <button type="submit" class="btn btn-danger btn-sm"> <span class="glyphicon glyphicon-log-out"></span> Log out</button> </form> </div> <br/> <form th:object="${loggedInUser}" method="post"> <div class="row"> <label for="email">Email :</label> <span th:text="*{email}" /> </div> <div class="row"> <label for="firstName">Name:</label> <span th:text="*{firstName}" /> <span th:text="*{lastName}" /> </div> <div class="row"> <label for="image">Image:</label> <img th:attr="src=@{*{image}}" style="width: 150px; height: 150px;"/> </div> </form> <br /> <a href="/login" th:href="@{/login}" class="btn btn-info btn-lg"> <span class="glyphicon glyphicon-chevron-left"></span> Login using other social Providers </a> </div> </body> </html>
This is the crucial step as would define here what to do when the user puts in his username/password combination.
If we want user authentication by a DAO class, we need to implement the UserDetailsService interface. This interface has loadUserByUsername() method which is used to validate the user, of course, we need to provide the implementation.
Remeber this method in Security config
@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }
We will write the implementation of method loadUserByUsername.In this method, we will do couple of things
package com.login.security.service.impl; import java.util.HashSet; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.login.model.UserBean; import com.login.repository.UserRepository; @Service public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Override @Transactional(readOnly = true) public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { UserBean user = userRepository.findByEmail(email); if (user == null) { throw new UsernameNotFoundException("No user found with email: " + email); } Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); grantedAuthorities.add(new SimpleGrantedAuthority("LOGGED_USER")); return new User(user.getEmail(), user.getPassword(), grantedAuthorities); } }
Once the user is logged in by social providers or by registering a user we need to update security context by setting the authentication
As per the spring API – “Authentication represents the token for an authentication request or for an authenticated principal once the request has been processed by the AuthenticationManager.authenticate(Authentication) method.
Once the request has been authenticated, the Authentication will usually be stored in a thread-local SecurityContext managed by the SecurityContextHolder by the authentication mechanism which is being used. An explicit authentication can be achieved, without using one of Spring Security’s authentication mechanisms, by creating an Authentication instance and using the code:
SecurityContextHolder.getContext().setAuthentication(authentication)
Which is exactly we are going to do in our method.
package com.login.autologin; import java.util.HashSet; import java.util.Set; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import com.login.model.UserBean; @Service public class Autologin { public void setSecuritycontext(UserBean userForm) { Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); grantedAuthorities.add(new SimpleGrantedAuthority(userForm.getProvider().toUpperCase())); Authentication authentication = new UsernamePasswordAuthenticationToken(userForm.getEmail(), userForm.getPassword(), grantedAuthorities); SecurityContextHolder.getContext().setAuthentication(authentication); } }
Since now we have most of the things in place, we need to add the controller so when the registration form is submitted,
Also, since in our Security config, we defined a “/login-error” path, we will handle that too in the same controller.
@Autowired private UserRepository userRepository; @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private Autologin autologin; @PostMapping("/registration") public String registerUser(HttpServletResponse httpServletResponse, Model model, @Valid UserBean userBean, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "registration"; } userBean.setProvider("REGISTRATION"); // Save the details in DB if (StringUtils.isNotEmpty(userBean.getPassword())) { userBean.setPassword(bCryptPasswordEncoder.encode(userBean.getPassword())); } userRepository.save(userBean); autologin.setSecuritycontext(userBean); model.addAttribute("loggedInUser", userBean); return "secure/user"; } /** If we can't find a user/email combination */ @RequestMapping("/login-error") public String loginError(Model model) { model.addAttribute("loginError", true); return "login"; }
In previous post we had created the FacebookProvider, GoogleProvider and LinkedInProvider now we need to make some changes in them so they
In our class BaseProvider.java, we will add saveUserDetails and autoLoginUser method
@Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private UserRepository userRepository; @Autowired protected Autologin autologin; protected void saveUserDetails(UserBean userBean) { if (StringUtils.isNotEmpty(userBean.getPassword())) { userBean.setPassword(bCryptPasswordEncoder.encode(userBean.getPassword())); } userRepository.save(userBean); } public void autoLoginUser(UserBean userBean) { autologin.setSecuritycontext(userBean); }
In our provider classes(FacebookProvider, GoogleProvider and LinkedInProvider) we just need to add the code to
baseProvider.saveUserDetails(userForm);
baseProvider.autoLoginUser(userForm);
return "secure/user"
You will need to do these changes in all 3 classes(GoogleProvider, FaceBookProvider and LinkedInProvider)
public String getLinkedInUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = baseProvider.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(LinkedIn.class) == null) { return REDIRECT_LOGIN; } populateUserDetailsFromLinkedIn(userForm); //Save the details in DB baseProvider.saveUserDetails(userForm); //Login the User baseProvider.autoLoginUser(userForm); model.addAttribute("loggedInUser",userForm); return "secure/user"; }
Remeber, we have added in our
defaultSuccessUrl("/secure/user")SecurityConfig, well this is the page where we want the user to redirect, once they are authenticated.
Also on user/secure.html, we have defined
th:object="${loggedInUser}"this attribute also needs to be initialized once the user is logged in by security config. So the question is how do we initialize the
"${loggedInUser}"model attribute. If we don’t initialise it we will get an error.
@ModelAttribute("loggedInUser"). If this annotation has been used all the RequestMapping method in the controller will be called only after the annotated method.
We can define a method where we will get the Authentication object and then use that object to find the user details.
@ModelAttribute("loggedInUser") public void secure(Model model) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); UserBean user = userRepository.findByEmail(auth.getName()); model.addAttribute("loggedInUser", user); }
Also, we need to define the method for mapping
@GetMapping("/secure/user")as shown below. Since we need to initialize model attribute
${loggedInUser}only for
@GetMapping("/secure/user")we need to define it in a separate controller class.
package com.login.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ModelAttribute; import com.login.model.UserBean; import com.login.repository.UserRepository; @Controller public class LoggedInUserController { @Autowired private UserRepository userRepository; @ModelAttribute("loggedInUser") public void secure(Model model) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); UserBean user = userRepository.findByEmail(auth.getName()); model.addAttribute("loggedInUser", user); } @GetMapping("/secure/user") public String securePage() { return "secure/user"; } }
Since we are using HSQLDB in this example and spring boot will configure most of the things for us, we don’t need to do more than defining a couple of properties for initializing the database. Add these properties in application.properties
spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true
Spring Social and Spring Security are very flexible and easily customizable for different use cases. They both can be used together to provide a seamless and smooth log in experience for users. The above code is just a step in explaining how both the offerings (Spring Social and Spring Security) can be used in unison.
For readers benefit, above code can be cloned from
This is how the project structure looks like
This is how the screen looks like after user is logged in, the logged in user and the roles are being displayed on top of screen.
The post Part-2: Authorising user using Spring Social (Google, Facebook, LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>The post Part -1 : Authorising user using Spring Social (Google, FaceBook and LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>Social Logins are becoming increasingly popular across web applications, they not only offer good user experience are safe and also saves the user from password fatigue. Imagine losing a customer because they can not log on to the application as they cannot remember a password are not bothered to reset passwords etc. A social login is a kind of single sign-on where you use login information of a social network like Facebook, Twitter, Google+ to log on to a third website, instead of creating a new log-in account specially for that website.
Earlier I have written few blogs about Spring-Social features where I had written about few problems like no support for Google, only one user allowed by spring-social etc and also few exceptions which occur and then how to fix them.I thought of collating all the problems and writing a step by step tutorial explaining in detail, on how to register a user using spring social.
The main aim of this tutorial will be to integrate spring-security and spring-social together and create a web application where users can be registered either by
In this first part of the tutorial, our aim will be to get data from spring-social and then display the details on the page, in the second part we will integrate the spring-security where we will ask the user to enter his details and then store on DB.
Assuming that you know how to create a simple spring boot project, let us move on to add maven-dependencies required to run the project. If not, then you can create a spring-starter project and add thymleaf and web support.
The first step would be to add spring-social maven repositories, following dependencies are required
<dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-facebook</artifactId> <version>3.0.0.M1</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-linkedin</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-google</artifactId> <version>1.0.0.RELEASE</version> </dependency>
In case you get the repository not available error, you might have to add the following repository to your pom.
<repositories> <repository> <id>alfresco-public</id> <url>https://artifacts.alfresco.com/nexus/content/groups/public</url> </repository> </repositories>
To make our web pages look nice we will add some Bootstrap CSS, Font-awesome and Jquery to them and also we will use thymleaf for HTML. Read Here if you want to know more in detail.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>font-awesome</artifactId> <version>4.7.0</version> </dependency>
To avoid any mistakes, here is the complete POM.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.login</groupId> <artifactId>SpringLogin</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>SpringLogin</name> <description>Spring Login - Example POC</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-core</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-config</artifactId> <version>2.0.0.M2</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-facebook</artifactId> <version>3.0.0.M1</version> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-linkedin</artifactId> </dependency> <dependency> <groupId>org.springframework.social</groupId> <artifactId>spring-social-google</artifactId> <version>1.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>2.1.1</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>font-awesome</artifactId> <version>4.7.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> </dependency> </dependencies> <repositories> <repository> <id>alfresco-public</id> <url>https://artifacts.alfresco.com/nexus/content/groups/public</url> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Once the dependencies have been resolved be will create a View and Controller
To start we will create a view which will have 3 buttons each for Google, Facebook and Login. When the user will click on any of them he will be asked for authorization for the selected provider, upon verification his details will be rendered on the page.
Create a file under src/main/resources/templates/login.html with the following content. Please note that this view has 3 different forms each for different provider
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Login</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width" /> <base href="/" /> <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"></link> </head> <body> <div class="container"> <h1>Login Using</h1> <form action="/connect/google" method="POST" style="display: inline"> <input type="hidden" name="scope" value="profile email" /> <button type="submit" class="btn btn-danger"> Google <span class="fa fa-google-plus"></span> </button> </form> <form action="/connect/facebook" method="POST" style="display: inline"> <input type="hidden" name="scope" value="public_profile,email" /> <button type="submit" class="btn btn-primary"> Facebook <span class="fa fa-facebook"></span> </button> </form> <form action="/connect/linkedin" method="POST" style="display: inline"> <input type="hidden" name="scope" value="r_basicprofile,r_emailaddress" /> <button type="submit" class="btn btn-primary"> LinkedIn <span class="fa fa-linkedin"></span> </button> </form> </div> </body> </html>
Let us create a controller class called LoginController in package com.login.controller with a simple method which will map the above page to URL / or /login.
@Controller public class LoginController { @RequestMapping(value = { "/","/login" }) public String login() { return "login"; } }
Now run the above application and at this point, a page like this should appear and of course, it won’t do anything yet.
Once we verify the user via our social provider, we will show the user details on another page. We can store these details in a POJO called UserBean.
Let us define a class called UserBean.java as shown below in package com.login.model
package com.login.model; import java.beans.Transient; import java.io.Serializable; public class UserBean implements Serializable{ private static final long serialVersionUID = 1L; private String firstName; private String lastName; private String email; private String title; private String country; private String password; private String passwordConfirm; private String provider; private String image; public String getEmail() { return email; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public void setEmail(String email) { this.email = email; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getPasswordConfirm() { return passwordConfirm; } public void setPasswordConfirm(String passwordConfirm) { this.passwordConfirm = passwordConfirm; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getProvider() { return provider; } public void setProvider(String provider) { this.provider = provider; } }
For Facebook, Google and LinkedIn we will create a Provider class which will be used to do authentication and save user details to the UserBean, which can be later displayed on the page. To start off with we will create a BaseProvider which will have a constructor where all the providers will be initialized.
This BaseProvider is created by injecting the Facebook, Google, LinkedIn and ConnectionRepository repository. These objects are a reference to Spring Social’s Facebook, Google, LinkedIn and ConnectionRepository API binding.
package com.login.social.providers; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.facebook.api.Facebook; import org.springframework.social.google.api.Google; import org.springframework.social.linkedin.api.LinkedIn; @Configuration @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) public class BaseProvider { private Facebook facebook; private Google google; private LinkedIn linkedIn; private ConnectionRepository connectionRepository; public BaseProvider(Facebook facebook,Google google, LinkedIn linkedIn, ConnectionRepository connectionRepository) { this.facebook = facebook; this.connectionRepository = connectionRepository; this.google=google; this.linkedIn= linkedIn; } public Facebook getFacebook() { return facebook; } public void setFacebook(Facebook facebook) { this.facebook = facebook; } public ConnectionRepository getConnectionRepository() { return connectionRepository; } public void setConnectionRepository(ConnectionRepository connectionRepository) { this.connectionRepository = connectionRepository; } public Google getGoogle() { return google; } public void setGoogle(Google google) { this.google = google; } public LinkedIn getLinkedIn() { return linkedIn; } public void setLinkedIn(LinkedIn linkedIn) { this.linkedIn = linkedIn; } }
Now let us create provider class for each provider
We will use the below Facebook provider to authorize the user and then access Facebook Data. We will save the fetched user data into our UserBean.
package com.login.social.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.facebook.api.Facebook; import org.springframework.social.facebook.api.User; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import com.login.model.UserBean; @Service public class FacebookProvider { private static final String FACEBOOK = "facebook"; private static final String REDIRECT_LOGIN = "redirect:/login"; @Autowired BaseProvider baseProvider ; public String getFacebookUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = baseProvider.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(Facebook.class) == null) { return REDIRECT_LOGIN; } populateUserDetailsFromFacebook(userForm); model.addAttribute("loggedInUser",userForm); return "user"; } protected void populateUserDetailsFromFacebook(UserBean userForm) { Facebook facebook = baseProvider.getFacebook(); User user = facebook.userOperations().getUserProfile(); userForm.setEmail(user.getEmail()); userForm.setFirstName(user.getFirstName()); userForm.setLastName(user.getLastName()); userForm.setImage(user.getCover().getSource()); userForm.setProvider(FACEBOOK); } }
We will use the below google provider to authorize the user and then access Google Data. We will save the fetched user data into our UserBean.
package com.login.social.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.google.api.Google; import org.springframework.social.google.api.plus.Person; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import com.login.model.UserBean; @Service public class GoogleProvider { private static final String REDIRECT_CONNECT_GOOGLE = "redirect:/login"; private static final String GOOGLE = "google"; @Autowired BaseProvider socialLoginBean ; public String getGoogleUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = socialLoginBean.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(Google.class) == null) { return REDIRECT_CONNECT_GOOGLE; } populateUserDetailsFromGoogle(userForm); model.addAttribute("loggedInUser",userForm); return "user"; } protected void populateUserDetailsFromGoogle(UserBean userform) { Google google = socialLoginBean.getGoogle(); Person googleUser = google.plusOperations().getGoogleProfile(); userform.setEmail(googleUser.getAccountEmail()); userform.setFirstName(googleUser.getGivenName()); userform.setLastName(googleUser.getFamilyName()); userform.setImage(googleUser.getImageUrl()); userform.setProvider(GOOGLE); } }
We will use the below LinkedIn provider to authorize the user and then accessLinkedIn Data. We will save the fetched user data into our UserBean.
package com.login.social.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.linkedin.api.LinkedIn; import org.springframework.social.linkedin.api.LinkedInProfileFull; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import com.login.model.UserBean; @Service public class LinkedInProvider { private static final String LINKED_IN = "linkedIn"; private static final String REDIRECT_LOGIN = "redirect:/login"; @Autowired BaseProvider socialLoginBean ; public String getLinkedInUserData(Model model, UserBean userForm) { ConnectionRepository connectionRepository = socialLoginBean.getConnectionRepository(); if (connectionRepository.findPrimaryConnection(LinkedIn.class) == null) { return REDIRECT_LOGIN; } populateUserDetailsFromLinkedIn(userForm); model.addAttribute("loggedInUser",userForm); return "user"; } private void populateUserDetailsFromLinkedIn(UserBean userForm) { LinkedIn linkedIn = socialLoginBean.getLinkedIn(); LinkedInProfileFull linkedInUser = linkedIn.profileOperations().getUserProfileFull(); userForm.setEmail(linkedInUser.getEmailAddress()); userForm.setFirstName(linkedInUser.getFirstName()); userForm.setLastName(linkedInUser.getLastName()); userForm.setImage(linkedInUser.getProfilePictureUrl()); userForm.setProvider(LINKED_IN); } }
If you would have noticed by now, spring social doesn’t support Google so to enable autoconfiguration of spring-social-google we need to
Spring social do have a spring-social-google project but it doesn’t have autoconfiguration for Google. So google authorization doesn’t work with spring boot autoconfigure straight away.
To enable autoconfigure of spring-social-google we need to
FacebookAutoConfiguration class you should be able to find it either in org.springframework.boot.autoconfigure.social package in spring-autoconfigure.jar does the autoconfiguration for Facebook, the cheat sheet or the trick would be to copy this File and replace Facebook with Google to enable GoogleAutoConfiguration
Alternatively, copy the below class and put in some package, make sure it is available in classpath(src/main/java or inside another source folder)
package com.login.config.google; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.social.SocialAutoConfigurerAdapter; import org.springframework.boot.autoconfigure.social.SocialWebAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.social.connect.Connection; import org.springframework.social.connect.ConnectionFactory; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.connect.web.GenericConnectionStatusView; import org.springframework.social.google.api.Google; import org.springframework.social.google.connect.GoogleConnectionFactory; @Configuration @ConditionalOnClass({ SocialConfigurerAdapter.class, GoogleConnectionFactory.class }) @ConditionalOnProperty(prefix = "spring.social.google", name = "app-id") @AutoConfigureBefore(SocialWebAutoConfiguration.class) @AutoConfigureAfter(WebMvcAutoConfiguration.class) public class GoogleAutoConfiguration { @Configuration @EnableSocial @EnableConfigurationProperties(GoogleProperties.class) @ConditionalOnWebApplication protected static class GoogleConfigurerAdapter extends SocialAutoConfigurerAdapter { private final GoogleProperties properties; protected GoogleConfigurerAdapter(GoogleProperties properties) { this.properties = properties; } @Bean @ConditionalOnMissingBean(Google.class) @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES) public Google google(ConnectionRepository repository) { Connection<Google> connection = repository.findPrimaryConnection(Google.class); return connection != null ? connection.getApi() : null; } @Bean(name = { "connect/googleConnect", "connect/googleConnected" }) @ConditionalOnProperty(prefix = "spring.social", name = "auto-connection-views") public GenericConnectionStatusView googleConnectView() { return new GenericConnectionStatusView("google", "Google"); } @Override protected ConnectionFactory<?> createConnectionFactory() { return new GoogleConnectionFactory(this.properties.getAppId(), this.properties.getAppSecret()); } } }
In the same package add the below class, this is needed so when we add secret keys in properties file for google. We wont see any error.
package com.login.config.google; import org.springframework.boot.autoconfigure.social.SocialProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "spring.social.google") public class GoogleProperties extends SocialProperties{ }
Spring Social is quick and easy to start but it needs some configuring before it can be used for production. The default flow for
The Views are predefined so the user flow is login.html -> connect/facebookConnected.html by default, irrespective of what our submit URL is, the facebook will override the flow and redirect you to connect/facebookConnected.html.
Read Here: How to change the default spring social redirect page flow
We will add a ChangeDefaultFlowController which will override the connectedView method and redirect the flow to “/facebook”, “/google” or “/LinkedIn”
package com.login.controller; import org.springframework.social.connect.ConnectionFactoryLocator; import org.springframework.social.connect.ConnectionRepository; import org.springframework.social.connect.web.ConnectController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/connect") public class ChangeDefaultFlowController extends ConnectController { public ChangeDefaultFlowController(ConnectionFactoryLocator connectionFactoryLocator, ConnectionRepository connectionRepository) { super(connectionFactoryLocator, connectionRepository); } @Override protected String connectedView(String providerId) { return "redirect:/"+providerId; } }
If you test the Spring Facebook example, accessing FB data you will realise that it only supports one user. So when the first users log in to Facebook, only his details will be shared across all new sessions/users and they won’t be asked for any kind of authentication. One of the forum says that this example is supposed to demonstrate what can be done and is not intended for production use.
Read Here: Spring Social Facebook Authentication Example for multiple users
To fix this problem you need to override a method which always returns a string called “anonymous”.The solution is to override the “anonymous�? as the UserId for each new user/session. So for each session, we can simply return a SessionID, however, it may not be unique enough to identify users, especially if it’sh being cached or stored somewhere in a connection database.
Using a Universally Unique Identifier(UUID) would be a safer bet.So we will store a new UUID in session so it persists between different requests but is alive only till the session is valid. See the below method which does the trick.
package com.login.identifier; import java.util.UUID; import org.springframework.context.annotation.Configuration; import org.springframework.social.UserIdSource; import org.springframework.social.config.annotation.SocialConfigurerAdapter; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @Configuration public class SessionIdentifier extends SocialConfigurerAdapter { @Override public UserIdSource getUserIdSource() { return new SessionIdUserIdSource(); } private static final class SessionIdUserIdSource implements UserIdSource { @Override public String getUserId() { RequestAttributes request = RequestContextHolder.currentRequestAttributes(); String uuid = (String) request.getAttribute("_socialUserUUID", RequestAttributes.SCOPE_SESSION); if (uuid == null) { uuid = UUID.randomUUID().toString(); request.setAttribute("_socialUserUUID", uuid, RequestAttributes.SCOPE_SESSION); } return uuid; } } }
Now since we have added all the fixes, its time to update the controller so our view can submit the forms to Controller
package com.login.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.login.model.UserBean; import com.login.social.providers.FacebookProvider; import com.login.social.providers.GoogleProvider; import com.login.social.providers.LinkedInProvider; @Controller public class LoginController { @Autowired FacebookProvider facebookProvider; @Autowired GoogleProvider googleProvider; @Autowired LinkedInProvider linkedInProvider; @RequestMapping(value = "/facebook", method = RequestMethod.GET) public String loginToFacebook(Model model) { return facebookProvider.getFacebookUserData(model, new UserBean()); } @RequestMapping(value = "/google", method = RequestMethod.GET) public String loginToGoogle(Model model) { return googleProvider.getGoogleUserData(model, new UserBean()); } @RequestMapping(value = "/linkedin", method = RequestMethod.GET) public String helloFacebook(Model model) { return linkedInProvider.getLinkedInUserData(model, new UserBean()); } @RequestMapping(value = { "/","/login" }) public String login() { return "login"; } }
Assuming that you know how to register the API and public/private keys on Google/FB and LinkedIn for this example purpose you can use the below properties file.
Also, notice that server starts on port 3000(don’t change that, since the URL registered with these keys is localhost:3000
spring.social.facebook.appId=384261248599251 spring.social.facebook.appSecret=fd7fa1c5f5a267f463263a0ce7ff2025 spring.social.linkedin.app-id=771mrzk94hye1w spring.social.linkedin.app-secret=iIJFgBf9lCb18zYe spring.social.google.appId=12894100090-tqso3lih5o42isneort886la2pesafmp.apps.googleusercontent.com spring.social.google.appSecret=9xfU16efvxQ-BTMsXT9wOLpw server.port:3000
This is how the project structure will look like
To show the authenticated user on the page, we will display the UserBean using thymleaf. Add the below html page under src/main/resources/static/ as user.html
<!doctype html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <title>Login</title> <meta name="description" content="" /> <meta name="viewport" content="width=device-width" /> <meta name="ctx" th:content="${#httpServletRequest.getContextPath()}" /> <base href="/" /> <link rel="stylesheet" type="text/css" href="/webjars/bootstrap/css/bootstrap.min.css" /> <script type="text/javascript" src="/webjars/jquery/jquery.min.js"></script> <script type="text/javascript" src="/webjars/bootstrap/js/bootstrap.min.js"></script> <link rel="stylesheet" href="/webjars/font-awesome/css/font-awesome.min.css"></link> </head> <body> <div class="container"> <br /><br /><br /> <form th:object="${loggedInUser}" method="post"> <div class="row"> <label for="username">Username:</label> <span th:text="${loggedInUser.email}" /> </div> <div class="row"> <label for="firstName">Name:</label> <span th:text="*{firstName}" /> <span th:text="*{lastName}" /> </div> <div class="row"> <label for="image">Name:</label> <img th:attr="src=@{*{image}}" style="width: 150px; height: 150px;"/> </div> </form> <br /> <a href="/login" th:href="@{/login}" class="btn btn-info btn-lg"> <span class="glyphicon glyphicon-chevron-left"></span> Login using other social Providers </a> </div> </body> </html>
Now run the Project and access the application on http://localhost:3000/login.
You should be greeted by an authentication popup depending upon your provider selected and then you should be redirected to the page where your username, name, profile image will be displayed as below.
Spring Social API helps us in implementing an authentication mechanism with social providers. It is easy to use and we have seen how to configure it to tailor it to our needs.
In the next part we will use spring-security to register users and then only allow logged in users to navigate to secure pages. The link has been provided below for second part.
The post Part -1 : Authorising user using Spring Social (Google, FaceBook and LinkedIn) and Spring Security appeared first on Little Big Extra.
]]>