Spring Securityに統合されたソーシャルネットワークAPIを介したサイトでの承認

ソーシャルネットワークREST APIツールを使用して、開発されたポータルに承認(登録)とユーザー識別を実装することにしました。このトピックは革新的ではなく、積極的に使用され、非常に便利です。 自分のサイトでそのような機能を使用することの便利さと利点をすべてリストするつもりはありませんが、メール転送による退屈な登録に参加することなく、各サイトのパスワードを覚えていないことは非常にうれしいことに気づくでしょう確認、および再びキャプチャに遭遇しないようにします。

APIデータの機能は非常に原始的であり、テクノロジーはシンプルであり、実装はまったく同じでシンプルです。 しかし、この技術に慣れると、特定のソーシャルネットワークのドキュメントとAPIの例では不十分です。 さらに、トピックで述べられているように、使用される言語はJavaであり、有用な情報の量が自動的に削減されます。 また、RuNetの説明にはそれほど多くありません。 最も抵抗の少ない方法でサードパーティのRESTful製品を使用できますが、a)プロセスを完全に理解することはできません。 b)必要なプロセスの切り替え特性を減らします。 c)多くの場合、サードパーティ製品の調査は、その実装を開発するよりも難しい場合があります。 このようなサードパーティ製品の使いやすさは、開発を大幅に促進できますが。 ただし、このレビューでは、普遍性を損なうことも含めて、すべてのプロセスを最大限に制御することに個人的に重点を置いています(特定のサイトに特定の機能を「固定」し、これを「あらゆる場面で」普遍的な製品にします)。 さらに、ユーザー認証の実装だけでなく、Spring Security 3フレームワークが提供するプロジェクトセキュリティシステムの実装にも興味があります。

プラットフォームとツールの使用セット: Spring Core 3.1Spring MVC 3.1Spring Security 3.1Hibernate 4.1 実装プロジェクトは外国のものであるため、実装されたソーシャルネットワークのセットは、 FacebookTwitterGoogle +LinkedInなどの「彼らにとって」標準です。

Springパッケージには、「すぐに使える」既成プロジェクトがあります。SpringSocial(今日のリリース1.0.2)は、製品のSpringフレームワークに著しくカプセル化され、他のSpring製品で使用するために設計されています。 確かにこれはプロフェッショナルなソリューションになりますが、私たちのタスクは、すべてを制御し、理解できるようにプロセスを可能な限り透明にすることです。 また、ソーシャル自体では、すべてがそれほどスムーズではありません。

1.モデル。


POJOUserDetails 、およびEntityをユーザーオブジェクトに組み合わせて、多少危険で矛盾したパスを取りました。 プログラミング手法の観点からはこれは間違っていますが、a)非常に便利です。 b)複数のレイヤーの作成を保存し、POJO + Entity、UserDetails、DTOを個別に実行して、コンテンツを実際に複製します。

提案されているモデル構築スキームは次のとおりです。
画像
プロジェクトの承認ロジックとビジネスロジックに干渉しないように2つのレイヤー(AuthUserとDataUser)を選択しました:訪問者、管理者、および他の同じ方法でログインしますが、独自のプロパティセットがあります。 たとえば、私のプロジェクトには求職者と雇用主がいます。彼らは同じ方法でサイトにアクセスしますが、モデル構造はまったく異なります。

レイヤー内の構造の分離に関しては、明らかです-Facebook、Twitterなどから受信したフィールドのセットは、特に標準の承認では、非常に異なっているため、すべてに対して1つのひどく引き伸ばされた構造を作成することは、データベース構築の観点からはばかげています-過度に。 スケーラビリティに関しては、新しいサービスプロバイダーを追加する場合、そのような構造で作業することは非常に不便です。

リストされたオブジェクトのいくつかのリスト、および使用されている列挙クラス。

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 } 

ご覧のとおり、小さな「化学」がないわけではありません。

図とコードからわかるように、1対1に関連しているにもかかわらず、異なるレイヤーのオブジェクト間で遅延関係を使用することにしました。 2つの目標があります:1)AuthUserは多くの場合、コントローラーとビューのフレームワークをジャークし、依存構造をその背後のどこにでもドラッグしたいという欲求はありません。 6、LAZYをカウントしません-これらは電話、住所、職業、その他の両方です)、したがって、私の意見では、再保険は傷つきません。 2)これらのレイヤーが異なるロジックレイヤーに属していることを忘れてはなりません:AuthUserはSpring Securityフレームワークと連携しており、同時にDataUserの変更が発生する可能性がありますが、常に監視および更新したくありません。 この決定は議論の余地があり、最終的なふりをしていないことに同意します。 おそらく他の方法で接続する必要があり、それによって問題がリストされたままになり、いつでもビジネスロジックから認証Beanをプルできます。 これは、開発者の裁量によります。

DataUserクラスと依存クラスについては、これらは単純なPOJOクラスです。DataUserにはすべてに共通のプロパティ(id、firstName、lastName、email、locationなど)が直接含まれ、残りはそれ自体に固有のプロパティを追加することで拡張されます(リストは非実用的です) 。

2.コントローラー。


原則として、 認証承認の用語ではそれほど違いはありません。承認は承認であり、さらに、異なるネットワークプロバイダーはこれらの用語を独自の方法で傾斜させます。 しかし、私のレポートでは、登録と直接認証またはログイン(両方ともソーシャルネットワークプロバイダーからの認証に基づいています)という2つの概念を明確に区別しています。 フォーラムに参加するとき、またはコメントを送信するときは、ログインするだけです(最初のエントリであろうと100分の1であろうと)。 登録申請時にユーザーモデルを作成する必要があるため、登録と単純な承認の分離を追求しています。 そして、これは簡単に実装できますが、入り口でそのような人がいるかどうかを確認し、最初のログインの場合にユーザー構造を作成します。 しかし、a)標準的な登録があり、「ここに1つ、ここにもう1つ」(悪名高いユーザビリティ )を視覚的に分離することは論理的です。 b)どんなにno辱的であっても、ソーシャルネットワーキングAPIは顧客に関する情報を提供することに全会一致ではありません。たとえば、 Facebook APIは電子メール、名、姓、性別、場所を提供します。 Twitter API -screen_nameを提供します。これは、「名、姓」ではない場合がありますが、電子メールは提供しません(実際と仮想を明確に区別する立場があります)。 Google+ APIは名、姓、メールを提供しますが、場所については何も提供しません。 LinkedIn API-名前、姓、性別、場所。ただし、メールは送信しません。 私のプロジェクトは訪問者の個人データ(採用企業のプロジェクト)と非常に密接に結びついているため、登録とともに、いくつかのフィールドに記入する必要があることを示します(最初は、Facebookユーザーを除いて、誰もが少なくとも何かを指定する必要がありました。 Twitterユーザーは、完全な拒否を除外するわけではありませんが、必要に応じてフィールドに入力します。たとえば、そのような情報がすでに必要な「ゾーン」に移動しようとする場合など) したがって、承認メカニズムをさらに理解するのに役立つだけですが、私の実装はやや膨らみます。

仕事(またはテスト)では、各ソーシャルネットワークで独自のアプリケーションを作成し、その設定を使用して作業する必要があることを思い出してください。 Facebookの場合、これはdeveloper.facebook.com/apps、Twitterの場合-dev.twitter.com/apps、Google+の場合-code.google.com/apis/console、LinkedInの場合-www.linkedin.com/secure/developerです。 アプリケーションを作成するとき、各プロバイダーが持つ3つのパラメーターが重要です:キー(またはAPIキー、コンシューマーキー、クライアントID)、シークレットキー(アプリシークレット、コンシューマーシークレット、クライアントシークレット、シークレットキー)およびリダイレクトアドレス(最近まで)一部のプロバイダーではlocalhostへのリダイレクトが機能していませんでしたが、今日はhttp:// localhost:8080 / myprojectのようなアドレスで全員が機能することを確認しています 。 また、アプリケーションのロゴなど、他のパラメーターを構成することもできますが、LinkedInでは、イメージへのリンクがSSLであることが必要です(理解できない願い)。

FacebookとGoogle+は長い間新しいOAuth 2プロトコルを使用しており、TwitterとLinkedInは引き続き古いOAuthプロトコルを使用しています(Google+は2012年4月20日までOAuthの最初のバージョンもサポートしていました)。 私の裁量で(別の意見があったとは想像できませんが)、OAuth 2を使用した作業は比類なくシンプルで便利ですが、非常に人気があるにもかかわらず、標準として承認されていません。 操作の原理は非常に原始的です(最も一般的なスキーム):
画像
そのため、ユーザーはWebページの登録ボタンのいずれかをクリックします。
画像
(そして、ページに「追加」機能を残さず、 www.myproject.com / registration / facebookなどのアドレスを持つボタンのみを残します)、リクエストはコントローラーに送られます(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)); } 

スコープのパラメーターはdeveloper.facebook.com/docs/authentication/permissionsで見つけることができます(Twitterの場合-dev.twitter.com/docs/platform-objects/users、Google +の場合はdeveloper.google.com/accounts/docs/OAuth2Login# userinfocall 、LinkedIn-developer.linkedin.com/ documents /profile- fields )、ここにカップルを連れてきました。 redirect_uriドメインは、アプリケーションの登録アドレスと一致する必要があります。 stateは「無料」のパラメーターであり、登録、サインイン、自動サインインなどの追加アクションのセマフォとして使用します。

次に、ユーザーはFacebookログインページの承認に「リダイレクト」します。ここで、アプリケーションがデータを使用できるようにし、許可が基本的な許可を超えている場合、許可ウィンドウにリストされます。

承認後、マッピングFACEBOOK_IRL_CALLBACK_REGISTRATIONを備えたコントローラーが呼び出しを受信します(クライアントによる任意の決定-ログイン、キャンセル、戻る)。 Spring MVCでは、マッピングによってリクエストをフィルタリングできます(この場合、プロジェクトのマッピングが提供されます)。
 @ 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 } 

利便性と単一の使用のために、このリストで使用されるUtilsクラスのいくつかの静的メソッド:
 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; } 

定数:
 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ライブラリを使用できます。私はmjsonライブラリ( http://sharegov.blogspot.com/2011/06/json-library.html )を使用しました-小さく、便利で、シリアル化が不要です。

ご覧のとおり、プロセスは単純であり、特別な質問をするべきではありません。 また、Facebookが位置パラメーターを提供し、その値からGoogle Maps APIを 「スリップ」して( http://maps.googleapis.com/maps/api/geocode/jsonで )、便利な形式で地理位置情報を引き出すことに注意してください (標準で) Googleマップ)。 これは、自分のFacebookアカウントのクライアントが場所の国だけではないことを示している場合にのみ実行できることは明らかです。

Google+へのサインアップも同様の方法で行われますが、唯一の違いは、システムのコールバックURLがアプリケーション設定で指定されたものと完全に一致する必要があることです。 したがって、すべてのリダイレクトは1つのマッピングのみに該当します。 プロセスを分離するには、返された状態パラメーターを使用すると便利です。
 @ 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 { ... } } 

アドレスと戻りパラメーターを除き、残りのアクションは同じです。

OAuth認証(TwitterとLinkedIn)では状況が異なります。 私は認証チェーン全体を調べましたが、これはトークンでリクエストを形成するため非常に不便です-特別な方法で「接着」し、base64をパックし、時間の経過とともにパラメーターを追加するなどの操作が必要です。 そして最も驚くべきこと-これらのソーシャルネットワークの開発者向けのセクションには、これらのプロセスは表示されません。 したがって、これは標準ですが、計算は標準的なアプローチになります。 いずれにせよ、「手動で」実装されたこの方法での承認は、アプリケーションの開発には関係ありません。 これを簡単にするサードパーティの無料ライブラリを使用することをお勧めします。 たとえば、Twitter専用のライブラリtwitter4j.jarがあります。 MITライセンスの権利の下で配布されているscribe-javaライブラリ( http://github.com/fernandezpablo85/scribe-java )を使用しました。 このパッケージには、 Digg APIFacebook APIFlickr APIFreelancer APIGoogle APILinkedIn APISkyrock APITumblr APITwitter APIVkontakte APIYahoo API 、その他多数の2つのAPIが含まれます。

スクライブライブラリを使用したTwitterの登録プロセスは次のようになります。 登録ページからの承認のためのクライアント要求コントローラー:
 @ 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コールバックコントローラー:
 @ 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 } 

繰り返しますが、すべてが非常にシンプルで手頃な価格です。 LinkedIn APIを介した登録は、まったく同じ方法で行われます。

最後-標準的な方法での登録。 標準-それが標準である理由です。コードは提供しません。その結果、AuthUserから継承したSimpleAuthUser型のオブジェクトを作成することを明確にします。
  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); 

この場合、権限NEW_CUSTOMERが必要でした -登録済みユーザーは登録の確認が必要であるため(標準プラクティス)、a)別の役割があります b)Spring Securityを許可することは許可されていません(enabled = false)。

サイトでの承認


単純な春の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
(クラスは完全に愚かですが、Spring SecurityはAuthenticationProviderインターフェースの後継を供給する必要がありますが、意味に関して最も近いPreAuthenticatedAuthenticationProviderは適切ではありません):
 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; } } 

そしておそらく、セキュリティを整理するための最も「ボトルネック」な場所は、 RememberMeメカニズムの実装です。 原則として、 RememberMeサービスの操作がTokenBasedRememberMeServices実装と完全に一貫するように、すべてがすでに整理されています。1つの明確化:自動クライアント認証のすべてのデータはデータベースにありますが、ユーザーがサイトにログオンしてからアカウントを削除する場合があります .そして、それは矛盾をもたらします-ユーザーを承認しますが、実際には彼はそこにいません。つまり、サードパーティのサービスを介した承認の主な原則に違反しています。つまり、RememberMeメカニズムがトリガーされると、自動的に着信するクライアントを確認する必要があります。各ネットワークプロバイダーのAPIにはそのようなメカニズムがありますが、適切な段階で確認するために、SpringのRememberMeの作業に「くさび」をかける必要があります。残念ながら、クラスを拡張しても機能しません(AbstractRememberMeServicesにはfinalに設定する必要があるメソッドがあります)ので、クラスを完全に再定義する必要があります。私の方法はより面倒で、最後から行きました、そして、人間の平凡な怠doesは、より単純なオプションにそれをやり直すことを許しません。AbstractRememberMeServicesクラスを完全に再定義しましたTokenBasedRememberMeServicesクラスのコードを含めて、パブリック認証autoLoginメソッド(HttpServletRequestリクエスト、HttpServletResponseレスポンス)に数行を追加します-メソッドで値を確認した後、即時認証の前に、クライアントの「現実」検証メソッドへの呼び出しを挿入しました:
 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メソッドを呼び出した後、直接認証する前に同じコードを配置する必要があります。コードの方が安価で理解しやすいですが、設定に介入する必要があります。誰もがどちらの方法をとるかを決めるでしょうが、私の意見では、2番目の方法はイデオロギー的に「純粋」です。

また、ExternalControllerクラスcheckAccount(user)の呼び出しについても明確にする必要があります私のすべてのコールバックコントローラーは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()); } 

など

サイト自体での直接認証(ログイン、サインイン)は、登録と非常に似ています。ユーザーはページに移動して「ログイン」をクリックし、承認にリダイレクトし
画像

ます。サーバーに渡すのは、「サインイン」または「自動サインイン」パラメーターのみです。「自動的にサインイン」チェックボックスがクリックされたかどうかによって異なります。さらに、すべてが登録と同様のシナリオに従って発生し、パラメーターの変更、コールバックURLのみ、すべてのスコープまたは権限の削除-クライアントIDとそのトークンのみを取得する必要があります。コントローラのメソッドを適切にチェックした後、データベース内のトークンを上書きすることをお勧めします。また、たとえば、Facebookはテスト中にクライアントトークンを変更しませんでしたが、Google +は毎回変更します。 「変更」が発生する頻度はわからないので、各access_tokenの後に(実際には、プロバイダーによる非自動承認ごとに)書き換えます。

そして、最も重要なポイントは、例としてFacebookコントローラーを使用した、Spring Securityのユーザーの直接承認です(もちろん、コンプライアンスを確認し、プロバイダーのAPIから権利を取得した後)。
 @ 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)); } } 

これで、自動ログインのチェックボックスが選択された状態で、クライアントは自動的にログインされます。したがって、チェックマークがない場合RememberMeサービスのログアウトメソッド呼び出すと、Cookieが消去されます(他に何も行われません)。ちなみに、「/ログアウト」リンクをクリックすると、認証が削除され、Cookieが自動的にクリアされます。これは、上記の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'})"))。これは、security:global-method-securityパラメーターのSpring Security構成でのみ指定する必要があります

同様に、ビュー(JSP内でSpring Securityの機能を利用できます例:
 <%@ 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> 

このような構造により、モデルをコントローラーからビューに転送せずに、モデルをビューに削除するメカニズムをそのままにすることができます(それ自体はDAOのモデルに適用されます)。jspページでjsp スクリプトレット

使用することもできます(スクリプトレットの使用には多くの敵がいますが、主に「bean-god、Caesar-cesarean」という位置のため、プログラマーはプログラミングに従事しており、レイアウトおよび/またはデザイナーは設計中です;しかしこれは議論の余地はありますが、私は個人的にはいずれの概念も支持していません-はい、い、はい、時には非常に便利です):
 <%@ 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> <% } %> 

指定されたページコードは、セキュリティのコンテキストを使用する可能性のみを反映しますが、ページのロジックの有意性を装います。

認証オブジェクトとそのパラメーター(プリンシパルオブジェクト)間の遅延依存に起因するボトルネックに焦点を当てたいと思います。ユーザーフィールド(getUser()メソッドを呼び出す)が含まれているため、変更を行わないと、ページコードの両方の部分でランタイム例外が発生しますすべてのフィールドがnullで埋められたデフォルトのオブジェクト。OpenSessionInViewパターンの使用この場合、依存オブジェクトの追加のロードなしでは、ここでのHTTPセッションは異なるため、役に立ちません。したがって、ロードするとすぐに依存オブジェクトをロードする必要がありますが、これは遅延接続が割り当てられる原因となったアプローチと矛盾します-オブジェクトがロードされ、依存オブジェクトを変更してもロードされたオブジェクトは更新されません。この場合、EAGER接続を確立する方が簡単です。一般的に使用されるsessionFactory.getCurrentSession()を新しいセッションの開始で置き換えることにより、authenticationDAOでこれを決定しましたSessionFactoryUtils.openSession(sessionFactory)おそらく、これはメモリの面で最も経済的なソリューションではありませんが、私はまだこの質問をしていないため、このトピックを掘り下げていません。現在のセッションの存在のチェックを設定することで、フィルターまたはOpenSessionInViewインターセプターを拒否して、実際にその作業を置き換えることができると思います

このテキストは必要以上のものであることが判明しました。確かに物議をかもしたり、誤った瞬間さえありますが、考えられたメカニズムを実装する際に私が遭遇した困難の解決策を反映しようとしました。

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


All Articles