Spring Security OAuth2 Demo —— 授权码模式 (Authorization Code)

本文可以转载,但请注明出处https://www.cnblogs.com/hellxz/p/oauth2_oauthcode_pattern.html

写在前边

在文章OAuth 2.0 概念及授权流程梳理 中我们谈到OAuth 2.0的概念与流程,这里我准备分别记一记这几种授权模式的demo,一方面为自己的最近的学习做个总结,另一方面做下知识输出,如果文中有错误的地方,请评论指正,在此不胜感激

受众前提

阅读本文,默认读者已经过Spring Security有一定的了解,对OAuth2流程有一定了解

本文目标

带领读者对Spring Security OAuth2框架的授权码模式有一个比较直观的概念,能使用框架搭建授权码模式授权服务器与资源服务器(分离版本)

授权码模式流程回顾

授权码模式要求:用户登录并对第三方应用(客户端)进行授权,出示授权码交给客户端,客户端凭授权码换取access_token(访问凭证)

此模式要求授权服务器与用户直接交互,在此过程中,第三方应用是无法获取到用户输入的密码等信息的,这个模式也是OAuth 2.0中最安全的一个

Demo基本结构

这里主要关注authorization-code-authorization-serverauthorization-code-resource-server这两个模块

本文以及后续文章的demo均放在GitHub上,欢迎大家Star & Fork,源码地址:https://github.com/hellxz/spring-security-oauth2-learn

authorization-code-client-resttemplate-jdbc这个项目是用来测试非OAuth2服务使用RestTemplate与JdbcTemplate对接OAuth2授权服务的,流程这里不讲,有兴趣可以debug看看,可能会让您对整个流程会有更清晰的感受

Maven依赖

        <!--Spring Security-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--Spring Boot Starter Web 所有demo均使用web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Spring Security OAuth2 -->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>${spring-security-oauth2.version}</version>
        </dependency>

搭建授权服务器(Authorization Server)

文中服务器均使用demo级别配置,请勿直接使用到生产环境

授权服务器结构主体如下:


启动类自不多说,先说下SecurityConfig

package com.github.hellxz.oauth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Collections;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // @formatter: off
        auth.inMemoryAuthentication()
                .withUser("hellxz")
                .password(passwordEncoder().encode("xyz"))
                .authorities(Collections.emptyList());
        // @formatter: on
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated() //所有请求都需要通过认证
                .and()
                .httpBasic() //Basic登录
                .and()
                .csrf().disable(); //关跨域保护
    }
}

通过@Configuration@EnableWebSecurity开启Spring Security配置,继承WebSecurityConfigurerAdapter的方法,实现个性化配置,这里我们使用内存保存一个名为hellxz、密码为xyz的用户,与授权服务器交互的用户就是他了

除了配置用户,我们需要对服务的资源进行保护,这里将所有的请求都要求通过认证才可以访问,用户登录需要使用httpBasic形式(就是那种网页弹个窗要求登录的那种😄)

Spring Security 5.x版本后,要求显示声明使用的密码器,就是PasswordEncoder了,常用BCryptPasswordEncoder,简单的可以认为它是使用时间戳和盐进行加密的一种算法,同一个密码被加密后也不会相同


接着看看授权服务器的配置画重点

package com.github.hellxz.oauth2.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

//授权服务器配置
@Configuration
@EnableAuthorizationServer //开启授权服务
public class AuthorizationConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        //允许表单提交
        security.allowFormAuthenticationForClients()
                .checkTokenAccess("isAuthenticated()");
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // @formatter: off
        clients.inMemory()
                .withClient("client-a") //client端唯一标识
                    .secret(passwordEncoder.encode("client-a-secret")) //客户端的密码,这里的密码应该是加密后的
                    .authorizedGrantTypes("authorization_code") //授权模式标识
                    .scopes("read_user_info") //作用域
                    .resourceIds("resource1") //资源id
                    .redirectUris("http://localhost:9001/callback"); //回调地址
        // @formatter: on
    }
}

1.通过@ConfigurationEnableAuthorizationServer开启授权服务器配置,通过重写AuthorizationServerConfigurerAdapter的方法来完成自定义授权服务器

2.OAuth2授权码模式中,要求不仅仅用户需要登录,还要求客户端也需要登录,这里就需要在configure(ClientDetailsServiceConfigurer clients)这个方法中配置客户端(第三方应用)的登录信息,

  • withClient中配置的是客户端id(client_id)
  • secret为客户端的密码,要求使用加密器进行加密
  • 授权码的authorizedGrantTypes必须配置有"authorization_code"(授权码模式),这里是可以同时支持多种授权模式的,为了简单只写一个
  • scopes,请求资源作用域,用于限制客户端与用户无法访问没有作用域的资源
  • resourceIds,可选,资源id,可以对应一个资源服务器,个人理解为某个资源服务器的所有资源标识
  • redirectUris,回调地址,有两个作用:1.回调客户端地址,返回授权码; 2.校验是否是同一个客户端

redirectUris校验是否同一个客户端这个,可能说的不是很准确,说下大体流程,我们在授权服务器上配置了这个回调地址,授权服务器在用户授权成功后,返回授权码的地址就是它,另外我们后续申请token时,也需要传递这个回调地址,所以我的理解是校验是否是同一客户端发来的第二次请求(换token时)

3.configure(AuthorizationServerSecurityConfigurer security)这里配置资源客户端(第三方应用)的表单提交权限,类似Spring Security配置的permitAll()等权限控制标识,如果不配置,客户端将无法换取token


4.application.properties

这里我只配置了server.port=8080


这样我们就配置了相当简易的授权服务器,启动测试

获取授权码的流程一般是由客户端使用自己的client_id与密码+response_type=code拼接url,让浏览器跳转完成的,用户的登录与授权过程都需要在浏览器中完成,启动项目后访问下列url

http://localhost:8080/oauth/authorize?client_id=client-a&client_secret=client-a-secret&response_type=code

登录用户/密码: hellxz/xyz ,选择Approve表示接受授权,Deny反之,如下动图所示

最后我们得到了回调地址http://localhost:9001/callback?code=2e6450

这里的code就是授权码,接下来我们使用授权码进行换取token

POST请求,http://localhost:8080/oauth/token,参数如图

BasicAuth:这里填的是客户端配置的client_id和client_secret的值,相当于curl --user client_id:client_secret,配置后会在Header中添加Authorization:Basic Y2xpZW50LWE6Y2xpZW50LWEtc2VjcmV0Basic空格 后的是client_id:client_secret具体值被Base64后得到的值

请求参数列表:

  • code=授权码
  • grant_type=authorization_code
  • redirect_uri=回调url ,要与配置处和获取授权码处相同
  • scope=作用域

最后我们获得了授权服务的响应,包含token的json

{
    "access_token": "99435e13-f9fe-438a-a94e-3b00d549b329", //访问token
    "token_type": "bearer", //token类型,使用时需要拼接在token前并在token前加空格
    "expires_in": 43199, //过期时间
    "scope": "read_user_info" //作用域
}

在access_token未过期之前,同一个用户名使用同一个客户端访问都会是同一个access_token

授权服务器先放在这里,不要关服,接下来搭建资源服务器

搭建资源服务器(Resource Server)

资源服务器结构

入口类不多说,先搭建资源服务器主要配置,这里直接使用ResourceConfig进行配置

package com.github.hellxz.oauth2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;

@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {

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

    @Primary
    @Bean
    public RemoteTokenServices remoteTokenServices() {
        final RemoteTokenServices tokenServices = new RemoteTokenServices();
        //设置授权服务器check_token端点完整地址
        tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
        //设置客户端id与secret,注意:client_secret值不能使用passwordEncoder加密!
        tokenServices.setClientId("client-a");
        tokenServices.setClientSecret("client-a-secret");
        return tokenServices;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //设置创建session策略
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        //@formatter:off
        //所有请求必须授权
        http.authorizeRequests()
                .anyRequest().authenticated();
        //@formatter:on
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId("resource1").stateless(true);
    }
}

1.通过@Configuration@EnableResourceServer这两个注解标识服务是一个资源服务器,重写ResourceServerConfigurerAdapter来实现自定义授权服务器

2.配置configure(HttpSecurity http)方法,这里可以代替Spring Security同名方法配置,开启所有请求需要授权才可访问

3.配置资源相关设置configure(ResourceServerSecurityConfigurer resources),这里只设置resourceId

后续的使用redis校验token也在这里设置

4.校验token的配置,这里使用了远程调用授权服务器帮忙校验token的方式,只需要显示注入RemoteTokenServices remoteTokenServices()的Bean,就可以调用授权服务器的/oauth/check_token端点,设置客户端配置的值,详见注释


这样一来我们就配置好了资源服务器,当然光有配置是不够的,我们搞一个资源接口做测试用

上边的ResourceControllerUserVO都比较简单,传入一个名称,返回用户对象,包含用户名和邮箱信息

package com.github.hellxz.oauth2.web.controller;

import com.github.hellxz.oauth2.web.vo.UserVO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ResourceController {

    @GetMapping("/user/{username}")
    public UserVO user(@PathVariable String username){
        return new UserVO(username, username + "@foxmail.com");
    }
}

package com.github.hellxz.oauth2.web.vo;

public class UserVO {
    private String username;
    private String email;

    public UserVO(String username, String email) {
        this.username = username;
        this.email = email;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

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


application.properties中配置了与授权服务器不同的端口:8081

server.port=8081


启动资源服务测试

什么也不传,直接访问接口,提示资源需要授权

复制之前获取到的token,添加token访问接口http://localhost:8081/user/hellxz001

Bearer Token相当于在Headers中添加Authorization:Bearer空格access_token

至此我们成功的搭建并测试了授权码模式下的最简单的授权服务与资源服务分离的demo

尾声

授权码模式就先在这里告一段落,写的比较基础,自认为该说到的点都说到了,后续还会写其它模式的文章,如文中有何遗漏,请不吝评论反馈,本人会尽快改正,谢谢

本文以及后续文章的demo均放在GitHub上,欢迎大家Star & Fork,源码地址:https://github.com/hellxz/spring-security-oauth2-learn ,Demo中的README文档写得比较详细,也可堪一看

本文可以转载,但请注明出处https://www.cnblogs.com/hellxz/p/oauth2_oauthcode_pattern.html

posted @ 2019-12-10 02:01  东北小狐狸  阅读(20138)  评论(10编辑  收藏  举报