Introduction
Overview
You can download source of this example application from GitHub.
Prerequisites
Core Dependencies
<spring.version>4.2.0.RELEASE</spring.version> <spring.security.version>3.2.3.RELEASE</spring.security.version> <spring.social.version>1.1.0.RELEASE</spring.social.version> <spring.social.facebook.version>1.1.0.RELEASE</spring.social.facebook.version> <spring.social.twitter.version>1.1.0.RELEASE</spring.social.twitter.version> <spring.social.linkedin.version>1.0.0.RELEASE</spring.social.linkedin.version>
Spring security are core module contains core authentication and and access control components.
Spring social module contains the connect framework and provides support for OAuth clients.The security module integrates Spring Security with Spring Social. It delegates the authentication concerns typically taken care by Spring Security to service providers by using Spring Social.
Spring Social Facebook (version 1.1.0.RELEASE) is an extension to Spring Social and it provides Facebook integration.
Spring Social Twitter (version 1.1.0.RELEASE) is an extension to Social Social which provides Twitter integration.
Spring Social Linkedin (version 1.1.0.RELEASE) is an extension to Social Social which provides Linkedin integration.
Profile :
Profiling properties required in application.1. Build will copy properties file from the profile and place in application.properties file.
2. Add datasource connection properties.
3. Add Hibernate properties.
4. Add the Facebook application id and application secret to the properties file.
5. Add the Twitter consumer key and consumer secret to the properties file.
6. Add the Linkedin key and secret to the properties file.
In the app,we use only one profile (Dev).You can add some more profiles into app like Production,stage,testing....
# Database configuration props database.driver=com.mysql.jdbc.Driver database.url=jdbc:mysql://localhost:3306/LoginDB database.username=root database.password=pramati # hibernate props hibernate.dialect=org.hibernate.dialect.MySQL5Dialect hibernate.show.sql=true hibernate.hbm2ddl.auto=create # facebook provider details facebook.api.key=781512165315051 facebook.api.secret=376f760458f2411bb9dd9a57b2a16fd9 # twitter provider details twitter.api.key=kBcdXhCEQcTE8wz5zxliQCJX6 twitter.api.secret=6qiXptqIR1rHyWirlK1Alrk2FXiccyOk0dc2paZRUyjkJqI1IL # linkedin provider details linkedin.api.key=75ileo93c5xmr5 linkedin.api.secret=pUdw0kH9ggnj9QQE
Database Models :
USER Model:
@Entity @Table(name = "USER") public class User implements Serializable{ @Id @Column(name = "USER_ID", unique = true, nullable = false) private String userId; @Column(name = "NAME", nullable = true, length = 32) private String name; @Column(name = "PASSWORD", nullable = true, length = 64) private String password; @Column(name = "EMAIL_ID", nullable = true, length = 128) private String emailId; @Column(name = "ACTIVE", nullable = false, length = 1) private Integer active; @Column(name = "PROVIDER", nullable = false, length = 32) private String provider; @ManyToMany(fetch = FetchType.EAGER,cascade = CascadeType.ALL) @JoinTable( name = "USER_ROLE", joinColumns = @JoinColumn(name = "USER_ID"), inverseJoinColumns = @JoinColumn(name = "ROLE_ID") ) private Set<Role> roles = new HashSet<>(); //constructor,setters & getters. }
ROLE Model :
@Entity @Table(name = "ROLE") public class Role implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "ROLE_ID", unique = true, nullable = false) private Integer roleId; @Column(name = "NAME", nullable = false, length = 32) private String name; @ManyToMany(mappedBy = "roles") private Set<User> users = new HashSet<>(); //constructor,setters & getters. }
User Services :
1. LocalUserDetailService :
LocalUser Model :
public class LocalUser extends User { private String userId; public LocalUser(final String userId, final String username, final String password, final boolean enabled, final boolean accountNonExpired, final boolean credentialsNonExpired, final boolean accountNonLocked, final Collection<? extends GrantedAuthority> authorities) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); this.userId = userId; } public String getUserId() { return userId; } }
LocalUserDeatilService.java :
@Service("localUserDetailService") public class LocalUserDetailService implements UserDetailsService { @Autowired private UserDAO userDAO; @Override @Transactional public LocalUser loadUserByUsername(final String userId) throws UsernameNotFoundException { User user = userDAO.get(userId); if (user == null) { return null; } List<SimpleGrantedAuthority> simpleGrantedAuthorities = buildSimpleGrantedAuthorities(user); return new LocalUser(user.getUserId(), user.getName(), user.getPassword(), user.getActive() == 1 ? true : false, true , true, true, simpleGrantedAuthorities); } private List<SimpleGrantedAuthority> buildSimpleGrantedAuthorities(final User user) { List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>(); if (user.getRoles() != null) { for (Role role : user.getRoles()) { simpleGrantedAuthorities.add(new SimpleGrantedAuthority(role.getName())); } } return simpleGrantedAuthorities; } }
2. SocialUserDetailService
@Service("socialUserDetailService") public class SocialUserDetailService implements SocialUserDetailsService { @Autowired @Qualifier(value = "localUserDetailService") private UserDetailsService userDetailService; @Override public SocialUserDetails loadUserByUserId(final String userId) throws UsernameNotFoundException, DataAccessException { LocalUser user = (LocalUser) userDetailService.loadUserByUsername(userId); if (user == null) { throw new SocialAuthenticationException("No local user mapped with social user " + userId); } return new SocialUser(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities()); } }
3. RegistrationUserDetailService
user service is used to register new user to local database.This is used in when a new want to sign up into application or if a new social user want to register into application.public interface UserService { public UserDetails registerNewUser(UserRegistrationForm UserRegistrationForm)throws UserAlreadyExistAuthenticationException; } @Service("registrationUserDetailService") public class RegistrationUserDetailService implements UserService { @Autowired @Qualifier(value = "localUserDetailService") private UserDetailsService userDetailService; @Autowired private UserDAO userDAO; @Override @Transactional(value = "transactionManager") public LocalUser registerNewUser(final UserRegistrationForm userRegistrationForm) throws UserAlreadyExistAuthenticationException { com.spring.security.social.login.example.database.model.User userExist = userDAO.get(userRegistrationForm.getUserId()); if (userExist != null) { throw new UserAlreadyExistAuthenticationException("User with email id " + userRegistrationForm.getEmail() + " already exist"); } com.spring.security.social.login.example.database.model.User user = buildUser(userRegistrationForm); userDAO.save(user); userDAO.flush(); return (LocalUser) userDetailService.loadUserByUsername(userRegistrationForm.getUserId()); } private User buildUser(final UserRegistrationForm formDTO) { User user = new User(); user.setUserId(formDTO.getUserId()); user.setEmailId(formDTO.getEmail()); user.setName(formDTO.getFirstName() + formDTO.getLastName()); user.setPassword(formDTO.getPassword()); final HashSet<Role> roles = new HashSet<Role>(); Role role = new Role(); role.setName("ROLE_USER"); roles.add(role); user.setRoles(roles); user.setActive(1); user.setProvider(formDTO.getSocialProvider().name()); return user; } }
Configuration :
1. Persistence configuration xml :
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <tx:annotation-driven /> <!-- Ensures scans all annotations --> <context:component-scan base-package="com.spring.security.social.login.example"/> <!-- Ensures that configuration properties are read from a property file --> <context:property-placeholder location="classpath:application.properties"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix"> <value>/WEB-INF/pages/</value> </property> <property name="suffix"> <value>.jsp</value> </property> </bean> <!-- data source, sessionfactory, hiberate template & transaction manager --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${database.driver}"></property> <property name="url" value="${database.url}"></property> <property name="username" value="${database.username}"></property> <property name="password" value="${database.password}"></property> </bean> <bean id="loginSessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.spring.security.social.login.example"/> <property name="hibernateProperties"> <props> <prop key="hibernate.hbm2ddl.auto"> ${hibernate.hbm2ddl.auto} </prop> <prop key="hibernate.show_sql"> ${hibernate.show.sql} </prop> </props> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager"> <property name="sessionFactory" ref="loginSessionFactory"/> </bean> <bean id="hibernateTemplate" class="org.springframework.orm.hibernate4.HibernateTemplate"> <property name="sessionFactory" ref="loginSessionFactory"/> </bean> </beans>
2. Social login configuration :
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" 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"> <import resource="spring-persistent-servlet.xml"/> <!-- authentication manager and its provider( social provider deals with social login & local user provider deals with form login ) --> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider ref="socialAuthenticationProvider"/> <security:authentication-provider user-service-ref="localUserDetailService"/> </security:authentication-manager> <bean id="socialAuthenticationProvider" class="org.springframework.social.security.SocialAuthenticationProvider"> <constructor-arg ref="inMemoryUsersConnectionRepository"/> <constructor-arg ref="socialUserDetailService"/> </bean> <!-- form login beans --> <bean id="appAuthenticationEntryPoint" class="com.spring.security.social.login.example.entrypoint.AppAuthenticationEntryPoint"> <constructor-arg name="loginFormUrl" value="/services/login"/> </bean> <bean id="rememberMeServices" class="org.springframework.security.web.authentication.NullRememberMeServices"/> <bean id="successHandler" class="com.spring.security.social.login.example.handler.AppSuccessHandler"/> <bean id="failureHandler" class="org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler"> <constructor-arg name="defaultFailureUrl" value="/services/accessdenied"/> </bean> <bean class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter" id="SecurityAuthFilter"> <property name="authenticationManager" ref="authenticationManager"/> <property name="authenticationSuccessHandler" ref="successHandler"/> <property name="authenticationFailureHandler" ref="failureHandler"/> <property name="filterProcessesUrl" value="/j_spring_security_check"/> <property name="rememberMeServices" ref="rememberMeServices"/> </bean> <!-- Anyone can access these urls --> <security:http pattern="/services/login" security="none"/> <security:http pattern="/services/accessdenied" security="none"/> <security:http pattern="/services/signup" security="none"/> <security:http pattern="/services/user/register" security="none"/> <security:http use-expressions="true" entry-point-ref="appAuthenticationEntryPoint"> <security:intercept-url pattern="/auth/**" access="permitAll"/> <security:intercept-url pattern="/j_spring_security_check" access="permitAll"/> <security:intercept-url pattern="/" access="isAuthenticated()"/> <security:intercept-url pattern="/**" access="isAuthenticated()"/> <!-- Adds social authentication filter to the Spring Security filter chain. --> <security:custom-filter ref="socialAuthenticationFilter" before="PRE_AUTH_FILTER"/> <security:custom-filter position="FORM_LOGIN_FILTER" ref="SecurityAuthFilter"/> </security:http> <!-- social login filter which is a pre authentication filter and works for /auth service url --> <bean id="socialAuthenticationFilter" class="org.springframework.social.security.SocialAuthenticationFilter"> <constructor-arg name="authManager" ref="authenticationManager"/> <constructor-arg name="userIdSource" ref="userIdSource"/> <constructor-arg name="usersConnectionRepository" ref="inMemoryUsersConnectionRepository"/> <constructor-arg name="authServiceLocator" ref="appSocialAuthenticationServiceRegistry"/> <property name="authenticationSuccessHandler" ref="successHandler"/> </bean> <!-- inmemory connection repository which holds connection repository per local user --> <bean id="inMemoryUsersConnectionRepository" class="org.springframework.social.connect.mem.InMemoryUsersConnectionRepository"> <constructor-arg name="connectionFactoryLocator" ref="appSocialAuthenticationServiceRegistry"/> <property name="connectionSignUp" ref="connectionSignUp"/> </bean> <!-- service registry will holds connection factory of each social provider --> <bean id="appSocialAuthenticationServiceRegistry" class="com.spring.security.social.login.example.registry.AppSocialAuthenticationServiceRegistry"> <constructor-arg> <list> <ref bean="facebookAuthenticationService"/> <ref bean="twitterAuthenticationService" /> <ref bean="linkedInAuthenticationService"/> </list> </constructor-arg> </bean> <bean id="facebookAuthenticationService" class="org.springframework.social.facebook.security.FacebookAuthenticationService"> <constructor-arg name="apiKey" value="${facebook.api.key}"/> <constructor-arg name="appSecret" value="${facebook.api.secret}"/> </bean> <bean id="twitterAuthenticationService" class="org.springframework.social.twitter.security.TwitterAuthenticationService"> <constructor-arg name="apiKey" value="${twitter.api.key}"/> <constructor-arg name="appSecret" value="${twitter.api.secret}"/> </bean> <bean id="linkedInAuthenticationService" class="org.springframework.social.linkedin.security.LinkedInAuthenticationService"> <constructor-arg name="apiKey" value="${linkedin.api.key}"/> <constructor-arg name="appSecret" value="${linkedin.api.secret}"/> </bean> <bean id="userIdSource" class="org.springframework.social.security.AuthenticationNameUserIdSource"/> <!-- If no local user is associated to a social connection then connection sign up will create a new local user and map it to social user --> <bean id="connectionSignUp" class="com.spring.security.social.login.example.registry.AppConnectionSignUp"/> </beans>
Brief description of above beans and its usage
1. UsernamePasswordAuthenticationFilter : This bean is used for only form login.For more explination on this filter please go though my previous blog.
2. AppSocialAuthenticationServiceRegistry :
AppSocialAuthenticationServiceRegistry is inherited from SocialAuthenticationServiceRegistry.This bean is used to register different Social authentication services.
For each social provider a Social authentication service should exist and which defines mapping between local user with social user.
ONE_TO_ONE : For a social user one local user will be associated.
ONE_TO_MANY : Multiple social user can be mapped with single local user.
In this application we are using ONE_TO_ONE mapping between local user v/s social user.
SocialAuthenticationServiceRegistry will maintains connection repositories per provider.Connection repository handles connection persistence methods across all users; this will be a normal singleton bean in your application context.
3. UserConnectionRepository : handles connection persistence methods for one specific user; the implementation bean will be request scoped, created for logged in users of your application.
On login of new social user, a connection repository will be created for a local user.And this connection repository will have connection for each provider.
For eq : testuser is your local user.
samplesocialuser is your facebook user and sampletwitteruse is your twitter user.
then for testuser a connection repository will be created.And in connection repository facebook will have a connection for samplesocialuser and twitter will have a conection for sampletwitteruse.
In this application we are using InMemory connection respository.We can use JDBC implemention of same for business applications.
4. SocialAuthenticationFilter :
This Filter will handle spring security social login for all social providers.Should be injected into the chain at or before the PRE_AUTH_FILTER location.
Controller
@RestController public class PagesController { @RequestMapping(value = "/login", method = RequestMethod.GET) public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException { ModelAndView model = new ModelAndView(); model.addObject("title", "Login Page"); model.setViewName("login"); return model; } @RequestMapping(value = {"/userpage"}, method = RequestMethod.GET) public ModelAndView userPage() { ModelAndView model = new ModelAndView(); model.addObject("title", "Spring social security Hello World"); model.addObject("user", getUser()); model.setViewName("user"); return model; } @RequestMapping(value = {"/accessdenied"}, method = RequestMethod.GET) public ModelAndView accessDeniedPage() { ModelAndView model = new ModelAndView(); model.addObject("message", "Either username or password is incorrect."); model.setViewName("accessdenied"); return model; } private String getUser() { String userName = null; Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { userName = ((UserDetails) principal).getUsername(); } else { userName = principal.toString(); } return userName; } }
User Interfaces
login.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>${title}</title> <!-- css and script are there in attached source--> </head> <body> <div class="wrap"> <div class="login-form"> <form class="login_form" name='loginForm' action="<c:url value='../j_spring_security_check' />" method='POST'> <h1>Login Into Your Account</h1> <ul> <li> <input type="text" class="textbox1" id="username" name="j_username" placeholder="Username" required=""> <p><img src="../images/contact.png" alt=""></p> </li> <li> <input type="password" id="password" name="j_password" class="textbox2" placeholder="Password"> <p><img src="../images/lock.png" alt=""></p> </li> </ul> <input type="submit" name="Sign In" value="Sign In"> <div class="clear"></div> <label class="checkbox"><input type="checkbox" name="checkbox" checked=""><i></i>Remember me</label> <div class="forgot"> <a href="#">forgot password?</a> </div> <div class="clear"></div> </form> <c:url value="../services/signup" var="signupurl" />` <div class="account"> <h2><a href="${signupurl}">Don't have an account? Sign Up!</a></h2> <div class="span"> <form name='facebookSocialloginForm' action="<c:url value='../auth/facebook?scope=email,user_about_me,user_birthday' />" method='POST'> <img src="../images/facebook.png" alt=""> <button type="submit"> <i>Sign In with Facebook</i> </button> <div class="clear"></div> </form> </div> <div class="span1"> <form name='TwitterSocialloginForm' action="<c:url value='../auth/twitter?scope=email,user_about_me,user_birthday' />" method='POST'> <img src="../images/twitter.png" alt=""> <button type="submit"> <i>Sign In with Twitter</i> </button> <div class="clear"></div> </form> </div> <div class="span2"> <form name='LinkedInSocialloginForm' action="<c:url value='../auth/linkedin' />" method='POST'> <img src="../images/linkedin.png" alt=""> <button type="submit"> <i>Sign In with Linkedin</i> </button> <div class="clear"></div> </form> </div> </div> <div class="clear"></div> </div> </div> </body> </html>
user.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>HelloWorld page</title> </head> <body> ${title}<br/><br/> Dear ${user}, you are successfully logged into this application. <br/> <a href="<c:url value="/j_spring_security_logout" />">Logout</a> </body> </html>
accessdenied.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> <title>Access Denied Page</title> </head> <body> <h2> Access Denied </h2> <br/>${message}<br/> Click here for<a href="<c:url value="/login" />"> Login</a> </body> </html>