(三)OAuth 2.0 授权码模式 (Authorization Code)

前言

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

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

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

授权码模式流程

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

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

Demo基本结构

这里主要关注security-authorization-serversecurity-resource-server这两个模块

Maven依赖

    <properties>
        <java.version>1.8java.version>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <spring-boot.version>2.1.18.RELEASEspring-boot.version>
        <spring-cloud.version>Greenwich.SR6spring-cloud.version>
    properties>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-securityartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.security.oauthgroupId>
            <artifactId>spring-security-oauth2artifactId>
            <version>2.3.6.RELEASEversion>
        dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.bootgroupId>
            <artifactId>spring-security-oauth2-autoconfigureartifactId>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

搭建授权服务器(Authorization Server)

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

授权服务器结构主体:

(三)OAuth 2.0 授权码模式 (Authorization Code)_第1张图片

WebSecurityConfiguration:

package com.authorization.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;

/**
 * @description:
 * @Author C_Y_J
 * @create 2022/1/7 14:53
 **/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    /**
     * 密码编码解码器
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 添加一个用户、保存在内存中
        auth.inMemoryAuthentication()
                .withUser("user")
                .password(passwordEncoder().encode("123456"))
                .authorities(Collections.emptyList());
    }

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

继承WebSecurityConfigurerAdapter的方法,实现个性化配置,这里我们使用内存保存一个名为user、密码为123456的用户,与授权服务器交互的用户就是他了。

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

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

AuthorizationServerConfiguration:

package com.authorization.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;

/**
 * @description: @EnableAuthorizationServer 实现授权服务器
 * @Author C_Y_J
 * @create 2022/1/7 11:35
 **/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                // 配置内存中
                .inMemory()
                // 该客户端id
                .withClient("client_1")
                // 该客户端密钥
                .secret(new BCryptPasswordEncoder().encode("client_1_secret"))
                // 该客户端token有效时间  秒
                .accessTokenValiditySeconds(3600)
                // 该客户端支持的模式
                .authorizedGrantTypes("refresh_token", "password", "authorization_code")
                // 该客户端允许的授权范围配置
                .scopes("userInfo", "server", "all")
                // 该客户端资源列表
                .resourceIds("resource")
                // false 允许跳转到授权页面
                .autoApprove(false)
                // 该客户端验证回调地址
                .redirectUris("http://www.baidu.com");

    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 允许表单提交
        security.allowFormAuthenticationForClients();
        // 校验token的条件是已经通过身份认证的
        security.checkTokenAccess("isAuthenticated()");
    }
}

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

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

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

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

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

application.yml:

这里我只配置了

# 服务器端口
server:
  port: 6600

搭建资源服务器(Resource Server)

资源服务器结构主体:

(三)OAuth 2.0 授权码模式 (Authorization Code)_第2张图片

ResourceServerConfiguration:

package com.resource.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.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;

/**
 * @description: @EnableResourceServer 实现资源服务器
 * @Author C_Y_J
 * @create 2022/1/7 11:33
 **/
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                // 针对所有的请求
                .authorizeRequests()
                // 匹配规则:/user/{id}  不需要登录认证
                .antMatchers("/user/info/{id}").permitAll()
                // 匹配规则:任何请求 需要登录认证
                .anyRequest().authenticated();

        //允许表单登录
        http.formLogin();

        // 开启httpBasic认证
        http.httpBasic();

        // 关闭csrf防护
        http.csrf().disable();
    }


    @Primary
    @Bean
    public RemoteTokenServices remoteTokenServices() {
        RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
        // 设置授权服务器check_token端点完整地址
        remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:6600/oauth/check_token");
        // 设置客户端id与secret,注意:client_secret值不能使用passwordEncoder加密!
        remoteTokenServices.setClientId("client_1");
        remoteTokenServices.setClientSecret("client_1_secret");
        return remoteTokenServices;
    }
    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 设置授权服务器的资源列表
        resources.resourceId("resource");
    }
}

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

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

配置资源相关设置configure(ResourceServerSecurityConfigurer resources),这里只设置resourceId 后续的使用redis校验token也在这里设置

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

SysUserController:

package com.resource.controller;

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description:
 * @Author C_Y_J
 * @create 2021/12/10 10:49
 **/
@RestController
@RequestMapping("/user")
public class SysUserController {

    /**
     * 通过ID查询用户信息
     *
     * @param id ID
     * @return 用户信息
     */
    @GetMapping("/info/{id}")
    public String user(@PathVariable Integer id) {
        return ("通过" + id + "查询用户信息");
    }

    /**
     * 用户认证信息
     */
    @GetMapping(value = "/authentication")
    public String info() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!authentication.isAuthenticated()) {
            return null;
        }
        Object principal = authentication.getPrincipal();
        String username = null;
        if (principal instanceof UserDetails) {
            username = ((UserDetails) principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }

}

application.yml:

# 服务器端口
server:
  port: 7700

获取授权码

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

localhost:6600/oauth/authorize?client_id=client_1&client_secret=client_1_secret&response_type=code

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

(三)OAuth 2.0 授权码模式 (Authorization Code)_第3张图片

最后我们得到了回调地址 www.baidu.com/?code=0XyNpB

(三)OAuth 2.0 授权码模式 (Authorization Code)_第4张图片

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

获取token

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

(三)OAuth 2.0 授权码模式 (Authorization Code)_第5张图片

(三)OAuth 2.0 授权码模式 (Authorization Code)_第6张图片

BasicAuth:这里填的是客户端配置的client_id和client_secret的值,配置后会在Header中添加Authorization:Basic Y2xpZW50XzE6Y2xpZW50XzFfc2VjcmV0,Basic空格后的是client_id:client_secret`具体值被Base64后得到的值

请求参数列表:

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

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

code:tbW5VJ
scope:userInfo
redirect_uri:http://www.baidu.com
grant_type:authorization_code
{
    "access_token": "2910bbb5-7f99-46ff-97cd-9b6b63124cfc",
    "token_type": "bearer",
    "refresh_token": "bda3de27-8b28-4278-beeb-ccb3a894e891",
    "expires_in": 3599,
    "scope": "userInfo"
}

不携带token访问

127.0.0.1:7700/user/info/12

(三)OAuth 2.0 授权码模式 (Authorization Code)_第7张图片

127.0.0.1:7700/user/authentication

(三)OAuth 2.0 授权码模式 (Authorization Code)_第8张图片

携带token访问

复制之前获取到的token,添加token访问接口。

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

(三)OAuth 2.0 授权码模式 (Authorization Code)_第9张图片

后记

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

你可能感兴趣的:(#,Spring,Security,OAuth2.0,java,spring,boot,spring)