⬆️ ⬇️

Authorization on the site through the social networking API with integration into Spring Security

I decided to implement authorization (registration) and user identification on the portal being developed using the Social Networks REST API (Social Networks Developer Tool) - the topic is far from being innovative, it is actively used and very convenient to use. I would not like to list all the conveniences and advantages of using such functionality on my websites, but I note that I am very happy not to remember the passwords for each website (even if I have a couple of standard ones used), not to participate in tedious registrations with email forwarding and confirmations, as well as once again not to encounter captcha.



The functionality of the API data is quite primitive, the technology is simple, and the implementation is quite the same type and simple. But when you get acquainted with the technology, the documentation and API examples of this or that social network are not enough. In addition, as written in the topic, the language used is Java, which automatically reduces the amount of useful information. And there are not so many descriptions in runet. You can take the path of least resistance and use third-party RESTful products, but a) it does not provide a complete understanding of the process; b) reduces the switching properties of the required process; c) often, the study of a third-party product may be more difficult to develop its implementation. Although the convenience of using such a third-party product can greatly facilitate development. However, I personally in this review put emphasis on maximally controlling all processes, even to the detriment of universality (we “screw” specific functionality to a specific site, and only a few make this a universal product “for all occasions”). In addition, I am interested not only in implementing user authorization, but also in implementing the project security system provided by the Spring Security 3 framework.



Used set of platforms and tools: Spring Core 3.1 , Spring MVC 3.1 , Spring Security 3.1 , Hibernate 4.1 . The implementation project is foreign, so the set of social networks being implemented is standard “for them” - Facebook , Twitter , Google+ , LinkedIn .



I want to note that in the Spring package there is a ready project out of the box - Spring Social (today release 1.0.2), which is wonderfully encapsulated in the Spring framework of the product and intended for use with other Spring products. Surely this would be a professional decision, but our task is to control everything and make the process as transparent as possible for understanding. Not so smooth with Social itself.

')

1. Model.



I went in a somewhat risky and controversial way, combining in a user object both POJO , UserDetails , and Entity . From the point of view of programming, this is wrong, but a) is very convenient; and b) it saves the creation of several layers, saving us from doing separate POJO + Entity, separate UserDetails and separate DTO, which actually duplicates the content.



The proposed model building scheme is as follows:

image

I singled out two layers (AuthUser and DataUser) in order not to interfere with each other the authorization logic with the business logic of the project: the visitor, and the administrator, and whoever is - authorize in the same way, but have their own set of properties. For example, I have Jobseekers and Employers in the project, they come to the site in the same way, but have a completely different model structure.



As for the separation of the structure inside the layers, it is obvious - the set of received fields from Facebook, Twitter, etc., and especially with standard authorization, is so different that creating one terribly stretched structure for everything is just silly and from the point of view of building a database - redundantly. As for scalability, when adding a new service provider, working with such a structure would be extremely inconvenient.



Listings of some of the listed objects, as well as used enum classes.



AuthUser.java:


@ Entity @ Table(name = "auth_user") @ Inheritance(strategy = InheritanceType.JOINED) public class AuthUser implements Serializable, UserDetails { @ Id @ Column(name = "id") @ GeneratedValue(strategy = GenerationType.AUTO) private Long id; @ Column(name = "identification_name", length = 64, nullable = false) private String identificationName; @ Enumerated(EnumType.STRING) @ Column(name = "type", nullable = false) private AuthorityType type; @ Column(name = "binary_authorities", nullable = false) private Long binaryAuthorities; @ Column(name = "enabled", nullable = false, columnDefinition = "tinyint") private Boolean enabled; @ Transient private Set<Authority> authorities; @ OneToOne(fetch = FetchType.LAZY, orphanRemoval = true) @ Cascade({CascadeType.ALL}) @ JoinColumn(name="user_id") private User user; @ Override public Collection<? extends GrantedAuthority> getAuthorities() { authorities = EnumSet.noneOf(Authority.class); for (Authority authority : Authority.values()) if ((binaryAuthorities & (1 << authority.ordinal())) != 0) authorities.add(authority); return authorities; } public void setAuthority(Set<Authority> authorities) { binaryAuthorities = 0L; for (Authority authority : authorities) binaryAuthorities |= 1 << authority.ordinal(); } @ Override public String getPassword() { return type.name(); } @ Override public String getUsername() { return identificationName; } @ Override public boolean isAccountNonExpired() { return true; } @ Override public boolean isAccountNonLocked() { return true; } @ Override public boolean isCredentialsNonExpired() { return true; } //getters/setters } 


AuthorityType.java:


 public enum AuthorityType implements Serializable { SIMPLE, FACEBOOK, TWITTER, GOOGLE, LINKEDIN; } 


Authority.java:


 public enum Authority implements GrantedAuthority { NEW_CUSTOMER, CUSTOMER, ADMINISTRATOR; @ Override public String getAuthority() { return toString(); } } 


FacebookAuthUser.java:


 @ Entity @ Table(name = "facebook_auth_user") public class FacebookAuthUser extends AuthUser { @ Column(name = "first_name", length = 32) private String firstName; @ Column(name = "last_name", length = 32) private String lastName; @ Column(name = "email", length = 64) private String email; @ Column(name = "token", length = 128) private String token; //any number of available properties //getters/setters } 


TwitterAuthUser.java:


 @ Entity @ Table(name = "twitter_auth_user") public class TwitterAuthUser extends AuthUser { @ Column(name = "screen_name", length = 64) private String screenName; @ Column(name = "oauth_token", length = 80) private String oauthToken; @ Column(name = "oauth_token_secret", length = 80) private String oauthTokenSecret; //any number of available properties //getters/setters } 


SimpleAuthUser.java:


 @ Entity @ Table(name = "simple_auth_user") public class SimpleAuthUser extends AuthUser { @ Column(name = "password", length = 40, nullable = false) private String password; @ Column(name = "uuid", length = 36, nullable = false) private String uuid; @ Override public String getPassword() { return password; } //getters/setters } 


As you can see, the slightest “chemistry” has not done:



As can be seen in the diagram and in the code, I decided to use a lazy dependency between objects of different layers, even though they correlate as “one-to-one”. There are two goals: 1) AuthUser is often twitched by the framework in the controller and view, and there is little desire to drag the dependent structure everywhere, especially since it can be very extensive and massive (in my project, the Jobseeker tables only have EAGER dependencies of pieces 5- 6, not counting LAZY - these are telephones, and the address, and professions, and others), therefore, in my opinion, reinsurance will not hurt. 2) we must not forget that these layers belong to different layers of logic: AuthUser is twitching with the Spring Security framework, and at the same time changes can occur in DataUser, but I don’t want to be engaged in constant monitoring and updating. I agree that this decision is controversial, and does not pretend to be final. Perhaps, it is necessary to connect vice versa, thus the listed problems disappear, and it will always be possible to pull an authorization bin from business logic. This remains at the discretion of the developers.



As for the DataUser class and the dependent ones, these are simple POJO classes, the DataUser itself contains properties common to all (say, id, firstName, lastName, email, location), and the rest are expanded by adding specific properties (listing impractical). .



2. The controller.



In principle, in the terminology of authentication and authorization there is not much difference - authorization is authorization, and, different network providers are inclined to these terms in their own way. However, in my report I clearly distinguish 2 concepts - registration and directly authorization or input (both are based on authorization from a social network provider). Say, when participating in a forum, or submitting a comment, you just need to log in - be it in the first entry, or in the 100th. I am pursuing the separation of registration and simple authorization by the need to create a user model when submitting an application for registration. And although it could be implemented more simply - at the entrance we check if there is such a person or not - and create a user structure in the case of the first entrance. But a) there is a standard registration and it is logical to make a visual separation “there is one, and here there is another” (the notorious usability ); b) no matter how annoying, but the social networking APIs are not unanimous in providing information about their customers - say, the Facebook API provides email, first name, last name, gender, location; Twitter API - gives a screen_name, which may not be the "first name", does not give email (their position clearly distinguishes between real and virtual); The Google+ API provides a first name, last name, email, but nothing about the location; LinkedIn API - name, surname, gender, location, but does not give email. Since my project is very closely tied to the personal data of the visitor (a project for a recruitment company), along with the registration I point out the need to fill in some fields (initially, except for Facebook users, everyone had to specify at least something, now it’s simplified and only Twitter users, although I do not exclude a complete refusal, and filling in the fields when there is a need - for example, when trying to get into the "zone", where such information will already be simply necessary). Therefore, my implementation is somewhat bloated, although it only helps to understand the authorization mechanism more.



I want to remind you that for work (or testing) you need to create your own application in each of the social networks, and use its settings for work. For Facebook, it's developers.facebook.com/apps , for Twitter — dev.twitter.com/apps , for Google+ — code.google.com/apis/console , for LinkedIn — www.linkedin.com/secure/developer . When creating an application, there are 3 important parameters that each provider has - a key (or API key, Consumer key, Client ID), a secret key (App Secret, Consumer secret, Client secret, Secret key) and a redirect address (they say, until recently time, some of the providers did not work redirect to localhost, but today it’s checked - everyone works with an address like http: // localhost: 8080 / myproject ). There you can also configure other settings, including the application logo, however, LinkedIn requires that the link to the picture be SSL (an incomprehensible wish).



Facebook and Google+ have long been using the new OAuth 2 protocol, Twitter and LinkedIn still remain on the older OAuth protocol (Google+ also supported the first version of OAuth until April 20, 2012). At my discretion (although I can’t imagine a different opinion), working with OAuth 2 is immeasurably easier and more convenient, though, despite the fact that it is quite popular, it is still not approved as a standard. The principle of operation is quite primitive (the most popular scheme):

image

So, a user on a web page clicks on one of the registration buttons:

image

(moreover, I don’t leave any “extra” functionality on the page, only a button with an address like www.myproject.com/registration/facebook , etc.), the request goes to the controller (for Facebook):

 @ RequestMapping(value = "/registrate/facebook", method = RequestMethod.POST) public ModelAndView facebookRegistration() throws Exception { return new ModelAndView(new RedirectView(FACEBOOK_URL + "?client_id=" + FACEBOOK_API_KEY + + "&redirect_uri=" + FACEBOOK_URL_CALLBACK_REGISTRATION + + "&scope=email,user_location&state=registration", true, true, true)); } 


The parameters for the scope can be found at developers.facebook.com/docs/authentication/permissions (for Twitter - dev.twitter.com/docs/platform-objects/users , Google+ - developers.google.com/accounts/docs/OAuth2Login# userinfocall , LinkedIn - developer.linkedin.com/documents/profile-fields ), I just brought a couple here. Domain redirect_uri must match the registered address of the application. state is a “free” parameter, I use it as a semaphore for further actions - registration, signin, autosignin.



Then the user will “redirect” to the Facebook authorization home page, where he will allow the application to use his data, and, if the permissions are beyond the base ones, they will be listed in the authorization window.



After authorization, our controller with mapping FACEBOOK_IRL_CALLBACK_REGISTRATION receives a call (for any decision of the client - authorize, cancel, return). Spring MVC allows us to immediately filter requests by mepping (in this case, the mapping of my project is shown):

 @ RequestMapping(value = "/callback/facebook", method = RequestMethod.GET) public class FacebookController extends ExternalController implements Constants { @ RequestMapping(value = "/registration", params = "code") public ModelAndView registrationAccessCode(@ RequestParam("code") String code, HttpServletRequest request) throws Exception { String authRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ACCESS_TOKEN, new String[]{"client_id", "redirect_uri", "client_secret", "code"}, new String[]{FACEBOOK_API_KEY, FACEBOOK_URL_CALLBACK_REGISTRATION, FACEBOOK_API_SECRET, code}); String token = Utils.parseURLQuery(authRequest).get("access_token"); String tokenRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ME, new String[]{"access_token"}, new String[]{token}) Map<String, Json> userInfoResponse = Json.read(tokenRequest).asJsonMap(); String email = userInfoResponse.get("email").asString().toLowerCase(); String id = userInfoResponse.get("id").asString(); //verifying ... is new? is email in DB? //creating objects Customer customer = new Customer(); customer.setEmail(email); //... customerer = (Customerer) userDAO.put(customer); FacebookAuthUser user = new FacebookAuthUser(); user.setFirstName(firstName); //... user.setIdentificationName(id); user.setToken(token); user.setType(AuthenticationType.FACEBOOK); user.setEnabled(true); user.setAuthority(EnumSet.of(Authority.CUSTOMER)); user.setUser(customer); authenticationDAO.put(user); return new ModelAndView(new RedirectView("/registrate.complete", true, true, false)); } @ RequestMapping(value = "/registration", params = "error_reason") public ModelAndView registrationError(@ RequestParam("error_description") String errorDescription, HttpServletRequest request, HttpServletResponse response) { //return client to registration page with errorDescription return new ModelAndView(new RedirectView("/registrate", true, true, false)); } //will signin and signinError } 


For convenience and unitary use of a pair of static methods of the Utils class used in this listing:

 public static String sendHttpRequest(String methodName, String url, String[] names, String[] values) throws HttpException, IOException { if (names.length != values.length) return null; if (!methodName.equalsIgnoreCase("GET") && !methodName.equalsIgnoreCase("POST")) return null; HttpMethod method; if (methodName.equalsIgnoreCase("GET")) { String[] parameters = new String[names.length]; for (int i = 0; i < names.length; i++) parameters[i] = names[i] + "=" + values[i]; method = new GetMethod(url + "?" + StringUtils.join(parameters, "&")); } else { method = new PostMethod(url); for (int i = 0; i < names.length; i++) ((PostMethod) method).addParameter(names[i], values[i]); method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); } new HttpClient().executeMethod(method); return getStringFromStream(method.getResponseBodyAsStream()); } public static Map<String, String> parseURLQuery(String query) { Map<String, String> result = new HashMap<String,String>(); String params[] = query.split("&"); for (String param : params) { String temp[] = param.split("="); try { result.put(temp[0], URLDecoder.decode(temp[1], "UTF-8")); } catch (UnsupportedEncodingException exception) { exception.printStackTrace(); } } return result; } 


Constants:

 final public static String FACEBOOK_API_KEY = "XXXXXXXXXXXXXXXX"; final public static String FACEBOOK_API_SECRET = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"; final public static String FACEBOOK_URL = "https://www.facebook.com/dialog/oauth"; final public static String FACEBOOK_URL_ACCESS_TOKEN = "https://graph.facebook.com/oauth/access_token"; final public static String FACEBOOK_URL_ME = "https://graph.facebook.com/me"; final public static String FACEBOOK_URL_CALLBACK_REGISTRATION = SITE_ADDRESS + "/callback/facebook/registration"; final public static String FACEBOOK_URL_CALLBACK_SIGNIN = SITE_ADDRESS + "/callback/facebook/signin"; 


JSON libraries can be used by everyone at their discretion, I used the mjson library ( http://sharegov.blogspot.com/2011/06/json-library.html ) - small, convenient and without serialization.



As you can see, the process is simple and should not cause any special questions. I also want to note that Facebook provides the location parameter, values ​​from which you can “slip” the Google Maps API (at http://maps.googleapis.com/maps/api/geocode/json ) and pull out geolocation in a convenient form (by standards Google Maps). Clearly, it can only be a case if the client in his Facebook account has indicated not only the country of the location.



Signing up on Google+ is similar, with the only difference being that the callback URL in their system must exactly match the one specified in the application settings. Thus, all redirects will only get on one mapping. To separate processes, it is convenient to use the returned state parameter:

 @ RequestMapping(value = "/callback/google", method = RequestMethod.GET) public class GoogleController extends ExternalController implements Constants { @ RequestMapping(value = {"/", ""}, params = "code") public ModelAndView googleProxy(@ RequestParam("code") String code, @ RequestParam("state") String state, HttpServletRequest request, HttpServletResponse response) throws Exception { ... } @ RequestMapping(value = {"/", ""}, params = "error") public ModelAndView googleErrorProxy(@RequestParam("error") String error, @RequestParam("state") String state, HttpServletRequest request) throws Exception { ... } } 


The remaining actions, with the exception of addresses and return parameters, are identical.



The situation is different with OAuth authorization (Twitter and LinkedIn). I went through the whole authorization chain, but this is very inconvenient due to the formation of a request with tokens - they need to be specially “glued” together to package base64, add parameters with time and other manipulations. And what is most surprising is that the sections for developers of these social networks do not reflect these processes. Although this is a standard, so the calculation goes to the standard approach. In any case, authorization in this way, implemented "manually", is of no interest for developing your application. I advise you to use third-party free libraries that facilitate this task. For example, there is a library specifically for Twitter - twitter4j.jar . I used the scribe-java library ( http://github.com/fernandezpablo85/scribe-java ), which is distributed under the MIT license . The package has work with Digg API , Facebook API , Flickr API , Freelancer API , Google API , LinkedIn API , Skyrock API , Tumblr API , Twitter API , Vkontakte API , Yahoo API and a dozen more 2 others.



The registration process for Twitter using the scribe library will look like this. Client request authorization controller from the registration page:

 @ RequestMapping(value = "/registrate/twitter", params = "action", method = RequestMethod.POST) public ModelAndView twitterRegistrationJobseeker(HttpServletRequest request) throws Exception { OAuthService service = new ServiceBuilder().provider(TwitterApi.class) .apiKey(TWITTER_CONSUMER_KEY).apiSecret(TWITTER_CONSUMER_SECRET) .callback(TWITTER_URL_CALLBACK_REGISTRATION).build(); Token requestToken = service.getRequestToken(); request.getSession().setAttribute("twitter", service); request.getSession().setAttribute("request_token", requestToken); return new ModelAndView(new RedirectView(service.getAuthorizationUrl(requestToken), true, true, true)); } 


Twitter callback controller:

 @ RequestMapping(value = "/callback/twitter", method = RequestMethod.GET) public class TwitterController extends ExternalController implements Constants { @ RequestMapping(value = "/registration", params = "oauth_verifier") public ModelAndView registrationAccessCode(@ RequestParam("oauth_verifier") String verifier, HttpServletRequest request, HttpServletResponse response) throws Exception { OAuthService service = (OAuthService) request.getSession().getAttribute("twitter"); Token accessToken = service.getAccessToken((Token) request.getSession().getAttribute("request_token"), new Verifier(verifier)); OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, TWITTER_URL_CREDENTIALS); service.signRequest(accessToken, oauthRequest); Map<String, Json> userInfoResponse = Json.read(oauthRequest.send().getBody()).asJsonMap(); String twitterId = userInfoResponse.get("id").asString(); //verifying ... Customer customer = new Customer(); customer.setFirstName((String) request.getSession().getAttribute("pageValueFirstName")); //... customer = (Customer) userDAO.put(customer); TwitterAuthUser user = new TwitterAuthUser(); user.setAuthority(EnumSet.of(Authority.CUSTOMER)); user.setIdentificationName(twitterId); //... user.setOauthToken(accessToken.getToken()); user.setOauthTokenSecret(accessToken.getSecret()); user.setType(AuthenticationType.TWITTER); user.setUser(customer); authenticationDAO.put(user); return new ModelAndView(new RedirectView("/registrate.complete", true, true, false)); } @ RequestMapping(value = "/registration", params = "denied") public ModelAndView registrationError(HttpServletRequest request) { //response does not contain the error text return new ModelAndView(new RedirectView("/registrate", true, true, false)); } //will signin and signinError } 


Again, everything is very simple and affordable. Registration through the LinkedIn API is done in exactly the same way.



The last is registration in the standard way. Standard - that’s standard, I’m not going to give the code, I’ll only clarify that as a result we create a SimpleAuthUser object inherited from AuthUser:

  SimpleAuthUser user = new SimpleAuthUser(); user.setAuthority(EnumSet.of(Authority.NEW_CUSTOMER)); user.setEnabled(false); user.setIdentificationName(email); user.setPassword(passwordEncoder.encodePassword(password, email)); user.setType(AuthenticationType.SIMPLE); user.setUser(customer); user.setUuid(uuid); authenticationDAO.put(user); 


It is for this case that authority NEW_CUSTOMER was needed - a registered user needs confirmation of registration (standard practice), and therefore a) has a different role; b) Spring Security is not allowed to authorize (enabled = false).



Authorization on the site



Simple springovsky application-context-security.xml :

 <security:global-method-security secured-annotations="enabled" jsr250-annotations="enabled" pre-post-annotations="enabled" proxy-target-class="true"/> <security:http auto-config="true" use-expressions="true"> <security:intercept-url pattern="/**" access="permitAll"/> <security:form-login login-page="/signin"/> <security:logout invalidate-session="true" logout-success-url="/" logout-url="/signout"/> <security:remember-me services-ref="rememberMeService" key="someRememberMeKey"/> </security:http> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider ref="authenticationProvider"/> </security:authentication-manager> <bean id="authenticationProvider" class="myproject.security.CustomAuthenticationProvider"/> <bean id="rememberMeService" class="myproject.security.RememberMeService"> <property name="key" value="someRememberMeKey"/> <property name="userDetailsService" ref="userDetailsService"/> </bean> <bean id="userDetailsService" class="myproject.security.CustomUserDetailsManager"/> <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"/> 


CustomUserDetailsManager.java:


 public class CustomUserDetailsManager implements UserDetailsService { @ Resource private AuthenticationDAO authenticationDAO; @ Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { return authenticationDAO.findAuthUser(username); } } 


CustomUserAuthentication.java:


 public class CustomUserAuthentication implements Authentication { private String name; private Object details; private UserDetails user; private boolean authenticated; private Collection<? extends GrantedAuthority> authorities; public CustomUserAuthentication(UserDetails user, Object details) { this.name = user.getUsername(); this.details = details; this.user = user; this.authorities = user.getAuthorities(); authenticated = true; } @ Override public String getName() { return name; } @ Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @ Override public Object getCredentials() { return user.getPassword(); } @ Override public Object getDetails() { return details; } @ Override public Object getPrincipal() { return user; } @ Override public boolean isAuthenticated() { return authenticated; } @ Override public void setAuthenticated(boolean authenticated) throws IllegalArgumentException { this.authenticated = authenticated; } } 


CustomAuthenticationProvider.java
(the class is completely stupid, but Spring Security needs to feed the heir of the AuthenticationProvider interface, but the closest one within the meaning of PreAuthenticatedAuthenticationProvider is not suitable):

 public class CustomAuthenticationProvider implements AuthenticationProvider { @ Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { //   .  return authentication; } @ Override public boolean supports(Class<?> authentication) { return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication); } public Authentication trust(UserDetails user) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication trustedAuthentication = new CustomUserAuthentication(user, authentication.getDetails()); authentication = authenticate(trustedAuthentication); SecurityContextHolder.getContext().setAuthentication(authentication); return authentication; } } 


And, perhaps, the “bottleneck” of the organization of security is the implementation of the Remember Me mechanism. In principle, everything is already organized in such a way that the work of the RememberMe service fully complies with the implementation of TokenBasedRememberMeServices , with one clarification: all the data for automatic client authorization are in our database, however there may be a situation when a user has registered on the site and then deleted the account on used social network. – , , . RememberMe . API , «» RememberMe, . , - ( AbstractRememberMeServices final ), . , , . AbstractRememberMeServices TokenBasedRememberMeServices , public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) – , «» :

 Class<? extends ExternalController> controller = externalControllers.get(user.getPassword()); if (controller != null && !controller.newInstance().checkAccount(user)) return null; 


, :

 private Map<String, Class<? extends ExternalController>> externalControllers; public CustomRememberMeService() { externalControllers = new HashMap<String, Class<? extends ExternalController>>(){{ put(AuthenticationType.FACEBOOK.name(), FacebookController.class); put(AuthenticationType.TWITTER.name(), TwitterController.class); put(AuthenticationType.GOOGLE.name(), GoogleController.class); put(AuthenticationType.LINKEDIN.name(), LinkedinController.class); }}; } 


( RememberMe ).



REMEMBER_ME_FILTER , autoLogin, . , , . – , , , «».



ExternalController checkAccount(user) . callback- ExternalController :

 public abstract class ExternalController { public abstract boolean checkAccount(UserDetails user) throws Exception; } 


. , Facebook :

 public boolean heckAccount(UserDetails user) throws Exception { FacebookAuthUser facebookUser = (FacebookAuthUser) user; String authRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ME, new String[]{"access_token"}, new String[]{facebookUser.getToken()}); Map<String, Json> tokenInfoResponse = Json.read(authRequest).asJsonMap(); return tokenInfoResponse.get("error") == null && tokenInfoResponse.get("id").asString().equalsIgnoreCase(facebookUser.getIdentificationName()); } 


Twitter:

 public boolean checkAccount(UserDetails user) throws Exception { TwitterAuthUser twitterUser = (TwitterAuthUser) user; OAuthService service = new ServiceBuilder().provider(TwitterApi.class).apiKey(TWITTER_CONSUMER_KEY).apiSecret(TWITTER_CONSUMER_SECRET).build(); OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, TWITTER_URL_CREDENTIALS); service.signRequest(new Token(twitterUser.getOauthToken(), twitterUser.getOauthTokenSecret()), oauthRequest); String response = oauthRequest.send().getBody(); Map<String, Json> info = Json.read(request).asJsonMap(); return info.get("id").asString().equalsIgnoreCase(twitterUser.getIdentificationName()); } 


etc.



(login, sign in) . , «» :

image



, – «signin» «autosignin», , « ». , , callback URL scope permission- we need to get only the client ID and its tokens. After appropriate checks in the controller's methods, I advise you to overwrite the tokens in the database. And although, for example, Facebook during my tests did not change the client token, and Google+ does it every time. I don’t know how often a “change” happens, so I rewrite after each access_token receipt (in fact, with each non-automatic authorization from the provider).



And the most important point is the immediate authorization of the user in Spring Security (after checking, of course, for compliance and obtaining rights from the provider API), using the example of the Facebook controller:

 @ RequestMapping(value = "/signin", params = "code") public ModelAndView signInAccessCode(@ RequestParam("code") String code, @ RequestParam("state") String state, HttpServletRequest request, HttpServletResponse response) throws Exception { String accessRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ACCESS_TOKEN, new String[]{"client_id", "redirect_uri", "client_secret", "code"}, new String[]{FACEBOOK_API_KEY, FACEBOOK_URL_CALLBACK_SIGNIN, FACEBOOK_API_SECRET, code}); String token = Utils.parseURLQuery(accessRequest).get("access_token"); Map<String, Json> userInfoResponse = Json.read(Utils.sendHttpRequest("GET", FACEBOOK_URL_ME, new String[]{"access_token"}, new String[]{token})).asJsonMap(); FacebookAuthUser user = (FacebookAuthUser) authenticationDAO.findAuthUser(userInfoResponse.get("id").asString(), AuthenticationType.FACEBOOK); if (user == null) { //-    ... return new ModelAndView(new RedirectView("/signin", true, true, false)); } else { if (!token.equals(user.getToken())) { user.setToken(token); user = (FacebookAuthUser) authenticationDAO.put(user); } Authentication authentication = customAuthenticationProvider.trust(user); if (state.equalsIgnoreCase("autosignin")) customRememberMeService.onLoginSuccess(request, response, authentication); else customRememberMeService.logout(request, response, authentication); //  RememberMe return new ModelAndView(new RedirectView("/signin.complete", true, true, false)); } } 


. , , logout RememberMe ( ). , "/logout" . Spring Security .



«» : ( , ..) :

 Authentication authentication = customAuthenticationProvider.trust(user); if (autosignin) customRememberMeService.onLoginSuccess(request, response, authentication); else customRememberMeService.logout(request, response, authentication); 


. , , – , RememberMe , , TokenBasedRememberMeServices.



Spring Security, , @Secured(«CUSTOM_ROLE») , ( , ). Spring Security – @PreAuthorize , @PostFilter : @PreAuthorize(«hasRole('ADMINISTRATOR')»), @PreAuthorize(«hasRole({'CUSTOMER', 'ADMINISTRATOR'})»). You only need to specify this in the security: global-method-security parameter in the Spring Security config .



Similarly, you can take advantage of the benefits and capabilities of Spring Security in a view (in JSP ). For example:

 <%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> ... <sec:authorize access="isAuthenticated()"> <div id="userBox"> <span>Welcome, <sec:authentication property="principal.user.firstName"/>!</span> </div> </sec:authorize> 


, ( ).



jsp- jsp- ( , - « – , — », , / – ; , , – , , , ):

 <%@ page import="org.springframework.security.core.context.SecurityContextHolder" %> <%@ page import="myproject.auth.AuthUser" %> <% Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); AuthUser authUser = null; if (!(principal instanceof String)) authUser = (AuthUser) principal; %> ... <input type="text" <%= authUser == null ? "" : "disabled=\"disabled\"" %> name="your_name" value="<%= authUser == null ? "" : authUser.getUser().getFirstName()%>"/> ... <% if (authUser == null) { %> <div id="recaptcha_widget"> <div id="recaptchaContainer"> ... </div> </div> <% } %> 


The given page code only reflects the possibility of using the context of security, but does not pretend to any meaningfulness of the page logic.



I want to focus on the “bottleneck” caused by a lazy dependency between the authentication object and its parameters (the principal object): without any modifications, both pages of the page code will cause a Runtime Exception when opened, since the user field (the getUser () method call) will contain default object with all fields filled in as null. Using the OpenSessionInView pattern , HTTP . , , - – , EAGER-. authenticationDAO sessionFactory.getCurrentSession() : SessionFactoryUtils.openSession(sessionFactory) . , , . , , OpenSessionInView , .



, , , .

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



All Articles