Spring Security OAuth2认证授权示例

本文介绍了如何使用Spring Security OAuth2构建一个授权服务器来验证用户身份以提供access_token,并使用这个access_token来从资源服务器请求数据。

1.概述

OAuth2是一种授权方法,用于通过HTTP协议提供对受保护资源的访问。首先,OAuth2使第三方应用程序能够获得对HTTP服务的有限访问权限,然后通过资源所有者和HTTP服务之间的批准交互来让第三方应用程序代表资源所有者获取访问权限。

1.1 角色

OAuth定义了四个角色

  • 资源所有者 - 应用程序的用户。
  • 客户端 - 需要访问资源服务器上的用户数据的应用程序。
  • 资源服务器 - 存储用户数据和http服务,可以将用户数据返回给经过身份验证的客户端。
  • 授权服务器 - 负责验证用户的身份并提供授权令牌。资源服务器接受此令牌并验证您的身份。

OAuth2的交互过程:

OAuth2机制

1.2 访问令牌与刷新令牌

访问令牌代表一个向客户授权的字符串。令牌包含了由资源所有者授予的权限范围和访问持续时间,并由资源服务器和授权服务器强制执行。

授权服务器向客户端发出刷新令牌,用于在当前访问令牌失效或过期时获取新的访问令牌,或获取具有相同或更窄范围的其他访问令牌,新的访问令牌可能具有比资源所有者授权的更短的生命周期和更少的权限。根据授权服务器的判断,发布刷新令牌是可选的。

访问令牌的责任是在数据到期之前访问它。
刷新令牌的责任是在现有访问令牌过期时请求新的访问令牌。

2. OAuth2 - 授权服务器

要使用spring Security OAuth2模块创建授权服务器,我们需要使用注解@EnableAuthorizationServer并扩展AuthorizationServerConfigurerAdapter类。

@Configuration
@EnableAuthorizationServer
public class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security)
            throws Exception {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients)
            throws Exception {
        clients.inMemory().withClient("clientapp")
                .secret(passwordEncoder.encode("654321"))
                .authorizedGrantTypes("password", "authorization_code",
                        "refresh_token")
                .authorities("READ_ONLY_CLIENT").scopes("read_user_info")
                .resourceIds("oauth2-resource")
                .redirectUris("http://localhost:8081/login")
                .accessTokenValiditySeconds(5000)
                .refreshTokenValiditySeconds(50000);
    }
}
  • Spring Security OAuth2会公开了两个端点,用于检查令牌(/oauth/check_token和/oauth/token_key),这些端点默认受保护denyAll()。tokenKeyAccess()和checkTokenAccess()方法会打开这些端点以供使用。

  • ClientDetailsServiceConfigurer用于定义客户端详细的服务信息,它可以在内存中或在数据库中定义。

    在本例中我们使用了内存实现。它具有以下重要属性:

    • clientId - (必需)客户端ID。
    • secret - (可信客户端所需)客户端密钥(可选)。
    • scope - 客户受限的范围。如果范围未定义或为空(默认值),则客户端不受范围限制。
    • authorizedGrantTypes - 授权客户端使用的授权类型。默认值为空。
    • authorities - 授予客户的权限(常规Spring Security权限)。
    • redirectUris - 将用户代理重定向到客户端的重定向端点。它必须是绝对URL。

3. OAuth2 - 资源服务器

要创建资源服务器组件,请使用@EnableResourceServer注解并扩展ResourceServerConfigurerAdapter类。

@Configuration
@EnableResourceServer
public class OAuth2ResourceServer extends ResourceServerConfigurerAdapter {
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/api/**").authenticated();
    }
}

以上配置启用/api下所有端点的保护,但可以自由访问其他端点。

资源服务器还提供了一种对用户自己进行身份验证的机制。在大多数情况下,它通常是基于表单的登录。

@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.antMatcher("/**")
                .requestMatchers()
                .antMatchers("/oauth/authorize**", "/login**", "/error**")
            .and()
                .authorizeRequests().anyRequest().authenticated()
            .and()
                .formLogin().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.inMemoryAuthentication().withUser("peterwanghao")
                .password(passwordEncoder().encode("123456")).roles("USER");
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

4. OAuth2保护的REST资源

本例中只创建了一个RESTful API,它返回登录用户的姓名和电子邮件。

@Controller
public class RestResource {
    @RequestMapping("/api/users/me")
    public ResponseEntity<UserInfo> profile() {
        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String email = user.getUsername() + "@126.com";

        UserInfo profile = new UserInfo();
        profile.setName(user.getUsername());
        profile.setEmail(email);

        return ResponseEntity.ok(profile);
    }
}
public class UserInfo {
    private String name;
    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public String toString() {
        return "User [name=" + name + ", email=" + email + "]";
    }
}

5.演示

我们有一个API http://localhost:8080/api/users/me ,我们想通过第三方应用程序来访问API需要OAuth2令牌。

5.1 从用户获取授权许可代码

如上面的序列图所示,第一步是从URL获取资源所有者的授权:

http://localhost:8080/oauth/authorize?client_id=clientapp&response_type=code&scope=read_user_info

通过浏览器访问上面的URL地址,它将展现一个登录页面。提供用户名和密码。对于此示例,请使用“peterwanghao”和“123456”。

登录

登录后,您将被重定向到授予访问页面,您可以在其中选择授予对第三方应用程序的访问权限。

授权

它会重定向到URL,如:http://localhost:8081/login?code=TUXuk9 。这里'TUXuk9'是第三方应用程序的授权代码。

5.2 从授权服务器获取访问令牌

现在,应用程序将使用授权码来获取访问令牌。在这里,我们需要提出以下请求。使用此处第一步中获得的代码。

curl -X POST --user clientapp:654321 http://localhost:8080/oauth/token -H "content-type: application/x-www-form-urlencoded" -d "code=TUXuk9&grant_type=authorization_code&redirect_uri=http://localhost:8081/login&scope=read_user_info"

访问令牌响应

{ 
    "access_token": "168aad01-05dc-4446-9fba-fd7dbe8adb9e", 
    "token_type": "bearer", 
    "refresh_token": "34065175-1e92-4bb0-918c-a5a6ece1dc5f", 
    "expires_in": 4999, 
    "scope": "read_user_info" 
}

5.3 从资源服务器访问用户数据

一旦我们有了访问令牌,我们就可以转到资源服务器来获取受保护的用户数据。

curl -X GET http://localhost:8080/api/users/me -H "authorization: Bearer 168aad01-05dc-4446-9fba-fd7dbe8adb9e"

获得资源响应

{
    "name":"peterwanghao",
    "email":"peterwanghao@126.com"
}

总结

本文讲解了OAuth2授权框架的实现机制,通过一个例子说明了第三方应用程序如何通过授权服务器的授权去资源服务器上获取受保护的数据。本例的完整代码在GitHub上。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 226,913评论 6 527
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,710评论 3 412
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 174,561评论 0 373
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 62,278评论 1 306
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 71,080评论 6 405
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,604评论 1 320
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,698评论 3 434
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,847评论 0 285
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 48,361评论 1 329
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 40,330评论 3 353
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,487评论 1 365
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,044评论 5 355
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,738评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,134评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 35,378评论 1 281
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 51,053评论 3 385
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,471评论 2 370

推荐阅读更多精彩内容