`
m635674608
  • 浏览: 5029186 次
  • 性别: Icon_minigender_1
  • 来自: 南京
社区版块
存档分类
最新评论

SSO with OAuth2: Angular JS and Spring Security

 
阅读更多

In this article we continue our discussion of how to use Spring Security with Angular JS in a “single page application”. Here we show how to use Spring Security OAuth together with Spring Cloud to extend our API Gateway to do Single Sign On and OAuth2 token authentication to backend resources. This is the fifth in a series of articles, and you can catch up on the basic building blocks of the application or build it from scratch by reading the first article, or you can just go straight to the source code in Github. In the last article we built a small distributed application that used Spring Session to authenticate the backend resources and Spring Cloud to implement an embedded API Gateway in the UI server. In this article we extract the authentication responsibilities to a separate server to make our UI server the first of potentially many Single Sign On applications to the authorization server. This is a common pattern in many applications these days, both in the enterprise and in social startups. We will use an OAuth2 server as the authenticator, so that we can also use it to grant tokens for the backend resource server. Spring Cloud will automatically relay the access token to our backend, and enable us to further simplify the implementation of both the UI and resource servers.

Reminder: if you are working through this article with the sample application, be sure to clear your browser cache of cookies and HTTP Basic credentials. In Chrome the best way to do that for a single server is to open a new incognito window.

Creating an OAuth2 Authorization Server

Our first step is to create a new server to handle authentication and token management. Following the steps in Part I we can begin with Spring Boot Initializr. E.g. using curl on a UN*X like system:

$ curl https://start.spring.io/starter.tgz -d style=web \-d style=security -d name=authserver | tar -xzvf -

You can then import that project (it’s a normal Maven Java project by default) into your favourite IDE, or just work with the files and “mvn” on the command line.

Adding the OAuth2 Dependencies

We need to add the Spring OAuth dependencies, so in our POM we add:

<dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.0.5.RELEASE</version></dependency>

The authorization server is pretty easy to implement. A minimal version looks like this:

@SpringBootApplicationpublicclassAuthserverApplicationextendsWebMvcConfigurerAdapter{publicstaticvoid main(String[] args){SpringApplication.run(AuthserverApplication.class, args);}@Configuration@EnableAuthorizationServerprotectedstaticclassOAuth2ConfigextendsAuthorizationServerConfigurerAdapter{@AutowiredprivateAuthenticationManager authenticationManager;@Overridepublicvoid configure(AuthorizationServerEndpointsConfigurer endpoints)throwsException{
      endpoints.authenticationManager(authenticationManager);}@Overridepublicvoid configure(ClientDetailsServiceConfigurer clients)throwsException{
      clients.inMemory().withClient("acme").secret("acmesecret").authorizedGrantTypes("authorization_code","refresh_token","password").scopes("openid");}}

We only had to do 2 things (after adding @EnableAuthorizationServer):

  • Register a client “acme” with a secret and some authorized grant types including “authorization_code”.

  • Inject the default AuthenticationManager from Spring Boot autoconfiguration and wire it into the OAuth2 endpoints.

Now let’s get it running on port 9999, with a predictable password for testing:

server.port=9999
security.user.password=password
server.contextPath=/uaa

We also set the context path so that it doesn’t use the default (“/”) because otherwise you can get cookies for other servers on localhost being sent to the wrong server. So get the server running and we can make sure it is working:

$ mvn spring-boot:run

or start the main() method in your IDE.

Testing the Authorization Server

Our server is using the Spring Boot default security settings, so like the server in Part I it will be protected by HTTP Basic authentication. To initiate an authorization code token grant you visit the authorization endpoint, e.g. at http://localhost:9999/uaa/oauth/authorize?response_type=code&client_id=acme&redirect_uri=http://example.com once you have authenticated you will get a redirect to example.com with an authorization code attached, e.g. http://example.com/?code=jYWioI.

Note: for the purposes of this sample application we have created a client “acme” with no registered redirect, which is what enables us to get a redirect the example.com. In a production application you should always register a redirect (and use HTTPS).

The code can be exchanged for an access token using the “acme” client credentials on the token endpoint:

$ curl acme:acmesecret@localhost:9999/uaa/oauth/token  \
-d grant_type=authorization_code -d client_id=acme     \
-d redirect_uri=http://example.com -d code=jYWioI{"access_token":"2219199c-966e-4466-8b7e-12bb9038c9bb","token_type":"bearer","refresh_token":"d193caf4-5643-4988-9a4a-1c03c9d657aa","expires_in":43199,"scope":"openid"}

The access token is a UUID (“2219199c…”), backed by an in-memory token store in the server. We also got a refresh token that we can use to get a new access token when the current one expires.

Note: since we allowed “password” grants for the “acme” client we can also get a token directly from the token endpoint using curl and user credentials instead of an authorization code. This is not suitable for a browser based client, but it’s useful for testing.

If you followed the link above you would have seen the whitelabel UI provided by Spring OAuth. To start with we will use this and we can come back later to beef it up like we did in Part II for the self-contained server.

 

Changing the Resource Server

If we follow on from Part IV, our resource server is using Spring Session for authentication, so we can take that out and replace it with Spring OAuth. We also need to remove the Spring Session and Redis dependencies, so replace this:

<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session</artifactId><version>1.0.0.RC1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-redis</artifactId></dependency>

with this:

<dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId></dependency>

and then remove the session Filter from the main application class, replacing it with the convenient @EnableOAuth2Resource annotation (from Spring Cloud Security):

@SpringBootApplication@RestController@EnableOAuth2ResourceclassResourceApplication{@RequestMapping('/')def home(){[id: UUID.randomUUID().toString(), content:'Hello World']}staticvoid main(String[] args){SpringApplication.run ResourceApplication, args
  }}

That much is enough to get us a protected resource. Run the application and hit the home page with a command line client:

$ curl -v localhost:9000> GET / HTTP/1.1>User-Agent: curl/7.35.0>Host: localhost:9000>Accept:*/*
> 
< HTTP/1.1 401 Unauthorized
...
< WWW-Authenticate: Bearer realm="null", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"
< Content-Type: application/json;charset=UTF-8
{"error":"unauthorized","error_description":"An Authentication object was not found in the SecurityContext"}

and you will see a 401 with a “WWW-Authenticate” header indicating that it wants a bearer token. We are going to add a small amount of external configuration (in “application.properties”) to allow the resource server to decode the tokens it is given and authenticate a user:

...
spring.oauth2.resource.userInfoUri: http://localhost:9999/uaa/user

This tells the server that it can use the token to access a “/user” endpoint and use that to derive authentication information (it’s a bit like the “/me” endpoint in the Facebook API). Effectively it provides a way for the resource server to decode the token, as expressed by the ResourceServerTokenServices interface in Spring OAuth2.

Note: the userInfoUri is by far not the only way of hooking a resource server up with a way to decode tokens. In fact it’s sort of a lowest common denominator (and not part of the spec), but quite often available from OAuth2 providers (like Facebook, Cloud Foundry, Github), and other choices are available. For instance you can encode the user authentication in the token itself (e.g. with JWT), or use a shared backend store. There is also a /token_info endpoint in CloudFoundry, which provides more detailed information than the user info endpoint, but which requires more thorough authentication. Different options (naturally) provide different benefits and trade-offs, but a full discussion of those is outside the scope of this article.

Implementing the User Endpoint

On the authorization server we can easily add that endpoint

@SpringBootApplication@RestController@EnableResourceServerpublicclassAuthserverApplication{@RequestMapping("/user")publicPrincipal user(Principal user){return user;}...}

We added a @RequestMapping the same as the UI server in Part II, and also the @EnableResourceServer annotation from Spring OAuth, which by default secures everything in an authorization server except the “/oauth/*” endpoints.

With that endpoint in place we can test it and the greeting resource, since they both now accept bearer tokens that were created by the authorization server:

$ TOKEN=2219199c-966e-4466-8b7e-12bb9038c9bb
$ curl -H "Authorization: Bearer $TOKEN" localhost:9000{"id":"03af8be3-2fc3-4d75-acf7-c484d9cf32b1","content":"Hello World"}
$ curl -H "Authorization: Bearer $TOKEN" localhost:9999/uaa/user
{"details":...,"principal":{"username":"user",...},"name":"user"}

(substitute the value of the access token that you obtain from your own authorization server to get that working yourself).

The UI Server

The final piece of this application we need to complete is the UI server, extracting the authentication part and delegating to the authorization server. So, as with the resource server, we first need to remove the Spring Session and Redis dependencies and replace them with Spring OAuth2.

Once that is done we can remove the session filter and the “/user” endpoint as well, and set up the application to redirect to the authorization server (using the @EnableOAuth2Sso annotation):

@SpringBootApplication@EnableZuulProxy@EnableOAuth2SsopublicclassUiApplication{publicstaticvoid main(String[] args){SpringApplication.run(UiApplication.class, args);}@Configuration@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)protectedstaticclassSecurityConfigurationextendsWebSecurityConfigurerAdapter{

Recall from Part IV that the UI server, by virtue of the @EnableZuulProxy, acts an API Gateway and we can declare the route mappings in YAML. So the “/user” endpoint can be proxied to the authorization server:

zuul:
  routes:
    resource:
      path:/resource/**
      url: http://localhost:9000
    user:
      path:/user/**
      url: http://localhost:9999/uaa/user

Lastly, we need to change the WebSecurityConfigurerAdapter to an OAuth2SsoConfigurerAdapter since now it is going to be used to modify the defaults in the SSO filter chain set up by @EnableOAuth2Sso:

@ConfigurationprotectedstaticclassSecurityConfigurationextendsOAuth2SsoConfigurerAdapter{@Overridepublicvoid match(RequestMatchers matchers){
      matchers.anyRequest();}@Overridepublicvoid configure(HttpSecurity http)throwsException{
      http.authorizeRequests().antMatchers("/index.html","/home.html","/").permitAll().anyRequest().authenticated().and().csrf().csrfTokenRepository(csrfTokenRepository()).and().addFilterAfter(csrfHeaderFilter(),CsrfFilter.class);}...// the csrf*() methods are the same as the old WebSecurityConfigurerAdapter}

The main changes (apart from the base class name) are that the matchers
go into their own method, and there is no need for formLogin() any more.

There are also some mandatory external configuration properties for the
@EnableOAuth2Sso annotation to be able to contact and authenticate with
thr right authorization server. So we need this in application.yml:

spring:
  oauth2:
    sso:
      home:
        secure:false
        path:/,/**/*.html
    client:
      accessTokenUri: http://localhost:9999/uaa/oauth/token
      userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
      clientId: acme
      clientSecret: acmesecret
    resource:
      userInfoUri: http://localhost:9999/uaa/user

The bulk of that is about the OAuth2 client (“acme”) and the
authorization server locations. There is also a userInfoUri (just
like in the resource server) so that the user can be authenticated in
the UI app itself. The “home” stuff is about allowing anonymous access
to the static resources in our single page application.

In the Client

There are some minor tweaks to the UI application on the front end that we still need to make to trigger the redirect to the authorization server. The first is in the navigation bar in “index.html” where the “login” link changes from an Angular route:

<divng-controller="navigation"class="container"><ulclass="nav nav-pills"role="tablist">
    ...
    <li><ahref="#/login">login</a></li>
    ...
  </ul></div>

to a plain HTML link

<divng-controller="navigation"class="container"><ulclass="nav nav-pills"role="tablist">
    ...
    <li><ahref="login">login</a></li>
    ...
  </ul></div>

The “/login” endpoint that this goes to is handled by Spring Security and if the user is not authenticated it will result in a redirect to the authorization server.

We can also remove the definition of the login() function in the “navigation” controller, and the “/login” route from the Angular configuration, which simplifies the implementation a bit:

angular.module('hello',['ngRoute']).config(function($routeProvider){

  $routeProvider.when('/',{
    templateUrl :'home.html',
    controller :'home'}).otherwise('/');}).// ....controller('navigation',function($rootScope, $scope, $http, $location, $route){

  $http.get('user').success(function(data){if(data.name){
      $rootScope.authenticated =true;}else{
      $rootScope.authenticated =false;}}).error(function(){
    $rootScope.authenticated =false;});

  $scope.credentials ={};

  $scope.logout =function(){
    $http.post('logout',{}).success(function(){
      $rootScope.authenticated =false;
      $location.path("/");}).error(function(data){
      $rootScope.authenticated =false;});}});

How Does it Work?

Run all the servers together now, and visit the UI in a browser at http://localhost:8080. Click on the “login” link and you will be redirected to the authorization server to authenticate (HTTP Basic popup) and approve the token grant (whitelabel HTML), before being redirected to the home page in the UI with the greeting fetched from the OAuth2 resource server using the same token as we authenticated the UI with.

The interactions between the browser and the backend can be seen in your browser if you use some developer tools (usually F12 opens this up, works in Chrome by default, requires a plugin in Firefox). Here’s a summary:

Verb Path Status Response
GET / 200 index.html
GET /css/angular-bootstrap.css 200 Twitter bootstrap CSS
GET /js/angular-bootstrap.js 200 Bootstrap and Angular JS
GET /js/hello.js 200 Application logic
GET /home.html 200 HTML partial for home page
GET /user 302 Redirect to login page
GET /login 302 Redirect to auth server
GET (uaa)/oauth/authorize 401 (ignored)
GET /resource 302 Redirect to login page
GET /login 302 Redirect to auth server
GET (uaa)/oauth/authorize 401 (ignored)
GET /login 302 Redirect to auth server
GET (uaa)/oauth/authorize 200 HTTP Basic auth happens here
POST (uaa)/oauth/authorize 302 User approves grant, redirect to /login
GET /login 302 Redirect to home page
GET /user 200 (Proxied) JSON authenticated user
GET /home.html 200 HTML partial for home page
GET /resource 200 (Proxied) JSON greeting

The requests prefixed with (uaa) are to the authorization server. The responses that are marked “ignored” are responses received by Angular in an XHR call, and since we aren’t processing that data they are dropped on the floor. We do look for an authenticated user in the case of the “/user” resource, but since it isn’t there in the first call, that response is dropped.

In the “/trace” endpoint of the UI (scroll down to the bottom) you will see the proxied backend requests to “/user” and “/resource”, with remote:true and the bearer token instead of the cookie (as it would have been in Part IV) being used for authentication. Spring Cloud Security has taken care of this for us: by recognising that we has @EnableOAuth2Sso and @EnableZuulProxy it has figured out that (by default) we want to relay the token to the proxied backends.

Note: As in previous articles, try to use a different browser for “/trace” so that there is no chance of authentication crossover (e.g. use Firefox if yoused Chrome for testing the UI).

The Logout Experience

If you click on the “logout” link you will see that the home page changes (the greeting is no longer displayed) so the user is no longer authenticated with the UI server. Click back on “login” though and you actually don’t need to go back through the authentication and approval cycle in the authorization server (because you haven’t logged out of that). Opinions will be divided as to whether that is a desirable user experience, and it’s a notoriously tricky problem (Single Sign Out: Science Direct article and Shibboleth docs). The ideal user experience might not be technically feasible, and you also have to be suspicious sometimes that users really want what they say they want. “I want ‘logout’ to log me out” sounds simple enough, but the obvious response is, “Logged out of what? Do you want to be logged out of all the systems controlled by this SSO server, or just the one that you clicked the ‘logout’ link in?” We don’t have room to discuss this topic more broadly here but it does deserve more attention. If you are interested then there is some discussion of the principles and some (fairly unappetising) ideas about implementations in the Open ID Connect specification.

Conclusion

This is almost the end of our shallow tour through the Spring Security and Angular JS stack. We have a nice architecture now with clear responsibilities in three separate components, UI/API Gateway, resource server and authorization server/token granter. The amount of non-business code in all layers is now minimal, and it’s easy to see where to extend and improve the implementation with more business logic. The next steps will be to tidy up the UI in our authorization server, and probably add some more tests, including tests on the JavaScript client. Another interesting task is to extract all the boiler plate code and put it in a library (e.g. “spring-security-angular”) containing Spring Security and Spring Session autoconfiguration and some webjars resources for the navigation controller in the Angular piece. Having read the articles in thir series, anyone who was hoping to learn the inner workings of either Angular JS or Spring Security will probably be disappointed, but if you wanted to see how they can work well together and how a little bit of configuration can go a long way, then hopefully you will have had a good experience. Spring Cloud is new and these samples required snapshots when they were written, but there are release candidates available and a GA release coming soon, so check it out and send some feedback via Github or gitter.im.

The next article in the series is about access decisions (beyond authentication) and employs multiple UI applications behind the same proxy.

Addendum: Bootstrap UI and JWT Tokens for the Authorization Server

You will find another version of this application in the source code in Github which has a pretty login page and user approval page implemented similarly to the way we did the login page in Part II. It also uses JWT to encode the tokens, so instead of using the “/user” endpoint, the resource server can pull enough information out of the token itself to do a simple authentication. The browser client still uses it, proxied through the UI server, so that it can determine if a user is authenticated (it doesn’t need to do that very often, compared to the likely number of calls to a resource server in a real application).

 

https://spring.io/blog/2015/02/03/sso-with-oauth2-angular-js-and-spring-security-part-v

分享到:
评论

相关推荐

    spring-boot spring-security-oauth2 完整demo

    《Spring Boot、Spring Security与OAuth2的完整示例解析》 在现代Web开发中,安全性是不可忽视的重要一环。Spring Boot、Spring Security和OAuth2是Java生态系统中用于构建安全Web应用的三大利器。本篇文章将围绕...

    spring security oauth2以及jwt实现sso单点登陆的功能

    1. Spring Security OAuth2: Spring Security 是一个全面的Java安全框架,它为Web应用和RESTful服务提供了强大的安全控制。OAuth2 是一个授权框架,允许第三方应用获取有限的访问权限,而无需共享用户的原始密码。...

    springboot-oauth:Spring boot 2 + Spring security 5 实现 SSO + OAuth认证

    client-2:资源服务器,使用RemoteTokenServices进行token验证(无法同时使用SSO和OAuth来保护同一资源) client-3:调用客户端,放开所有访问权限,通过OAuth2RestTemplate调用client-4(client_credentials模式) ...

    spring security + spring oauth2 +spring mvc SSO单点登录需要的最小jar包集合

    Spring Security、Spring OAuth2 和 Spring MVC 是构建现代Web应用程序中常用的安全框架和技术。Spring Security 提供了一套全面的授权和认证解决方案,而Spring OAuth2 则是用于处理身份验证和授权的服务提供者协议...

    spring security 基于oauth 2.0 实现 sso 单点登录Demo.zip

    spring security 基于oauth 2.0 实现 sso 单点登录Demo 使用 spring security 基于oauth 2.0 实现 sso 单点登录Demo spring boot + spring security + spring security oauth

    视频配套笔记_Spring Security OAuth2.0认证授权_v1.1.rar

    此外,笔记中可能还涉及了Spring Security与OAuth2.0结合时的常见问题和最佳实践,例如如何处理刷新令牌以延长访问权限,如何实现单点登录(SSO),以及如何利用JWT(JSON Web Tokens)来提高性能和安全性。...

    spring security oauth2 实现jwt sso

    Spring Security OAuth2 与 JWT 实现的SSO详解 在当今的互联网应用中,单点登录(Single Sign-On,简称SSO)已经成为一种常见的身份验证机制,它允许用户在一个系统中登录后,无需再次登录就能访问其他关联系统。在...

    spring-security-oauth2-2.0.3.jar(包括jar包,源码,doc)

    开发人员可以通过配置Spring Security的OAuth2模块来保护REST API、实现单点登录(SSO)或者让第三方应用接入。 3. **spring-security-oauth2-2.0.3.RELEASE-sources.jar**:这是源码JAR文件,包含Spring Security ...

    spring security和oauth2整合开发资料汇总

    Spring Security和OAuth2的整合主要是为了实现更复杂的授权场景,比如在单点登录(SSO)系统中,Spring Security可以处理用户登录,OAuth2则用于第三方应用的授权。以下是一些整合的关键点: 1. **Spring Security...

    SpringCloud+SpringBoot+OAuth2+Spring Security+Redis实现的微服务统一认证授权.doc

    SpringCloud+SpringBoot+OAuth2+Spring Security+Redis实现的微服务统一认证授权

    Spring Cloud 集成OAuth2实现身份认证和单点登录

    本篇将深入探讨如何利用Spring Cloud集成OAuth2来实现身份认证和单点登录(Single Sign-On,简称SSO)。 OAuth2是一种授权框架,它允许第三方应用获取资源所有者的特定资源,同时保护了资源所有者的隐私信息。在...

    spring security + oauth 2.0 实现单点登录、认证授权

    Spring Security和OAuth 2.0是两个在Web应用安全领域广泛应用的框架,它们结合使用可以构建强大的单点登录(SSO)和认证授权系统。在这个系统中,`xp-sso-server`代表了认证服务器,而`xp-sso-client-a`和`xp-sso-...

    Spring Security Oauth2.0认证授权专题

    Spring boot+Spring Security Oauth2.0,Sprint cloud+Spring Security Oauth2集成。四种认证方式。附带有代码,和案例,案例,还有视频链接。我保证看完就回,如果视频链接失效,评论回复我,我单独再给你一份。

    Spring Gateway+Security+OAuth2+RBAC 实现SSO统一认证平台

    内容:完整集成了Spring Gateway, Spring Security, Nacos, OAuth2, RBAC, 手机验证码登录等SSO统一认证平台,全平台唯一,全平台最全,全面细致,已经帮你踩了很多坑,一看就会,可以本地运行,或者直接作为认证...

    spring security oauth2 单点登录

    要实现SSO,你需要配置一个OAuth2认证服务器,例如使用Spring Security OAuth2的Authorization Server组件。这个服务器负责处理用户的登录请求,验证用户身份,并生成访问令牌(Access Token)和刷新令牌(Refresh ...

    enjoy-oauth2-parent:Spring Cloud Security OAuth SSO

    Spring cloud security oauth sso 主要实现spring cloud security oauth2配置 access_token存放用户授权信息,通过redis关联,以达到授权服务与资源服务共享,防止token无效 enjoy-oauth2-authorization:授权服务 ,...

    spring-security-oauth2与spring-security-web 3.1.2 源码

    在这个源码分析中,我们将深入探讨`spring-security-web 3.1.2`和`spring-security-oauth2`这两个关键组件。 首先,`spring-security-web`是Spring Security的核心库,它提供了Web应用程序的基本安全功能,如HTTP...

    spring security oauth2 实现的SSO单点登录案例.rar

    在本案例中,我们将探讨如何利用Spring Security OAuth2实现SSO(Single Sign-On)单点登录功能。 SSO是一种让用户只需一次登录就能够访问多个相互信任的应用系统的技术。在企业环境中,这种功能能够极大地提高用户...

    spring security oauth2整合的代码汇总,一共包括5个资料

    Spring Security OAuth2 是一个强大的安全框架,用于保护基于 REST 的 Web 服务和应用程序。它提供了认证和授权功能,使得第三方应用能够安全地访问资源服务器上的数据。本代码汇总包含五个资料,将帮助我们深入理解...

Global site tag (gtag.js) - Google Analytics