Go to list of users who liked
Share on X(Twitter)
Share on Facebook
More than 5 years have passed since last update.
AngularJSでSpring Security
2017/04/30 追記
現バージョンでは、CookieCsrfTokenRepositoryを利用したほうが簡単に実装できるはずです。
https://docs.spring.io/spring-security/site/docs/current/reference/html/csrf.html#csrf-cookie
「はじめてのSpring Boot」が面白く、これはSpring覚えねば!と、いう気分になりWebアプリを実装中。
Spring Securityを使用すると認証周りの実装がとても簡単になるようなので使ってみるかー、SPAでもなんとかなるだろうと手を出したら割と大変でした。
もっと簡単に実現できそうではあるのですが。。
フロントは2.0の移行が大変そうなAngularJSを使用しました。
テンプレートがわかりやすいのでAngular1.x系は残しておいて欲しいなあ。
あと、開発が遅くてイマイチな評価のUI Bootstrapを使ってます。
参考
はじめてのSpring Boot
読むといろいろ作りたくなります。本の薄さと裏腹に内容がとても濃いです。Spring Security and Angular JS
Spring公式のAngularJSとSpring Securityの組み合わせ例。
ほぼこれでやりたいことできるのですが、自分の場合、ログイン画面をモーダルで表示したかったので、そこだけ適用できず。Spring Security
Spring Security公式です。
実装
サーバサイド
SecurityConfigで、静的リソースとログインAPI以外は未認証の場合エラーとします。
POSTがデフォルトでCSRFチェック対象のため、最初のログインのURLをチェック対象外に設定しています。(requireCsrfProtectionMatcherの設定をしないと、ログインのPOSTが403)
チェック対象外ロジックはstackoverflowのここから拝借しました。
csrfTokenRepositoryはSpring Security and Angular JSのコードを使わせてもらっています。
Spring SecurityはデフォルトでCSRFチェックをしていて、AngularJSも$http($resourceでも)でX-XSRF-TOKENヘッダが自動的に付加されることを利用して、ここでCSRFのヘッダ名を指定しています。
AngularJSはCookie(名称:XSRF-TOKEN)からX-XSRF-TOKENトークンをHTTPヘッダにセットする(参考:$http)ので、Spring SecurityがAngularJSに合わせるとこうなるということですね。
@ConfigurationpublicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@Overrideprotectedvoidconfigure(HttpSecurityhttp)throwsException{RequestMatchercsrfRequestMatcher=newRequestMatcher(){// CSRF対象外URL:privateAntPathRequestMatcher[]requestMatchers={newAntPathRequestMatcher("/api/login")};@Overridepublicbooleanmatches(HttpServletRequestrequest){for(AntPathRequestMatcherrm:requestMatchers){if(rm.matches(request)){returnfalse;}}returntrue;}};http.authorizeRequests().antMatchers("/images/**","/scripts/**","/styles/**","/views/**","/index.html","/api/login").permitAll().anyRequest().authenticated()//上記にマッチしなければ未認証の場合エラー.and().csrf().requireCsrfProtectionMatcher(csrfRequestMatcher).csrfTokenRepository(this.csrfTokenRepository());}privateCsrfTokenRepositorycsrfTokenRepository(){HttpSessionCsrfTokenRepositoryrepository=newHttpSessionCsrfTokenRepository();repository.setHeaderName("X-XSRF-TOKEN");returnrepository;}}UserDetailsServiceを設定します。
@ConfigurationpublicclassGlobalAuthenticationConfigextendsGlobalAuthenticationConfigurerAdapter{@Overridepublicvoidinit(AuthenticationManagerBuilderauth)throwsException{auth.userDetailsService(userDetailsService());}@BeanUserDetailsServiceuserDetailsService(){returnnewFooAuthService();}}ログインのエンドポイントになるRestControllerです。
サービスクラスで認証されたら組み込みのCSRFフィルターから取得したトークンをCookieにセットして、NGなら401を返却しています。
@RestController@RequestMapping("/api/")publicclassLoginRestController{@AutowiredLoginServiceloginService;@RequestMapping(value="login",method=RequestMethod.POST)ResponseEntity<PageDto>login(@RequestBodyLoginDtologinDto,HttpServletRequestrequest,HttpServletResponseresponse){PageDtopageDto=loginService.login(loginDto);if(pageDto.getHeaderDto().isAuth()){CsrfTokencsrf=(CsrfToken)request.getAttribute(CsrfToken.class.getName());if(csrf!=null){Cookiecookie=WebUtils.getCookie(request,"XSRF-TOKEN");Stringtoken=csrf.getToken();Authenticationauthentication=SecurityContextHolder.getContext().getAuthentication();if((cookie==null||token!=null&&!token.equals(cookie.getValue()))&&(authentication!=null&&authentication.isAuthenticated())){cookie=newCookie("XSRF-TOKEN",token);cookie.setPath("/");//適切なスコープでresponse.addCookie(cookie);}}returnnewResponseEntity<>(pageDto,null,HttpStatus.OK);}else{returnnewResponseEntity<>(pageDto,null,HttpStatus.UNAUTHORIZED);}}@RequestMapping(value="logout",method=RequestMethod.POST)voidlogout(HttpServletRequestrequest){try{request.logout();}catch(ServletExceptione){thrownewRuntimeException(e);}}}認証処理は公式のロジックを拝借してます。
UserDetailsとUserDetailsServiceの実装クラスははじめてのSpring Bootがわかりやすいです。
@ServicepublicclassLoginService{@AutowiredprotectedMappermapper;@AutowiredprivateAuthenticationManagerauthManager;/** * ログイン処理を行います. * @return */publicPageDtologin(LoginDtologinDto){PageDtopageDto=newPageDto();LoginDtooutLoginDto=mapper.map(loginDto,LoginDto.class);pageDto.setLoginDto(outLoginDto);HeaderDtoheaderDto=newHeaderDto();pageDto.setHeaderDto(headerDto);//Spring Security認証処理AuthenticationauthResult=null;try{Authenticationrequest=newUsernamePasswordAuthenticationToken(loginDto.getMailAddress(),loginDto.getPassword());authResult=authManager.authenticate(request);SecurityContextHolder.getContext().setAuthentication(authResult);FooUserDetailsprincipal=(FooUserDetails)authResult.getPrincipal();headerDto.setNickName(principal.getUserInfo().getNickName());headerDto.setAuth(true);}catch(AuthenticationExceptione){outLoginDto.setMessage("メールアドレスかパスワードがまちがっています。");headerDto.setAuth(false);}returnpageDto;}}フロントエンド
UI Bootstrap使ってモーダルでログイン画面を表示しています。
index.htmlでログインのhtmlテンプレートを読み込みます。
<!-- 略 --><divclass="header"ng-controller="HeaderCtrl"><!-- 略 --><divclass="collapse navbar-collapse"id="js-navbar-collapse"><ulclass="nav navbar-nav"><liclass="active"><ahref="#/">Home</a></li><ling-if="!headerDto.auth"><inputtype="button"class="btn btn-info"value="ログイン"ng-click="loginDialog()"/></li><ling-if="headerDto.auth"><inputtype="button"class="btn btn-info"value="ログアウト"ng-click="logout()"/></li><li><divng-if="headerDto.auth">{{headerDto.nickName}}さん</div></li></ul><divng-includesrc="'./views/login.html'"></div></div><!-- 略 --></div><!-- 略 -->モーダルで実行されるログインテンプレートです。
<div><scripttype="text/ng-template"id="login.tmpl.html"><divclass="modal-header"><h4>ログイン</h4></div><divclass="modal-body"><span>{{loginDto.message}}</span><br/><label>メールアドレス</label><inputtype="text"class="form-control"ng-model="loginDto.mailAddress"><br/><label>パスワード</label><inputtype="password"class="form-control"ng-model="loginDto.password"><br/><inputtype="button"class="btn btn-info"value="ログイン"ng-click="login()"/><inputtype="button"class="btn btn-default"value="cancel"ng-click="cancel()"/></div></script></div>LoginRestControllerで定義されたエンドポイントにアクセスするサービスです。
SIerがAngular+Javaで実装するんならRestController(Java)とそれにアクセスするサービス(JavaScript)がExcelで自動生成されたりするんでしょうか。
angular.module('fooApp').factory('loginHttp',['$http',function($http){varlogin={login:function(param){return$http.post('/foo/api/login',param);},logout:function(){return$http.post('/foo/api/logout');}}returnlogin;}]);ヘッダ部分のコントローラで「ログイン」ボタンが押下時にモーダルダイアログを起動させています。
angular.module('fooApp').controller('HeaderCtrl',['$scope','$modal','loginHttp',function($scope,$modal,loginHttp){$scope.loginDialog=function(){varmodalInstance=$modal.open({size:'sm',templateUrl:'login.tmpl.html',controller:'LoginCtrl'});modalInstance.result.then(function(data){$scope.headerDto=data;});};$scope.logout=function(){loginHttp.logout();$scope.headerDto={};}}]);ログイン画面のコントローラでログインサービスを実行します。
認証エラーの場合は401が返ってくるのでerror()に入ります。
angular.module('fooApp').controller('LoginCtrl',['$scope','loginHttp','$modalInstance',function($scope,loginHttp,$modalInstance){$scope.login=function(){loginHttp.login({mailAddress:$scope.loginDto.mailAddress,password:$scope.loginDto.password}).success(function(data){$scope.loginDto=data.loginDto;$modalInstance.close(data.headerDto);}).error(function(data){$scope.loginDto=data.loginDto;})}$scope.cancel=function(){$modalInstance.close();}}]);まとめ
モーダルのログイン画面でSpring Securityを実装するのは、ちょっと面倒でした。
もっと簡潔に実現できる方法があったら知りたいなあ。。
でも、Spring Securityで実現できるセキュリティ対策をフルスクラッチで作成することと比べたら、はるかに楽かなと思います。
Spring BootでSpring盛り上がっている感じなので、Spring関連の日本語資料いっぱいでるといいですね!
Register as a new user and use Qiita more conveniently
- You get articles that match your needs
- You can efficiently read back useful information
- You can use dark theme