springsecurity 认证之授权码模式

Posted 小码农叔叔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springsecurity 认证之授权码模式相关的知识,希望对你有一定的参考价值。

前言

在上一篇,我们探讨了一下使用cookie实现单点登录,其实单点登录的实现方案以及由此衍生出来的单点登录模式是很多种的,其中有下面一种大家比较熟悉的模式



如上图,当进入网站主页进行登录的时候,登录框下面提供了多种其他登录方式,如微信登录,QQ登录,新浪账户登录等扩展登录,如果选择微信登录,会弹出一个二维码,后面的过程我就不再详细描述了,相信大家都懂了

这是社交应用常用的一种登录方式,说白了,属于基于oauth2的一种登录认证方式,如果进行分类的话,经验认为,也属于单点登录模式扩展的一种;

为什么这么说呢?

首先,扫描 ——> 授权——>登录,之后跳转到用户待访问的产品页面之后,如果页面有多个应用,仍然可以在多个应用之间任意切换,而不需要继续认证;

springsecurity oauth2.0 简述

以上面的业务场景为引子,下面谈谈基于springsecurity oauth2.0实现的单点登录

  • oauth2.0 ,是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容
  • springsecurity 提供了对 oauth2.0 标准的内置实现,即提供了一套完整的实现方案

springsecurity 关于登录认证,提供了4种模式

授权码模式(authorization code)

第三方先获取授权码,然后用该授权码获取授权。这种方式是最常用,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

一般流程为:

  • 用户打开客户端,客户端要求资源拥有者给予授权,浏览器重定向到认证中心(含有客户端信息)
  • 跳转后,网站会要求用户登录,然后询问是否同意给予授权,这一步需要用户事先具有资源的使用权限
  • 认证中心通过第一步提供的回调地址将授权码返回给服务端。注意这个授权码并非通行凭证
  • 服务端拿着授权码向认证中心索要访问 access_token,认证中心返回 token 和 refresh token

简化模式(implicit)

一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。这时就不能用授权码模式,必须将令牌储存在前端,token 直接暴露再浏览器。这种方式没有授权码这个中间步骤,所以称为授权码简化模式。

一般流程为:

  • 用户打开客户端,客户端要求资源拥有者给予授权,浏览器重定向到认证中心(含有客户端信息)
  • 跳转后,网站会询问是否同意给予授权
  • 认证中心将授权码将 access_token 以 Hash 的形式存放在重定向 uri 的 fargment 中发送给浏览器。( fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的未尾通过 # 作为 fragment 的开头,其中 # 不属于 fragment 的值)

密码模式(resource owner password credentials)

这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了客户端,因此这就说明这种模式只能用于客户端是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生 app 或第一方单页面应用。

一般流程为:

  • 用户再客户端填写用户名、密码
  • 客户端拿着资源拥有者的用户名、密码向认证中心请求 access_token
  • 认证中心给客户端返回 access_token

客户端模式(client credentials)

这种模式其实已经不太属于 OAuth2 的范畴了。A 服务完全脱离用户,以自己的身份去向 B 服务索取 token。换言之,用户无需具备 B 服务的使用权也可以。完全是 A 服务与 B 服务内部的交互,与用户无关了。适用于没有前端的命令行应用。

一般流程为:

  • 客户端向认证中心发送自己的身份信息,并请求 access_token
  • 确认客户端身份无误后,将 access_token 发送给客户端

适用场景

适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制

其中,授权码模式最为复杂,但整个认证和授权的过程也是最安全的一种,因此得到了许大中小各类互联网公司的广泛使用,本篇将基于授权码模式,探讨下如何基于springsecurity实现单点登录的流程

认证原理

下边分析一个Oauth2认证的例子,网站使用微信认证的过程:

1. 用户进入网站的登录页面,点击微信的图标以微信账号登录系统,用户是自己在微信里信息的资源拥有者;

点击“微信”出现一个二维码,此时用户扫描二维码,开始给网站授权

2. 资源拥有者同意给客户端授权

资源拥有者扫描二维码表示资源拥有者同意给客户端授权,微信会对资源拥有者的身份进行验证,验证通过后,微信会询问用户是否给授权网站访问自己的微信数据,用户点击“确认登录”表示同意授权,微信认证服务器会颁发一个授权码,并重定向到网站

3. 客户端获取到授权码,请求认证服务器申请令牌

此过程用户看不到,客户端应用程序请求认证服务器,请求携带授权码

4. 认证服务器向客户端响应令牌

认证服务器验证了客户端请求的授权码,如果合法则给客户端颁发令牌,令牌是客户端访问资源的通行证。此交互过程用户看不到,当客户端拿到令牌后,用户在网站看到已经登录成功

5. 客户端请求资源服务器的资源

客户端携带令牌访问资源服务器的资源。网站携带令牌请求访问微信服务器获取用户的基本信息

6. 资源服务器返回受保护资源

资源服务器校验令牌的合法性,如果合法则向用户响应资源信息内容

注意:
资源服务器和认证服务器可以是一个服务也可以分开的服务,如果是分开的服务资源服务
器通常要请求认证服务器来校验令牌的合法性

简而言之,Oauth2.0认证流程可总结如下:

补充说明几个关键术语:

1、客户端

本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:android客户端、Web客户端(浏览器端)、微信客户端等

2、资源拥有者

通常为用户,也可以是应用程序,即该资源的拥有者,比如进入A网站时需要通过微信二维码扫码登录,A网站要通过这种方式拿到用户信息,那么可理解微信即为资源拥有者

3、授权服务器(也称认证服务器)

用来对资源拥有的身份进行认证、对访问资源进行授权。客户端要想访问资源需要通过认证服务器由资源拥有者授权后方可访问

4、资源服务器

存储资源的服务器,比如,网站用户管理服务器存储了网站用户信息,网站相册服务器存储了用户的相册信息,微信的资源服务存储了微信的用户信息等。客户端最终访问资源服务器获取资源信息

5、SpringSecurity中的几种令牌类型

  • 授权码 :仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌
  • 访问令牌 :用于代表一个用户或服务直接去访问受保护的资源
  • 刷新令牌 :用于去授权服务器获取一个刷新访问令牌
  • BearerToken :不管谁拿到Token都可以访问资源,类似现金的作用
  • Proof of Possession(PoP) Token :可以校验client是否对Token有明确的拥有权

以上内容大致描述了下springsecurity oauth2.0下整个认证的过程,下面用一个演示程序来简要验证下完整的过程

前置准备:搭建一个springboot工程

1、引入核心依赖

   <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR1</spring-cloud.version>
        <lombok.version>1.18.6</lombok.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>$lombok.version</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>$spring-cloud.version</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

2、几个核心配置类

AuthorizationServerConfig 认证服务器配置类

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;

@Configuration
//开启oauth2,auth server模式
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter 

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception 
        clients.inMemory()
                //clientId 请求授权码时需要携带的参数,可自定义
                .withClient("tenant")
                //请求授权码时需要携带的参数,可自定义
                .secret(passwordEncoder.encode("112233"))
                //授权服务器返回授权码时跳转的地址,该地址为客户端地址,可自定义
                .redirectUris("http://www.baidu.com")
                //授权的作用域
                .scopes("all")
                //这里采用授权码形式,如果有多个,中间用逗号分割开
                //authorization_code授权码模式,这个是标准模式
                //implicit简单模式,这个主要是给无后台的纯前端项目用的
                //password密码模式,直接拿用户的账号密码授权,不安全
                //client_credentials客户端模式,用clientid和密码授权,和用户无关的授权方式
                .authorizedGrantTypes("authorization_code");
    


SecurityConfig web安全访问控制配置类,用于对资源请求做拦截过滤,或特定资源路径放行

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;

@Configuration
//@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter 

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

    @Override
    protected void configure(HttpSecurity http) throws Exception 
        http.authorizeRequests()
        		//允许这几个路径开头的url请求不需要认证
                .antMatchers("/oauth/**","/login/**","logout/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
                .csrf()
                .disable();
    


ResourceServerConfig 配置那些受保护的资源,即需要通过认证服务器统一认证的资源

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

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter 

    @Override
    public void configure(HttpSecurity http) throws Exception 
        http
                .authorizeRequests()
                .anyRequest()
                .authenticated()
                .and()
                .requestMatchers()
                //以 user 开头的资源请求受保护,必须走统一认证
                .antMatchers("/user/**");
    

3、自定义一个UserDetails

只需要实现UserDetails接口,该类的作用是,通过重写UserDetails接口,覆盖里面的方法,在自定义类中可以拿到用户凭证相关的信息,包括:用户名,密码,授权等信息

public class User implements UserDetails 

    private String username;
    private String password;

    private List<GrantedAuthority> authorities;

    public User(String username,String password,List<GrantedAuthority> authorities)
        this.username=username;
        this.password=password;
        this.authorities=authorities;
    

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() 
        return authorities;
    

    @Override
    public String getPassword() 
        return password;
    

    @Override
    public String getUsername() 
        return username;
    

    @Override
    public boolean isAccountNonExpired() 
        return true;
    

    @Override
    public boolean isAccountNonLocked() 
        return true;
    

    @Override
    public boolean isCredentialsNonExpired() 
        return true;
    

    @Override
    public boolean isEnabled() 
        return true;
    


4、自定义UserDetailsService

该类中的loadUserByUsername方法 是springsecurity的默认核心登录实现,可以在此方法中定制自己的业务

@Service
public class UserService implements UserDetailsService 

    static Map<String,String> userMap = new HashMap<>();

    static 
        userMap.put("admin","123456");
        userMap.put("zhangsan","zhangsan");
        userMap.put("test","test");
    

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 
        if(!StringUtils.isEmpty(username))
            if(!userMap.containsKey(username))
                return null;
            
        
        String password = passwordEncoder.encode(userMap.get(username));
        return new User(username,password,AuthorityUtils.commaSeparatedStringToAuthorityList(username));
    


5、编写2个controller接口,作为服务端的资源接口

UserController

@RestController
@RequestMapping("/user")
public class UserController 

    //localhost:7747/user/getResources
    @GetMapping("/getResources")
    public String getResources()
        return "getResources";
    

    @PostMapping("/getCurrentUser")
    public Object getCurrentUser(Authentication authentication)
        return authentication.getPrincipal();
    


GroupController

@RestController
@RequestMapping("/group")
public class GroupController 

    @GetMapping("/getGroupRes")
    public String getGroupRes()
        return "getGroupRes";
    


以上就是全部的代码逻辑,按照流程图中springsecurity 的授权码模式认证的完整流程,启动项目后,让我们来做一下测试吧

1、调用获取授权码接口

http://localhost:7747/oauth/authorize?response_type=code&client_id=tenant&redirect_uri=http://www.baidu.com&scope=all

浏览器输入上述url之后,跳转到如下页面

简单说明下,这个页面是springsecurity自身的统一认证登录入口,类似于cas默认的登录页面,即所有访问系统资源的请求均需要通过统一认证,走授权码这套流程的都要通过这个登录,用户名和密码输入我们程序中初始化的那几个(实际中使用数据库账号)

输入 test/test之后,来到下面的页面

意思是说,是否允许为 clientId 为 : tenant的这个客户端返回授权码,点击 Approve ,然后点击下面的授权按钮,跳转到下面的页面

这个跳转的地址即程序中配置的,实际开发中,这个地址需要由客户端给定,认证服务器将授权码一起重定向到这个url里面,因此重点关注这个 code

2、拿到授权码的code,再去请求令牌

请求接口:http://localhost:7747/oauth/token,这里使用postman工具进行请求,注意里面的几个重要的请求参数设置

  • 请求类型,选择:Basic Auth
  • 用户名和密码即程序中配置的: tenant / 112233

    请求参数这一栏,使用表单的方式,将如下几个参数悉数填入,code那里,即为第一步中返回的code

    请求之后,返回值中的access_token即为令牌

3、使用令牌,访问系统的接口资源

访问后端接口:http://localhost:7747/user/getCurrentUser

TYPE 那里选择Bearer Token ,参数Token的值即为上一步返回的那个token值,请求一下,得到如下返回结果

或者访问:http://localhost:7747/user/getResources 这个接口,这时候都可以拿到结果

从上面的步骤可以看到,我们最终请求后端受保护的 /user 这样的接口时,需要走一个完整的认证过程,假如不走这样的流程,我们直接请求后端接口是什么样的呢?重启下项目
再次访问userController的某个接口,效果如下,直接提醒权限不足

如果是访问: localhost:7747/group/getGroupRes呢?直接跳转到登录接口

登录之后即可以直接访问到后端的接口了

通过以上内容我们演示了基于springsecurity的一个完整的授权码模式的演示过程,这个过程其实和本文开头的案例,其底层的核心认证原理是一样的,也属于单点登录的扩展实现,只不过实际业务中,认证服务器,客户都安,甚至资源服务器并不在同一个工程中,而是各自部署的,甚至认证的页面也是自己开发的,但是掌握核心的原理之后,再移植到自己的系统中,就不是难事了

本篇到此结束,最后感谢观看!

以上是关于springsecurity 认证之授权码模式的主要内容,如果未能解决你的问题,请参考以下文章

Springboot2.0 + OAuth2.0之授权码模式

OAuth2.0 - 介绍与使用 及 授权码模式讲解

SpringSecurity---认证+授权代码实现

SpringSecurity---认证+授权代码实现

SpringSecurity - 整合JWT使用 Token 认证授权

第十一节:IdentityServer4授权码模式介绍和代码实操演练