Skip to content
标签
spring
鉴权
字数
12298 字
阅读时间
52 分钟

一、概述

认证系统独立、需要认证的系统通过http获取其他协议与认证系统通信完成认证、用户信息存储在单独的存储介质中。

OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不 需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向 后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服 务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

参考 Oauth协议

1.1 主要角色

OAauth2.0包括以下角色:

  1. 客户端 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。
  2. 资源拥有者 通常为用户,也可以是应用程序,即该资源的拥有者。
  3. 授权服务器(也称认证服务器) 用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌 (access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。
  4. 资源服务器 存储资源的服务器,本例子为微信存储的用户信息。 现在还有一个问题,服务提供商不能允许随便一个客户端就接入到它的授权服务器,服务提供商会 给准入的接入方一个身份,用于接入时的凭据:client_id:客户端标识 client_secret:客户端秘钥 因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者客户端

1.2 Spring-Security-OAuth2

Spring-Security-OAuth2是对OAuth2的一种实现,OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用 同一个授权服务的多个资源服务.

授权服务 **(Authorization Server)**应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌 的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:

AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。 TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。 资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴 权等,下面的过滤器用于实现 OAuth 2.0 资源服务:

OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

1.3 表结构说明

官方sql地址

oauth_client_details【核心表】

字段名字段说明
client_id主键,必须唯一,不能为空. 用于唯一标识每一个客户端(client); 在注册时必须填写(也可由服务 端自动生成). 对于不同的grant_type,该字段都是必须的. 在实际应用中的另一个名称叫 appKey,与client_id是同一个概念.
resource_ids客户端所能访问的资源id集合,多个资源时用逗号(,)分隔,如: “unity-resource,mobile- resource”. 该字段的值必须来源于与security.xml中标签‹oauth2:resource-server的属性 resource-id值一致. 在security.xml配置有几个‹oauth2:resource-server标签, 则该字段可以 使用几个该值. 在实际应用中, 我们一般将资源进行分类,并分别配置对应 的‹oauth2:resource-server,如订单资源配置一个‹oauth2:resource-server, 用户资源又配置 一个‹oauth2:resource-server. 当注册客户端时,根据实际需要可选择资源id,也可根据不同的 注册流程,赋予对应的资源id.
client_secret用于指定客户端(client)的访问密匙; 在注册时必须填写(也可由服务端自动生成). 对于不同的 grant_type,该字段都是必须的. 在实际应用中的另一个名称叫appSecret,与client_secret是 同一个概念.
scope指定客户端申请的权限范围,可选值包括read,write,trust;若有多个权限范围用逗号(,)分隔,如: “read,write”. scope的值与security.xml中配置的‹intercept-url的access属性有关系. 如‹intercept-url的配置为‹intercept-url pattern="/m/**" access=“ROLE_MOBILE,SCOPE_READ”/>则说明访问该URL时的客户端必须有read权限范 围. write的配置值为SCOPE_WRITE, trust的配置值为SCOPE_TRUST. 在实际应该中, 该值一 般由服务端指定, 常用的值为read,write.
authorized_grant_types指定客户端支持的grant_type,可选值包括 authorization_code,password,refresh_token,implicit,client_credentials, 若支持多个 grant_type用逗号(,)分隔,如: “authorization_code,password”. 在实际应用中,当注册时,该字 段是一般由服务器端指定的,而不是由申请者去选择的,最常用的grant_type组合有: “authorization_code,refresh_token”(针对通过浏览器访问的客户端); “password,refresh_token”(针对移动设备的客户端). implicit与client_credentials在实际中 很少使用.
web_server_redirect_uri客户端的重定向URI,可为空, 当grant_type为authorization_code或implicit时, 在Oauth的流 程中会使用并检查与注册时填写的redirect_uri是否一致. 下面分别说明:当 grant_type=authorization_code时, 第一步 从 spring-oauth-server获取 'code’时客户端发 起请求时必须有redirect_uri参数, 该参数的值必须与 web_server_redirect_uri的值一致. 第 二步 用 ‘code’ 换取 ‘access_token’ 时客户也必须传递相同的redirect_uri. 在实际应用中, web_server_redirect_uri在注册时是必须填写的, 一般用来处理服务器返回的code, 验证 state是否合法与通过code去换取access_token值.在spring-oauth-client项目中, 可具体参考 AuthorizationCodeController.java中的authorizationCodeCallback方法.当 grant_type=implicit时通过redirect_uri的hash值来传递access_token值. 如:http://localhost:7777/spring-oauth-client/implicit#access_token=dc891f4a-ac88- 4ba6-8224-a2497e013865&token_type=bearer&expires_in=43199然后客户端通过JS等从 hash值中取到access_token值.
authorities指定客户端所拥有的Spring Security的权限值,可选, 若有多个权限值,用逗号(,)分隔, 如: "ROLE_
access_token_validity设定客户端的access_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间 值(60 * 60 * 12, 12小时). 在服务端获取的access_token JSON数据中的expires_in字段的值 即为当前access_token的有效时间值. 在项目中, 可具体参考DefaultTokenServices.java中属 性accessTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端 自定义.refresh_token_validity 设定客户端的refresh_token的有效时间值(单位:秒),可选, 若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天). 若客户端的grant_type不包 括refresh_token,则不用关心该字段 在项目中, 可具体参考DefaultTokenServices.java中属 性refreshTokenValiditySeconds. 在实际应用中, 该值一般是由服务端处理的, 不需要客户端 自定义.
additional_information这是一个预留的字段,在Oauth的流程中没有实际的使用,可选,但若设置值,必须是JSON格式的 数据,如:{“country”:“CN”,“country_code”:“086”}按照spring-security-oauth项目中对该字段 的描述 Additional information for this client, not need by the vanilla OAuth protocol but might be useful, for example,for storing descriptive information. (详见 ClientDetails.java的getAdditionalInformation()方法的注释)在实际应用中, 可以用该字段来 存储关于客户端的一些其他信息,如客户端的国家,地区,注册时的IP地址等等.create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
archived用于标识客户端是否已存档(即实现逻辑删除),默认值为’0’(即未存档). 对该字段的具体使用请 参考CustomJdbcClientDetailsService.java,在该类中,扩展了在查询client_details的SQL加上 archived = 0条件 (扩展字段)
trusted设置客户端是否为受信任的,默认为’0’(即不受信任的,1为受信任的). 该字段只适用于 grant_type="authorization_code"的情况,当用户登录成功后,若该值为0,则会跳转到让用户 Approve的页面让用户同意授权, 若该字段为1,则在登录后不需要再让用户Approve同意授权 (因为是受信任的). 对该字段的具体使用请参考OauthUserApprovalHandler.java. (扩展字 段)
autoapprove设置用户是否自动Approval操作, 默认值为 ‘false’, 可选值包括 ‘true’,‘false’, ‘read’,‘write’. 该 字段只适用于grant_type="authorization_code"的情况,当用户登录成功后,若该值为’true’或 支持的scope值,则会跳过用户Approve的页面, 直接授权. 该字段与 trusted 有类似的功能, 是 spring-security-oauth2 的 2.0 版本后添加的新属性. 在项目中,主要操作 oauth_client_details表的类是JdbcClientDetailsService.java, 更多的细节请参考该类. 也可 以根据实际的需要,去扩展或修改该类的实现.

oauth_client_token

字段名字段说明
create_time数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
token_id从服务器端获取到的access_token的值.
token这是一个二进制的字段, 存储的数据是OAuth2AccessToken.java对象序列化后的二进制数据.
authentication_id该字段具有唯一性, 是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultClientKeyGenerator.java类.
user_name登录时的用户名
client_id

该表用于在客户端系统中存储从服务端获取的token数据, 在spring-oauth-server项目中未使用到. 对oauth_client_token表的主要操作在JdbcClientTokenServices.java类中, 更多的细节请参考该类.

oauth_access_token

字段名字段说明
create_time数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
token_id该字段的值是将access_token的值通过MD5加密后存储的.
token存储将OAuth2AccessToken.java对象序列化后的二进制数据, 是真实的AccessToken的数据值.
authentication_id该字段具有唯一性, 其值是根据当前的username(如果有),client_id与scope通过MD5加密生成的. 具体实现请参考DefaultAuthenticationKeyGenerator.java类.
user_name登录时的用户名, 若客户端没有用户名(如grant_type=“client_credentials”),则该值等于client_id
client_id
authentication存储将OAuth2Authentication.java对象序列化后的二进制数据.
refresh_token该字段的值是将refresh_token的值通过MD5加密后存储的. 在项目中,主要操作oauth_access_token表的对象是JdbcTokenStore.java. 更多的细节请参考该类.

oauth_refresh_token

字段名字段说明
create_time数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
token_id该字段的值是将refresh_token的值通过MD5加密后存储的.
token存储将OAuth2RefreshToken.java对象序列化后的二进制数据.
authentication存储将OAuth2Authentication.java对象序列化后的二进制数据

在项目中,主要操作oauth_refresh_token表的对象是JdbcTokenStore.java. (与操作oauth_access_token表的对象一样);更多的细节请参考该类. 如果客户端的grant_type不支持refresh_token,则不会使用该表.

oauth_code

字段名字段说明
create_time数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
code存储服务端系统生成的code的值(未加密).
authentication存储将AuthorizationRequestHolder.java对象序列化后的二进制数据.

在项目中,主要操作oauth_code表的对象是JdbcAuthorizationCodeServices.java. 更多的细节请参考该类。 只有当grant_type为"authorization_code"时,该表中才会有数据产生; 其他的grant_type没有使用该表。

1.4 认证流程

  1. 前端返送认证请求到认证系统,认证成功后将令牌存储在redis中并返回令牌
  2. 前端应用访问需要认证的资源时携带令牌到服务网关,网关会对令牌的合法性进行校验

1.5 授权模式

授权码模式(Authorization Code) 隐式授权模式(Implicit) 密码模式(Resource Owner Password Credentials) 客户端模式(Client Credentials) 授权码模式与密码模式应用较多

二、使用示例

2.1 入门demo

授权码模式

授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。 授权流程 1、客户端请求认证服务授权 2、用户(资源拥有者)同意给客户端授权 3、客户端获取到授权码,请求认证服务器申请令牌 4、认证服务器向客户端响应令牌 5、客户端请求资源服务器的资源,资源服务校验令牌合法性,完成授权 6、资源服务器返回受保护资源

申请授权码

sh
localhost:40400/auth/oauth/authorize?client_id=客户端id&response_type=code(固定)&scop=客户端范围,与授权配置中设置的一致&redirect_uri=授权码申请成功调转的地址

申请令牌

sh
Post请求:http://认证地址/auth/oauth/token

参数信息:
grant_type:授权类型,authorization_code(表示授权码模式)
code:授权码,一个授权码只能使用一次
redirect_uri:申请授权码时的跳转url,与申请授权码时用的redirect_uri一致。
还需在请求头header中携带客户端id和客户端密码拼接后的base64编码、
这是http协议定义的一种认证方式。http Basic认证

客户端id和密码匹配数据库中的客户端id和客户端密码。

申请成功返回数据:

access_token:访问令牌,携带此令牌访问资源
token_type:有MAC Token与Bearer Token两种类型,两种的校验算法不同,RFC 6750建议Oauth2采用 Bearer Token(http://www.rfcreader.com/#rfc6750)。
refresh_token:刷新令牌,使用此令牌可以延长访问令牌的过期时间。
expires_in:过期时间,单位为秒。
scope:范围,与定义的客户端范围一致。

资源服务授权授权流程

  1. 客户端请求认证服务申请令牌
  2. 认证服务生成令牌 认证服务采用非对称加密算法,使用私钥生成令牌。
  3. 客户端携带令牌访问资源服务 客户端在Http header 中添加: Authorization:Bearer 令牌。
  4. 资源服务请求认证服务校验令牌的有效性 资源服务接收到令牌,使用公钥校验令牌的合法性。
  5. 令牌有效,资源服务向客户端响应资源信息

服务授权配置 配置公钥 认证服务生成令牌采用非对称加密算法,认证服务采用私钥加密生成令牌,对外向资源服务提供公钥,资源服务使用公钥 来校验令牌的合法性。

代码: 依赖

xml
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring‐cloud‐starter‐oauth2</artifactId>
</dependency>

配置类

java
@Configuration
@EnableResourceServer
//可以用 @EnableAuthorizationServer 注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权 服务器。
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)//激活方法上的
PreAuthorize注解
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
	//公钥
	private static final String PUBLIC_KEY = "publickey.txt";
	//定义JwtTokenStore,使用jwt令牌
	@Bean
	public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
		return new JwtTokenStore(jwtAccessTokenConverter);
	}
	//定义JJwtAccessTokenConverter,使用jwt令牌
	@Bean
	public JwtAccessTokenConverter jwtAccessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setVerifierKey(getPubKey());
		return converter;
	}
	/**
	* 获取非对称加密公钥 Key
	* @return 公钥 Key
	*/
	private String getPubKey() {
		Resource resource = new ClassPathResource(PUBLIC_KEY);
		try {
			InputStreamReader inputStreamReader = new
			InputStreamReader(resource.getInputStream());
			BufferedReader br = new BufferedReader(inputStreamReader);
			return br.lines().collect(Collectors.joining("\n"));
		} catch (IOException ioe) {
			return null;
		}
	}
	//Http安全配置,对每个到达系统的http请求链接进行校验
	@Override
	public void configure(HttpSecurity http) throws Exception {
	//所有请求必须认证通过
		http.authorizeRequests()
            //路径放行 放行swagger(无法单元测试)
		.antMatchers("/v2/api‐docs", 
                     "/swagger‐resources/configuration/ui",
                     "/swagger‐resources",
                     "/swagger‐resources/configuration/security",
                     "/swagger‐ui.html",
                     "/webjars/**")
            .permitAll()
            .anyRequest().authenticated();
	}
}

密码授权模式

密码模式(Resource Owner Password Credentials)与授权码模式的区别是申请令牌不再使用授权码,而是直接通过用户名和密码即可申请令牌。其余相同

sh
Post请求:http://地址/auth/oauth/token

参数:
grant_type:密码模式授权填写password
username:账号
password:密码
也需要http Basic认证
返回同授权码模式
当令牌没有过期时同一个用户再次申请不会再颁发新令牌

校验令牌

sh
Get: http://地址/auth/oauth/check_token?token=
参数:
token:令牌

返回结果
{
	"user_name": 用户名,
	"scope": [ 客户端范围
		"app"
	],
	"exp": 1531254828,   //过期时间
	"jti": 与令牌对应的唯一标识,
	"client_id": 客户端Id
}

刷新令牌

刷新令牌是当令牌快过期时重新生成一个令牌,它于授权码授权和密码授权生成令牌不同,刷新令牌不需要授权码也不需要账号和密码,只需要一个刷新令牌、客户端id和客户端密码。

sh
Post:http://地址/auth/oauth/token
参数:
grant_type: 固定为 refresh_token
refresh_token:刷新令牌

返回结果为新的令牌

2.2 springboot集成

资源模块

依赖

xml
<dependencies>
    <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>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
        <version>2.1.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
</dependencies>

配置类 将资源交给OAuth2来管理,使用通行的token来访问资源

java
package com.itheima.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableResourceServer
public class OauthSourceConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private DataSource dataSource;

    /**
     * 指定token的持久化策略
     * InMemoryTokenStore表示将token存储在内存
     * Redis表示将token存储在redis中
     * JdbcTokenStore存储在数据库中
     * @return
     */
    @Bean
    public TokenStore jdbcTokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    /**
     * 指定当前资源的id和存储方案
     * @param resources
     * @throws Exception
     */
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("product_api").tokenStore(jdbcTokenStore());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception{
        http.authorizeRequests()
                //指定不同请求方式访问资源所需要的权限,一般查询是read,其余是write。
                .antMatchers(HttpMethod.GET, "/**").access("#oauth2.hasScope('read')")
                .antMatchers(HttpMethod.POST, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PATCH, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.PUT, "/**").access("#oauth2.hasScope('write')")
                .antMatchers(HttpMethod.DELETE, "/**").access("#oauth2.hasScope('write')")
                .and()
                .headers().addHeaderWriter((request, response) -> {
            response.addHeader("Access-Control-Allow-Origin", "*");//允许跨域
            if (request.getMethod().equals("OPTIONS")) {//如果是跨域的预检请求,则原封不动向下传达请求头信息
                response.setHeader("Access-Control-Allow-Methods", request.getHeader("Access-Control-Request-Method"));
                response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
            }
        });
    }


}

授权模块

依赖

xml
<dependencies>
    <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>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
        <version>2.1.0.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>
</dependencies>

配置

yaml
server:
  port: 9001
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///security_authority
    username: root
    password: root
  main:
    allow-bean-definition-overriding: true # 这个表示允许我们覆盖OAuth2放在容器中的bean对象,一定要配置
mybatis:
  type-aliases-package: com.itheima.domain
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    com.itheima: debug

配置类

java

import com.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

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

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginProcessingUrl("/login")
                .permitAll()
                .and()
                .csrf()
                .disable();
    }

    //AuthenticationManager对象在OAuth2认证服务中要使用,提前放入IOC容器中
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

}

授权配置类

java

import com.itheima.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;

import javax.sql.DataSource;

@Configuration
@EnableAuthorizationServer
public class OauthServerConfig extends AuthorizationServerConfigurerAdapter {

    //数据库连接池对象
    @Autowired
    private DataSource dataSource;

    //认证业务对象
    @Autowired
    private UserService userService;

    //授权模式专用对象
    @Autowired
    private AuthenticationManager authenticationManager;

    //客户端信息来源
    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService(){
        return new JdbcClientDetailsService(dataSource);
    }

    //token保存策略
    @Bean
    public TokenStore tokenStore(){
        return new JdbcTokenStore(dataSource);
    }

    //授权信息保存策略
    @Bean
    public ApprovalStore approvalStore(){
        return new JdbcApprovalStore(dataSource);
    }

    //授权码模式数据来源
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(){
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    //指定客户端信息的数据库来源
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(jdbcClientDetailsService());
    }

    //检查token的策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.allowFormAuthenticationForClients();
        security.checkTokenAccess("isAuthenticated()");
    }

    //OAuth2的主配置信息
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .approvalStore(approvalStore())
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices())
                .tokenStore(tokenStore());
    }

}

三、核心知识

3.1 授权服务器配置

EnableAuthorizationServer

可以用 @EnableAuthorizationServer 注解并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权 服务器

java
@Configuration 
@EnableAuthorizationServer 
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter { 
    //略...
}

AuthorizationServerConfigurerAdapter要求配置以下几个类,这几个类是由Spring创建的独立的配置对象,它们 会被Spring传入AuthorizationServerConfigurer中进行配置。

java
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
    public AuthorizationServerConfigurerAdapter() {}
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {} 
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {} 
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {} 
}

ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。 AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。 AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.

配置客户端详细信息

ClientDetailsServiceConfigurer 能够使用内存或者JDBC来实现客户端详情服务(ClientDetailsService), ClientDetailsService负责查找ClientDetails,而ClientDetails有几个重要的属性如下列表

clientId:(必须的)用来标识客户的Id。 secret:(需要值得信任的客户端)客户端安全码,如果有的话。 scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。 authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。 authorities:此客户端可以使用的权限(基于Spring Security authorities)。

客户端详情(Client Details)能够在应用程序运行的时候进行更新,可以通过访问底层的存储服务(例如将客户 端详情存储在一个关系数据库的表中,就可以使用 JdbcClientDetailsService)或者通过自己实现 ClientRegistrationService接口(同时你也可以实现 ClientDetailsService 接口)来进行管理。

使用内存方式存储客户端详情信息:

java
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    // clients.withClientDetails(clientDetailsService); 
    clients.inMemory()// 使用in‐memory存储 
        .withClient("c1")// client_id 
        .secret(new BCryptPasswordEncoder().encode("secret")) .resourceIds("res1") 
        .authorizedGrantTypes("authorization_code", "password","client_credentials","implicit","refresh_token")// 该client允许的授权类型 authorization_code,password,refresh_token,implicit,client_credentials 
        .scopes("all")// 允许的授权范围 
        .autoApprove(false) //加上验证回调地址 
        .redirectUris("http://www.baidu.com");
}

管理令牌

AuthorizationServerTokenServices 接口定义了一些操作使得你可以对令牌进行一些必要的管理,令牌可以被用来 加载身份信息,里面包含了这个令牌的相关权限。

可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类, 里面包含了一些有用实现,你可以使用它来修改令牌的格式和令牌的存储。默认的,当它尝试创建一个令牌的时 候,是使用随机值来进行填充的,除了持久化令牌是委托一个 TokenStore 接口来实现以外,这个类几乎帮你做了 所有的事情。并且 TokenStore 这个接口有一个默认的实现,它就是 InMemoryTokenStore ,如其命名,所有的 令牌是被保存在了内存中。除了使用这个类以外,你还可以使用一些其他的预定义实现,下面有几个版本,它们都 实现了TokenStore接口:

InMemoryTokenStore:这个版本的实现是被默认采用的,它可以完美的工作在单服务器上(即访问并发量 压力不大的情况下,并且它在失败的时候不会进行备份),大多数的项目都可以使用这个版本的实现来进行 尝试,你可以在开发的时候使用它来进行管理,因为不会被保存到磁盘中,所以更易于调试。

JdbcTokenStore:这是一个基于JDBC的实现版本,令牌会被保存进关系型数据库。使用这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使用这个版本的时候请注意把"spring-jdbc"这个依赖加入到你的 classpath当中。

JwtTokenStore:这个版本的全称是 JSON Web Token(JWT),它可以把令牌相关的数据进行编码(因此对 于后端服务来说,它不需要进行存储,这将是一个重大优势),但是它有一个缺点,那就是撤销一个已经授 权令牌将会非常困难,所以它通常用来处理一个生命周期较短的令牌以及撤销刷新令牌(refresh_token)。 另外一个缺点就是这个令牌占用的空间会比较大,如果你加入了比较多用户凭证信息。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。

1、定义TokenConfig 在config包下定义TokenConfig,我们暂时先使用InMemoryTokenStore,生成一个普通的令牌。

java
@Configuration
public class TokenConfig { 
    @Bean 
    public TokenStore tokenStore() { 
        return new InMemoryTokenStore(); 
    } 
}

2、定义AuthorizationServerTokenServices 在AuthorizationServer中定义AuthorizationServerTokenServices

java
@Autowired 
private TokenStore tokenStore; 
@Autowired 
private ClientDetailsService clientDetailsService;
@Bean 
public AuthorizationServerTokenServices tokenService() { 
    DefaultTokenServices service=new DefaultTokenServices(); 
    service.setClientDetailsService(clientDetailsService);
    service.setSupportRefreshToken(true);
    service.setTokenStore(tokenStore); 
    service.setAccessTokenValiditySeconds(7200);// 令牌默认有效期2小时 
    service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
    return service;
}

令牌访问端点配置

AuthorizationServerEndpointsConfifigurer 这个对象的实例可以完成令牌服务以及令牌endpoint配置。

配置授权类型(Grant Types) AuthorizationServerEndpointsConfigurer 通过设定以下属性决定支持的授权类型(Grant Types): authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置 这个属性注入一个 AuthenticationManager 对象。 userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对 象),当你设置了这个之后,那么 "refresh_token" 即刷新令牌授权类型模式的流程中就会包含一个检查,用 来确保这个账号是否仍然有效,假如说你禁用了这个账户的话。 authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 "authorization_code" 授权码类型模式。 implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。 tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并 且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的 需求的时候,才会考虑使用这个。

配置授权端点的URL(Endpoint URLs) AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点URL链 接,它有两个参数: 第一个参数:String 类型的,这个端点URL的默认链接。 第二个参数:String 类型的,你要进行替代的URL链接。 以上的参数都将以 "/" 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的 第一个参数: /oauth/authorize:授权端点。 /oauth/token:令牌端点。 /oauth/confirm_access:用户确认授权提交端点。 /oauth/error:授权服务错误信息端点。 /oauth/check_token:用于资源服务访问的令牌解析端点。 /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。 需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问.

在AuthorizationServer配置令牌访问端点

java
@Autowired 
private AuthorizationCodeServices authorizationCodeServices;
@Autowired 
private AuthenticationManager authenticationManager; 
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) { 
    endpoints 
        .authenticationManager(authenticationManager) 
        .authorizationCodeServices(authorizationCodeServices) 
        .tokenServices(tokenService()) 
        .allowedTokenEndpointRequestMethods(HttpMethod.POST);
}
@Bean 
public AuthorizationCodeServices authorizationCodeServices() { 
    //设置授权码模式的授权码如何 存取,暂时采用内存方式 
    return new InMemoryAuthorizationCodeServices();
}

令牌端点的安全约束

AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束

java
@Override 
public void configure(AuthorizationServerSecurityConfigurer security){ 
    security .tokenKeyAccess("permitAll()")// (1)
        .checkTokenAccess("permitAll()")//(2) 
        .allowFormAuthenticationForClients()// (3)
        ;
}

(1)tokenkey这个endpoint当使用JwtToken且使用非对称加密时,资源服务用于获取公钥而开放的,这里指这个 endpoint完全公开。 (2)checkToken这个endpoint完全公开 (3) 允许表单认证

web安全配置

java
@Configuration 
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true) 
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    @Bean 
    public PasswordEncoder passwordEncoder() { 
        return new BCryptPasswordEncoder(); 
    }
    @Bean 
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean(); 
    }
    //安全拦截机制(最重要) 
    @Override 
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable() 
            .authorizeRequests() 
            .antMatchers("/r/r1").hasAnyAuthority("p1") 
            .antMatchers("/login*").permitAll() 
            .anyRequest().authenticated()
            .and() 
            .formLogin() 
            ;
    } 
}

3.2 授权码模式

授权码模式是OAuth2中最安全最完善的一种模式,应用场景最广泛,可以实现服务之间的调用,常见的微信,QQ等第三方登录也可采用这种方式实现。

oauth授权码模式

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:

sh
/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
client_id:客户端准入标识。 
response_type:授权码模式固定为code。 
scope:客户端权限。
redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。 (3)授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。 (4)客户端拿着授权码向授权服务器索要访问access_token,请求如下:

sh
/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://w ww.baidu.com
client_id:客户端准入标识。 
client_secret:客户端秘钥。 
grant_type:授权类型,填写authorization_code,表示授权码模式 
code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。 
redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

(5)授权服务器返回令牌(access_token) 这种模式是四种模式中最安全的一种模式。一般用于client是Web服务器端应用或第三方的原生App调用资源服务 的时候。因为在这种模式中access_token不会经过浏览器或移动端的App,而是直接从服务端去交换,这样就最大 限度的减小了令牌泄漏的风险

3.3 简化模式

适用于A服务没有服务器的情况。比如:纯手机小程序,JavaScript语言实现的网页插件等。

oauth简化模式

(1)资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会 附加客户端的身份信息。如:

sh
/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

同授权码模式

(2)浏览器出现向授权服务器授权页面,之后将用户同意授权。 (3)授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览 器。注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头, 其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要 知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。 一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码

3.4 密码模式

此种模式虽然简单,但是用户将B服务的用户名和密码暴露给了A服务,需要两个服务信任度非常高才能使用。

oauth密码模式

(1)资源拥有者将用户名、密码发送给客户端 (2)客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:

sh
/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123
client_id:客户端准入标识。 
client_secret:客户端秘钥。 
grant_type:授权类型,填写password表示密码模式 
username:资源拥有者用户名。
password:资源拥有者密码。

(3)授权服务器将令牌(access_token)发送给client 这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,因此这就说明这种模式只能用于client是我 们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

3.5 客户端模式

A服务本身需要B服务资源,与用户无关

oauth客户端模式 (1)客户端向授权服务器发送自己的身份信息,并请求令牌(access_token) (2)确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:

sh
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials
client_id:客户端准入标识。
client_secret:客户端秘钥。
grant_type:授权类型,填写client_credentials表示客户端模式

这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因 此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

3.6 资源服务器配置

@EnableResourceServer 注解到一个 @Confifiguration 配置类上,并且必须使用 ResourceServerConfifigurer 这个 配置对象来进行配置(可以选择继承自 ResourceServerConfifigurerAdapter 然后覆写其中的方法,参数就是这个 对象的实例),下面是一些可以配置的属性:

ResourceServerSecurityConfifigurer中主要包括:

  • tokenServices:ResourceServerTokenServices 类的实例,用来实现令牌服务。
  • tokenStore:TokenStore类的实例,指定令牌如何访问,与tokenServices配置可选
  • resourceId:这个资源服务的ID,这个属性是可选的,但是推荐设置并在授权服务中进行验证。
  • 其他的拓展属性例如 tokenExtractor 令牌提取器用来提取请求中的令牌。

HttpSecurity配置这个与Spring Security类似:

  • 请求匹配器,用来设置需要进行保护的资源路径,默认的情况下是保护资源服务的全部路径。
  • 通过http.authorizeRequests()来设置受保护资源的访问规则
  • 其他的自定义权限保护规则通过 HttpSecurity 来进行配置。

@EnableResourceServer 注解自动增加了一个类型为 OAuth2AuthenticationProcessingFilter 的过滤器链 编写ResouceServerConfifig

java
package com.itheima.security.distributed.order.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
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.TokenStore;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableResourceServer
public class ResouceServerConfig extends ResourceServerConfigurerAdapter {


    public static final String RESOURCE_ID = "res1";

    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID)//资源 id
                .tokenStore(tokenStore)
//                .tokenServices(tokenService())//验证令牌的服务
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('ROLE_ADMIN')")
                .and().csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }

    //资源服务令牌解析服务
 /*   @Bean
    public ResourceServerTokenServices tokenService() {
        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
        RemoteTokenServices service=new RemoteTokenServices();
        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
        service.setClientId("c1");
        service.setClientSecret("secret");
        return service;
    }*/

}

验证token

ResourceServerTokenServices 是组成授权服务的另一半。令牌解析方法: 使用 DefaultTokenServices 在资源服务器本地配置令牌存储、解码、解析方式 使用 RemoteTokenServices 资源服务器通过 HTTP 请求来解码令牌,每次都请求授权服务器端点 /oauth/check_token 使用授权服务的 /oauth/check_token 端点你需要在授权服务将这个端点暴露出去,以便资源服务可以进行访问,

java
@Override 
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { 
    security .tokenKeyAccess("permitAll()")// /oauth/token_key 安全配置 
        .checkTokenAccess("permitAll()") // /oauth/check_token 安全配置
}

在资源 服务配置RemoteTokenServices ,在ResouceServerConfig中配置:

java
//资源服务令牌解析服务 
@Bean 
public ResourceServerTokenServices tokenService() { 
    //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
    RemoteTokenServices service=new RemoteTokenServices(); 
    service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
    service.setClientId("c1"); 
    service.setClientSecret("secret");
    return service; 
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
    resources.resourceId(RESOURCE_ID) 
        .tokenServices(tokenService()) 
        .stateless(true);
}

配置使用

java
@RestController
public class OrderController { 
    @GetMapping(value = "/r1") 
    @PreAuthorize("hasAnyAuthority('p1')") 
    public String r1(){ return "访问资源1"; } 
}

添加安全访问限制

java
package com.itheima.security.distributed.order.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {


    //安全拦截机制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
//                .antMatchers("/r/r1").hasAuthority("p2")
//                .antMatchers("/r/r2").hasAuthority("p2")
                .antMatchers("/r/**").authenticated()//所有/r/**的请求必须认证通过
                .anyRequest().permitAll()//除了/r/**,其它的请求可以访问
        ;


    }
}

3.7 集成JWT

配置类

java
@Configuration
public class TokenConfig { 
    private String SIGNING_KEY = "uaa123"; 
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter()); 
    }
    @Bean 
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); 
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证 
        return converter; 
    } 
}

jwt令牌服务类

java
@Autowired 
private JwtAccessTokenConverter accessTokenConverter; 
@Bean 
public AuthorizationServerTokenServices tokenService() { 
    DefaultTokenServices service=new DefaultTokenServices(); 
    service.setClientDetailsService(clientDetailsService);
    service.setSupportRefreshToken(true);
    service.setTokenStore(tokenStore);
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); 
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter)); 
    service.setTokenEnhancer(tokenEnhancerChain);
    service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时 
    service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天 
    return service;
}

校验jwt 资源服务需要和授权服务拥有一致的签字、令牌服务等:

1、将授权服务中的TokenConfifig类拷贝到资源 服务中 2、屏蔽资源 服务原来的令牌服务类

java
@Configuration 
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class ResouceServerConfig extends ResourceServerConfigurerAdapter { 
    public static final String RESOURCE_ID = "res1";
    @Autowired 
    TokenStore tokenStore; 
    //资源服务令牌解析服务 
    // @Bean 
    // public ResourceServerTokenServices tokenService() { 
    // //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
    // RemoteTokenServices service=new RemoteTokenServices(); 
    // service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
    // service.setClientId("c1"); 
    // service.setClientSecret("secret"); 
    // return service; 
    // }
    @Override 
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.resourceId(RESOURCE_ID) 
            .tokenStore(tokenStore)
            .stateless(true);
    }

令牌申请成功可以使用/uaa/oauth/check_token校验令牌的有效性,并查询令牌的内容

3.8 网关验证

认证服务器生成jwt令牌, 所有请求统一在网关层验证,判断权限等操作

API网关在认证授权体系里主要负责两件事: (1)作为OAuth2.0的资源服务器角色,实现接入方权限拦截。 (2)令牌解析并转发当前登录用户信息(明文token)给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事: (1)用户授权拦截(看当前用户是否有权访问该资源) (2)将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

代码实现

依赖

xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
    <groupId>com.netflix.hystrix</groupId>
    <artifactId>hystrix-javanica</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

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

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

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

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

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-jwt</artifactId>
</dependency>

<dependency>
    <groupId>javax.interceptor</groupId>
    <artifactId>javax.interceptor-api</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

配置

properties
spring.application.name=gateway-server
server.port=53010
spring.main.allow-bean-definition-overriding = true

logging.level.root = info
logging.level.org.springframework = info

zuul.retryable = true
zuul.ignoredServices = *
zuul.add-host-header = true
zuul.sensitiveHeaders = *

zuul.routes.uaa-service.stripPrefix = false
zuul.routes.uaa-service.path = /uaa/**

zuul.routes.order-service.stripPrefix = false
zuul.routes.order-service.path = /order/**

eureka.client.serviceUrl.defaultZone = http://localhost:53000/eureka/
eureka.instance.preferIpAddress = true
eureka.instance.instance-id = ${spring.application.name}:${spring.cloud.client.ip-address}:${spring.application.instance_id:${server.port}}
management.endpoints.web.exposure.include = refresh,health,info,env

feign.hystrix.enabled = true
feign.compression.request.enabled = true
feign.compression.request.mime-types[0] = text/xml
feign.compression.request.mime-types[1] = application/xml
feign.compression.request.mime-types[2] = application/json
feign.compression.request.min-request-size = 2048
feign.compression.response.enabled = true

启动类

java

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

/**
 * @author Administrator
 * @version 1.0
 **/
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class GatewayServer {

    public static void main(String[] args) {
        SpringApplication.run(GatewayServer.class, args);
    }
}

token配置

java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        //JWT令牌存储方案
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

   /* @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
        return new InMemoryTokenStore();
    }*/
}

配置资源服务 UAAServerConfig指定了若请求匹配/uaa/网关不进行拦截。 OrderServerConfig指定了若请求匹配/order/,也就是访问统一用户服务,接入客户端需要有scope中包含 read,并且authorities(权限)中需要包含ROLE_USER。 由于res1这个接入客户端,read包括ROLE_ADMIN,ROLE_USER,ROLE_API三个权限。

java

import org.springframework.beans.factory.annotation.Autowired;
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;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class ResouceServerConfig  {

    public static final String RESOURCE_ID = "res1";


    //uaa资源服务配置
    @Configuration
    @EnableResourceServer
    public class UAAServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;

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

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                 .antMatchers("/uaa/**").permitAll();
        }
    }


    //order资源
    //uaa资源服务配置
    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private TokenStore tokenStore;

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

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_API')");
        }
    }


    //配置其它的资源服务..


}

安全配置

java

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .authorizeRequests()
                .antMatchers("/**").permitAll()
                .and().csrf().disable();
    }
}

转发明文token给微服务 实现Zuul前置过滤器,完成当前登录用户信息提取,并放入转发微服务的request中

java

import com.alibaba.fastjson.JSON;
import com.itheima.security.distributed.gateway.common.EncryptUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author Administrator
 * @version 1.0
 **/
public class AuthFilter extends ZuulFilter {

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 0;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        //从安全上下文中拿 到用户身份对象
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if(!(authentication instanceof OAuth2Authentication)){
            return null;
        }
        OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) authentication;
        Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
        //取出用户身份信息
        String principal = userAuthentication.getName();

        //取出用户权限
        List<String> authorities = new ArrayList<>();
        //从userAuthentication取出权限,放在authorities
        userAuthentication.getAuthorities().stream().forEach(c->authorities.add(((GrantedAuthority) c).getAuthority()));

        OAuth2Request oAuth2Request = oAuth2Authentication.getOAuth2Request();
        Map<String, String> requestParameters = oAuth2Request.getRequestParameters();
        Map<String,Object> jsonToken = new HashMap<>(requestParameters);
        if(userAuthentication!=null){
            jsonToken.put("principal",principal);
            jsonToken.put("authorities",authorities);
        }

        //把身份信息和权限信息放在json中,加入http的header中,转发给微服务
        ctx.addZuulRequestHeader("json-token", EncryptUtil.encodeUTF8StringBase64(JSON.toJSONString(jsonToken)));

        return null;
    }
}

配置AuthFilter 将filter纳入spring 容器

java

import com.itheima.security.distributed.gateway.filter.AuthFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

/**
 * @author Administrator
 * @version 1.0
 **/
@Configuration
public class ZuulConfig {

    @Bean
    public AuthFilter preFileter() {
        return new AuthFilter();
    }

    @Bean
    public FilterRegistrationBean corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.addAllowedMethod("*");
        config.setMaxAge(18000L);
        source.registerCorsConfiguration("/**", config);
        CorsFilter corsFilter = new CorsFilter(source);
        FilterRegistrationBean bean = new FilterRegistrationBean(corsFilter);
        bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
        return bean;
    }
}

鉴权拦截

java

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.itheima.security.distributed.order.common.EncryptUtil;
import com.itheima.security.distributed.order.model.UserDTO;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author Administrator
 * @version 1.0
 **/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            //解析出头中的token
        String token = httpServletRequest.getHeader("json-token");
        if(token!=null){
            String json = EncryptUtil.decodeUTF8StringBase64(token);
            //将token转成json对象
            JSONObject jsonObject = JSON.parseObject(json);
            //用户身份信息
//            UserDTO userDTO = new UserDTO();
//            String principal = jsonObject.getString("principal");
//            userDTO.setUsername(principal);
            UserDTO userDTO = JSON.parseObject(jsonObject.getString("principal"), UserDTO.class);
            //用户权限
            JSONArray authoritiesArray = jsonObject.getJSONArray("authorities");
            String[] authorities = authoritiesArray.toArray(new String[authoritiesArray.size()]);
            //将用户信息和权限填充 到用户身份token对象中
            UsernamePasswordAuthenticationToken authenticationToken
                    = new UsernamePasswordAuthenticationToken(userDTO,null, AuthorityUtils.createAuthorityList(authorities));
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
            //将authenticationToken填充到安全上下文
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);


        }
        filterChain.doFilter(httpServletRequest,httpServletResponse);

    }
}

资源 服务中就可以方便到的获取用户的身份信息

java
UserDTO user = (UserDTO) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

1.解析token 2.新建并填充authentication 3.将authentication保存进安全上下文 剩下的事儿就交给Spring Security好了。

3.9 拓展用户信息

在认证阶段DaoAuthenticationProvider会调用UserDetailService查询用户的信息,这里是可以获取到齐全的用户 信息的。由于JWT令牌中用户身份信息来源于UserDetails,UserDetails中仅定义了username为用户的身份信息, 这里有两个思路:第一是可以扩展UserDetails,使之包括更多的自定义属性,第二也可以扩展username的内容 ,比如存入json数据内容作为username的内容。相比较而言,方案二比较简单还不用破坏UserDetails的结构

修改UserDetailService

java
package com.itheima.security.distributed.uaa.service;

import com.alibaba.fastjson.JSON;
import com.itheima.security.distributed.uaa.dao.UserDao;
import com.itheima.security.distributed.uaa.model.UserDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author Administrator
 * @version 1.0
 **/
@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    //根据 账号查询用户信息
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //将来连接数据库根据账号查询用户信息
        UserDto userDto = userDao.getUserByUsername(username);
        if(userDto == null){
            //如果用户查不到,返回null,由provider来抛出异常
            return null;
        }
        //根据用户的id查询用户的权限
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        //将permissions转成数组
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        //将userDto转成json
        String principal = JSON.toJSONString(userDto);
        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}

修改资源服务器过滤器

java
if (token != null){ 
    //1.解析token 
    String json = EncryptUtil.decodeUTF8StringBase64(token);
    JSONObject userJson = JSON.parseObject(json);
    //取出用户身份信息 
    String principal = userJson.getString("principal");
    //将json转成对象 
    UserDTO userDTO = JSON.parseObject(principal, UserDTO.class); 
    JSONArray authoritiesArray = userJson.getJSONArray("authorities");

3.10 分布式认证方案

基于token的认证方式有以下优点:

1、适合统一认证的机制,客户端、一方应用、三方应用都遵循一致的认证机制。 2、token认证方式对第三方应用接入更适合,因为它更开放,可使用当前有流行的开放协议Oauth2.0、JWT等。 3、一般情况服务端无需存储会话信息,减轻了服务端的压力。

分布式系统认证技术方案分布式认证

流程描述: (1)用户通过接入方(应用)登录,接入方采取OAuth2.0方式在统一认证服务(UAA)中认证。 (2)认证服务(UAA)调用验证该用户的身份是否合法,并获取用户权限信息。 (3)认证服务(UAA)获取接入方权限信息,并验证接入方是否合法。 (4)若登录用户以及接入方都合法,认证服务生成jwt令牌返回给接入方,其中jwt中包含了用户权限及接入方权 限。 (5)后续,接入方携带jwt令牌对API网关内的微服务资源进行访问。 (6)API网关对令牌解析、并验证接入方的权限是否能够访问本次请求的微服务。 (7)如果接入方的权限没问题,API网关将原请求header中附加解析后的明文Token,并将请求转发至微服务。 (8)微服务收到请求,明文token中包含登录用户的身份和权限信息。因此后续微服务自己可以干两件事: 1,用 户授权拦截(看当前用户是否有权访问该资源) 2,将用户信息存储进当前线程上下文(有利于后续业务逻辑随时 获取当前用户信息)

流程所涉及到UAA服务、API网关这三个组件职责如下: 1)统一认证服务(UAA) 它承载了OAuth2.0接入方认证、登入用户的认证、授权以及生成令牌的职责,完成实际的用户认证、授权功能。 2)API网关 作为系统的唯一入口,API网关为接入方提供定制的API集合,它可能还具有其它职责,如身份验证、监控、负载均 衡、缓存等。API网关方式的核心要点是,所有的接入方和消费端都通过统一的网关接入微服务,在网关层处理所 有的非业务功能。