(五)OAuth 2.0 密码模式(Password)

前言

我们了解到授权码模式是OAuth2.0四种模式流程最复杂模式,复杂程度由大至小:授权码模式 > 隐式授权模式 > 密码模式 > 客户端模式

其中密码模式的流程是:让用户填写表单提交到授权服务器,表单中包含用户的用户名、密码、(client_Id + client_secret)的加密串,授权服务器先解析并校验客户端信息,然后校验用户信息,完全通过返回access_token,否则默认都是401 状态码,提示未授权无法访问

Demo基本结构

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

Maven依赖

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

    <dependencies>
        <!--web容器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--安全认证框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!--稳定的spring-security-oauth2版本-->
        <dependency>
            <groupId>org.springframework.security.oauth</groupId>
            <artifactId>spring-security-oauth2</artifactId>
            <version>2.3.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

搭建授权服务器(Authorization Server)

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

授权服务器结构主体:

img

WebSecurityConfiguration:

package com.authorization.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.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();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @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 lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

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

    private final AuthenticationManager authenticationManager;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
		// 配置端点
        endpoints.authenticationManager(authenticationManager);
    }

    @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", "implicit")
                // 该客户端允许的授权范围配置
                .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.0密码模式中,要求不仅仅用户需要登录,还要求客户端也需要登录,这里就需要在configure(ClientDetailsServiceConfigurer clients)这个方法中配置客户端(第三方应用)的登录信息:

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

application.yml:

这里我只配置了

# 服务器端口
server:
  port: 6600

搭建资源服务器(Resource Server)

资源服务器结构主体:

img

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

获取token

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

img

img

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

请求参数列表:

  • username=账号
  • password=密码
  • scope=作用域
  • grant_type=password

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

username:user
password:123456
scope:userInfo
grant_type:password
{
   "access_token": "abf35123-2d51-4ce8-bd71-4e493f5e26e4",
   "token_type": "bearer",
   "refresh_token": "1bfefc92-f26d-4e10-a813-6c4df759cda2",
   "expires_in": 3599,
   "scope": "userInfo"
}

携带token访问

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

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

img

后记

本文仅说明密码模式的精简化配置,某些部分如资源服务再访问授权服务去校验token这部分生产环境可能会换成Jwt、Redis等tokenStore实现,授权服务器中的用户信息与客户端信息生产环境应从数据库中读取,对应Spring Security的UserDetailsService实现类或用户信息的Provider等。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
OAuth 2.0定义了多种授权模式,用于在第三方应用和资源拥有者之间进行授权。以下是OAuth 2.0中常用的几种授权模式: 1. 授权码模式(Authorization Code Grant):这是最常见的授权模式,适用于Web应用。用户在第三方应用中点击授权按钮后,将被重定向到授权服务器进行登录和授权,然后授权服务器返回一个授权码给第三方应用,再通过授权码获取访问令牌。 2. 隐式授权模式(Implicit Grant):适用于纯前端应用,如单页应用(SPA)或移动应用。在这种模式下,用户直接在浏览器中进行登录和授权,并将访问令牌作为返回结果直接传递给第三方应用。相比于授权码模式,隐式授权模式省略了获取授权码的步骤。 3. 密码模式Password Grant):适用于受信任的第一方应用,如移动应用。用户将用户名和密码直接提供给第三方应用,并由第三方应用将其发送到认证服务器进行验证和授权,然后返回访问令牌给第三方应用。 4. 客户端凭证模式(Client Credentials Grant):适用于第三方应用代表自身进行身份验证和授权,而不是代表用户。第三方应用使用其自己的凭证(客户端ID和客户端密钥)直接向授权服务器请求访问令牌。 5. 设备授权模式(Device Authorization Grant):适用于没有直接的用户界面的设备或应用,如智能电视或物联网设备。设备通过展示授权码和用户指令,引导用户在另一个设备上进行授权。 这些授权模式提供了不同的方式来满足不同场景下的授权需求,开发者可以根据具体应用情况选择适合的模式。同时,需要注意选择合适的模式时要考虑安全性和用户体验之间的权衡。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值