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

201 阅读7分钟

本文目标

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

授权码模式流程回顾

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

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

Demo基本结构

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

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

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.通过@Configuration 和EnableAuthorizationServer开启授权服务器配置,通过重写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