为了保证服务的安全性,往往都会在接口调用时做权限校验。在分布式架构中我们会把复杂的业务拆成多个微服务,这样不得不在所有服务中都实现这样的权限校验逻辑,这样就会有很多代码和功能冗余。所以在微服务架构中一般会独立出一个单独的认证授权服务,供其他所有服务调用。
在SpringCloud体系中,我们只对网关层开放外网访问权限,其他后端微服务做网络隔离,所有外部请求必须要通过网关才能访问到后端服务。在网关层对请求进行转发时先校验用户权限,判断用户是否有权限访问。
我们一般使用Oauth2.0来实现对所有后端服务的统一认证授权。
gateway作为程序服务的入口。做鉴权应用,流程如下:
OAuth 2.0 的标准是 RFC 6749 文件。该文件先解释了 OAuth 是什么。OAuth 的核心就是向第三方应用颁发令牌。然后,RFC 6749 接着写道:它定义了获得令牌的四种授权方式(authorization grant )。
简单一点的诉述就是发起一个认证请求,根据授权类型去认证服务器认证,如果成功就返回token,再使用token去访问资源服务器,token验证通过就返回被保护的资源。
OAuth2的4种模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
密码模式(resource owner password credentials): 用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。
客户端模式(client credentials): 指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。
授权码模式(authorization code): 授权码模式,是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
简化模式(implicit): 不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。
OAuth2的角色
资源拥有者(Resource Owner) - 例如:用户Tom
资源服务器(Resource Server) - 例如:微信
授权服务器(Authorization Server) - 这里是微信,因为微信有相关数据
客户端(Client) - 这里是某第三方App或某应用
Oauth2 Token内容简介
Token基本内容如下
密码模式使用的例子
以某App登陆为例,用户请求获取授权信息
+-----------+ +-------------+
| | 1-Request Authorization | |
| |------------------------------------>| |
| | grant_type&username&password | |--+
| | |Authorization| | 2-Gen
| Client | |Service | | JWT
| | 3-Response Authorization | |<-+
| |<------------------------------------| Private Key |
| | access_token / refresh_token | |
| | token_type / expire_in / jti | |
+-----------+ +-------------+
本例中Spring OAuth2中Token结构简介
key | 备注 |
---|---|
access_token | JWT Access Token,过期时间,默认12小时 |
refresh_token | JWT Refresh Token,过期时间,默认30天 |
expires_in | 过期时间,单位秒 |
token_type | Bearer和Mac |
scope | read和write |
JWT(JSON Web Tokens)简介
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
简短来说,用户请求时,将用户信息和授权范围序列化后放入一个JSON字符串,然后使用Base64进行编码,最终在授权服务器用私钥对这个字符串进行签名,得到一个JSON Web Token,我们可以像使用Access Token一样的直接使用它,假设其他所有的资源服务器都将持有一个RSA公钥。当资源服务器接收到这个在Http Header中存有Token的请求,资源服务器就可以拿到这个Token,并验证它是否使用正确的私钥签名(是否经过授权服务器签名,也就是验签)。验签通过,反序列化后就拿到OAuth 2的验证信息。
{Header 头部}.{Payload 负载}.{Signature 签名}
JWT包含了使用.分隔的三部分: 通常包含了两部分,token类型和采用的加密算法
{
"alg": "HS256",
"typ": "JWT"
}
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据。
{
"user_name": "admin",
"scope": [
"read"
],
"organization": "admin",
"exp": 1531975621,
"authorities": [
"ADMIN"
],
"jti": "23408d38-8cdc-4460-beac-24c76dc7629a",
"client_id": "test_client"
}
使用Base64编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。
Jwt Token例子:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbInJlYWQiXSwib3JnYW5pemF0aW9uIjoiYWRtaW4iLCJleHAiOjE1MzE5NzU2MjEsImF1dGhvcml0aWVzIjpbIkFETUlOIl0sImp0aSI6IjIzNDA4ZDM4LThjZGMtNDQ2MC1iZWFjLTI0Yzc2ZGM3NjI5YSIsImNsaWVudF9pZCI6InRlc3RfY2xpZW50In0.qawS1Z4j_h4vNx10GBC_Y_PHM1LLSQt64eniWLGzsJY
可到http://www.bejson.com/enc/base64 解码,注意分3部分分别解, 也可使用官网解码工具官网解码
Spring OAuth2表结构
表名 | 简介 | 默认地址 |
---|---|---|
oauth_client_details | client持久化表 | 本例子中dml初使化了test_client |
oauth_client_token | 用户客户端存储从服务端获取的token | 未使用,本例中均为服务端 |
oauth_access_token | access_token的持久表 | 未使用,本例中使用了jwt,无需持久化到服务器中 |
oauth_refresh_token | refresh_token的持久化表 | 本例中使用了jwt |
oauth_approvals | 授权码模式授权信息持久化表 | 用户授权记录 |
oauth_code | 授权码模式code持久化表 | code临时存放,code使用过就删除 |
具体表结构请参考spring-oauth-server 数据库表说明
用户角色资源等表结构
表名 | 简介 | 备注 |
---|---|---|
users | 用户表 | 使用应用的用户 |
groups | 组织表 | 通过user_group_relation与users关联,多对多 |
position | 岗位表 | 通过user_position_relation与users关联,多对多 |
roles | 角色表 | 通过user_role_relation与users关联,多对多 |
menu | 菜单表 | 通过role_menu_relation与roles关联,多对多 |
resource | 资源表 | 通过role_resource_relation与roles关联,多对多 |
org.springframework.boot
spring-boot-starter-web
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
org.springframework.cloud
spring-cloud-starter-oauth2
mysql
mysql-connector-java
com.baomidou
mybatis-plus-boot-starter
com.jianzh5.cloud
cloud-common
引入mysql主要是我们需要将oauth2.0的客户端信息以及token认证存入数据库。
主要是导入oauth2相关数据表以及用户表,在实际开发过程中用户权限这一套应该是基于RBAC进行设计,这里为了方便演示我就直接只做一个用户表。
(oauth2.0相关表结构
SET NAMES utf8;
DROP DATABASE IF EXISTS sc_auth;
CREATE DATABASE sc_auth DEFAULT CHARSET utf8mb4;
USE sc_auth;
-- access_token存储表
DROP TABLE IF EXISTS oauth_access_token;
CREATE TABLE oauth_access_token
(
token_id VARCHAR(256) COMMENT 'MD5加密的access_token的值',
token BLOB COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
authentication_id VARCHAR(256) COMMENT 'MD5加密过的username,client_id,scope',
user_name VARCHAR(256) COMMENT '登录的用户名',
client_id VARCHAR(256) COMMENT '客户端ID',
authentication BLOB COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据',
refresh_token VARCHAR(256) COMMENT 'MD5加密果的refresh_token的值'
) COMMENT '访问令牌表';
-- refresh_token存储表
DROP TABLE IF EXISTS oauth_refresh_token;
CREATE TABLE oauth_refresh_token
(
token_id VARCHAR(256) COMMENT 'MD5加密过的refresh_token的值',
token BLOB COMMENT 'OAuth2RefreshToken.java对象序列化后的二进制数据',
authentication BLOB COMMENT 'OAuth2Authentication.java对象序列化后的二进制数据'
) COMMENT '更新令牌表';
-- 授权记录表
DROP TABLE IF EXISTS oauth_approvals;
CREATE TABLE oauth_approvals
(
userid VARCHAR(256) COMMENT '登录的用户名',
clientid VARCHAR(256) COMMENT '客户端ID',
scope VARCHAR(256) COMMENT '申请的权限',
status VARCHAR(10) COMMENT '状态(Approve或Deny)',
expiresat DATETIME COMMENT '过期时间',
lastmodifiedat DATETIME COMMENT '最终修改时间'
) COMMENT '授权记录表';
-- 授权码表
DROP TABLE IF EXISTS oauth_code;
CREATE TABLE oauth_code
(
code VARCHAR(256) COMMENT '授权码(未加密)',
authentication BLOB COMMENT 'AuthorizationRequestHolder.java对象序列化后的二进制数据'
) COMMENT '授权码表';
-- client用户表
DROP TABLE IF EXISTS oauth_client_details;
CREATE TABLE oauth_client_details
(
client_id VARCHAR(256) NOT NULL COMMENT '客户端ID',
resource_ids VARCHAR(256) COMMENT '资源ID集合,多个资源时用逗号(,)分隔',
client_secret VARCHAR(256) COMMENT '客户端密匙',
scope VARCHAR(256) COMMENT '客户端申请的权限范围',
authorized_grant_types VARCHAR(256) COMMENT '客户端支持的grant_type',
web_server_redirect_uri VARCHAR(256) COMMENT '重定向URI',
authorities VARCHAR(256) COMMENT '客户端所拥有的Spring Security的权限值,多个用逗号(,)分隔',
access_token_validity INTEGER COMMENT '访问令牌有效时间值(单位:秒)',
refresh_token_validity INTEGER COMMENT '更新令牌有效时间值(单位:秒)',
additional_information VARCHAR(4096) COMMENT '预留字段',
autoapprove VARCHAR(256) COMMENT '用户是否自动Approval操作',
CONSTRAINT pk_oauth_client_details_client_id PRIMARY KEY (client_id)
) COMMENT '客户端信息';
-- 客户端授权令牌表
DROP TABLE IF EXISTS oauth_client_token;
CREATE TABLE oauth_client_token
(
token_id VARCHAR(256) COMMENT 'MD5加密的access_token值',
token BLOB COMMENT 'OAuth2AccessToken.java对象序列化后的二进制数据',
authentication_id VARCHAR(256) COMMENT 'MD5加密过的username,client_id,scope',
user_name VARCHAR(256) COMMENT '登录的用户名',
client_id VARCHAR(256) COMMENT '客户端ID'
) COMMENT '客户端授权令牌表';
-- DML数据准备
INSERT INTO oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove)
VALUES ('test_client', NULL, '$2a$10$2szDKjvKHJCWE6YQNznogOeQF3USZHmCYj1fG7YbfK.vnTgNKLzri', 'read', 'client_credentials,authorization_code,mobile,password,refresh_token', 'http://baidu.com', NULL, 7200, 108000, NULL, NULL);
给用户表添加数据:
INSERT INTO `user` VALUES ('1', '$2a$10$gExKdT3nkoFKfW1cFlqQUuFji3azHG.W4Pe3/WxHKANg3TpkSJRfW', 'zhangjian', 'ADMIN');
注意:在spring-security 5.x版本必须要注入密码实现器,我们使用了 BCryptPasswordEncoder
加密器,所以需要这里也需要对密码进行加密
添加client信息,使用 oauth_client_details
表:
INSERT INTO `oauth_client_details` VALUES ('app', 'app', '$2a$10$fG7ou8CNxDESVFLIM7LrneDmIpwbrxGM2W6.coGPddfQPyZxiqXE6', 'web', 'implicit,client_credentials,authorization_code,refresh_token,password', 'http://www.baidu.com', 'ROLE_USER', null, null, null, null);
同理也需要对client_secret字段进行加密
spring:
main:
allow-bean-definition-overriding: true
application:
name: auth-service
cloud:
nacos:
discovery:
server-addr: xx.xx.xx.xx:8848/
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://xx.xx.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: root
password: xxxxxx
driver-class-name: com.mysql.jdbc.Driver
server:
port: 5000
mybatis-plus:
mapper-locations: classpath:/mapper/*Mapper.xml
只需要继承 AuthorizationServerConfigurerAdapter
并在开始处加上@EnableAuthorizationServer
注解即可
/**
*
* AuthorizationServerConfig
*
* Description:
* 授权/认证服务器配置
* @author javadaily
* @date 2020/2/26 16:26
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private UserDetailServiceImpl userDetailService;
// 认证管理器
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private DataSource dataSource;
/**
* access_token存储器
* 这里存储在数据库,大家可以结合自己的业务场景考虑将access_token存入数据库还是redis
*/
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
/**
* 从数据库读取clientDetails相关配置
* 有InMemoryClientDetailsService 和 JdbcClientDetailsService 两种方式选择
*/
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 注入密码加密实现器
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 认证服务器Endpoints配置
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//如果需要使用refresh_token模式则需要注入userDetailService
endpoints.userDetailsService(userDetailService);
endpoints.authenticationManager(this.authenticationManager);
endpoints.tokenStore(tokenStore());
}
/**
* 认证服务器相关接口权限管理
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients() //如果使用表单认证则需要加上
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
/**
* client存储方式,此处使用jdbc存储
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
}
/**
*
* WebSecurityConfig
*
* Description:
* 自定义web安全配置类
* @author javadaily
* @date 2020/2/26 16:35
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public UserDetailsService userDetailsService(){
return new UserDetailServiceImpl();
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 认证管理
* @return 认证管理对象
* @throws Exception 认证异常信息
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService())
.passwordEncoder(passwordEncoder());
}
/**
* http安全配置
* @param http http安全对象
* @throws Exception http安全异常信息
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().httpBasic()
.and().cors()
.and().csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/error",
"/static/**",
"/v2/api-docs/**",
"/swagger-resources/**",
"/webjars/**",
"/favicon.ico"
);
}
}
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//获取本地用户
User user = userMapper.selectByUserName(userName);
if(user != null){
//返回oauth2的用户
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
AuthorityUtils.createAuthorityList(user.getRole())) ;
}else{
throw new UsernameNotFoundException("用户["+userName+"]不存在");
}
}
}
实现 UserDetailsService
接口并实现 loadUserByUsername
方法,这一部分大家根据自己的技术框架实现,Dao层我就不贴出来了。
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
public UserMapper userMapper;
@GetMapping("getByName")
public User getByName(){
return userMapper.selectByUserName("zhangjian");
}
/**
* 获取授权的用户信息
* @param principal 当前用户
* @return 授权信息
*/
@GetMapping("current/get")
public Principal user(Principal principal){
return principal;
}
}
Oauth2.0的认证服务器也资源服务器,我们在启动类上加入 @EnableResourceServer
注解即可
@SpringBootApplication
//对外开启暴露获取token的API接口
@EnableResourceServer
@EnableDiscoveryClient
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
}
后端所有微服务都是资源服务器,所以我们需要对其进行改造,下面以account-service为例说明改造过程
org.springframework.cloud
spring-cloud-starter-oauth2
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.requestMatchers(EndpointRequest.toAnyEndpoint()).permitAll()
.antMatchers(
"/v2/api-docs/**",
"/swagger-resources/**",
"/swagger-ui.html",
"/webjars/**"
).permitAll()
.anyRequest().authenticated()
.and()
//统一自定义异常
.exceptionHandling()
.and()
.csrf().disable();
}
}
security:
oauth2:
resource:
user-info-uri: http://localhost:5000/user/current/get
id: account-service
通过以上几步我们将后端服务加上了认证服务,必须要先进行认证才能正常访问后端服务。整个实现过程还是比较复杂的,建议大家都实践一下,理解其中相关配置的作用,也方便更深入理解Oauth2协议。
在网关集成Oauth2.0后,我们的流程架构如上。主要逻辑如下:
1、客户端应用通过api网关请求认证服务器获取access_token http://localhost:8090/auth-service/oauth/token
2、认证服务器返回access_token
{
"access_token": "f938d0c1-9633-460d-acdd-f0693a6b5f4c",
"token_type": "bearer",
"refresh_token": "4baea735-3c0d-4dfd-b826-91c6772a0962",
"expires_in": 43199,
"scope": "web"
}
3、客户端携带access_token通过API网关访问后端服务
4、API网关收到access_token后通过 AuthenticationWebFilter
对access_token认证
5、API网关转发后端请求,后端服务请求Oauth2认证服务器获取当前用户
在前面文章中我们搭建好了单独的Oauth2认证授权服务,基本功能框架都实现了,这次主要是来实现第四条,SpringCloud 整合 Oauth2 后如何进行access_token过滤校验。
org.springframework.boot
spring-boot-starter-security
org.springframework.security
spring-security-oauth2-resource-server
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
主要引入跟oauth2相关的jar包,这里还需要引入数据库相关的jar包,因为我们的token是存在数据库中,要想在网关层校验token的有效性必须先从数据库取出token。
spring:
application:
name: cloud-gateway
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:mysql://xx.0.xx.xx:3306/oauth2_config?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
username: xxxxx
password: xxxxxxx
driver-class-name: com.mysql.jdbc.Driver
主要配置oauth2的数据库连接地址
在webFlux环境下通过实现 ReactiveAuthenticationManager
接口 自定义认证接口管理,由于我们的token是存在jdbc中所以命名上就叫ReactiveJdbcAuthenticationManager
@Slf4j
public class ReactiveJdbcAuthenticationManager implements ReactiveAuthenticationManager {
private TokenStore tokenStore;
public JdbcAuthenticationManager(TokenStore tokenStore){
this.tokenStore = tokenStore;
}
@Override
public Mono authenticate(Authentication authentication) {
return Mono.justOrEmpty(authentication)
.filter(a -> a instanceof BearerTokenAuthenticationToken)
.cast(BearerTokenAuthenticationToken.class)
.map(BearerTokenAuthenticationToken::getToken)
.flatMap((accessToken ->{
log.info("accessToken is :{}",accessToken);
OAuth2AccessToken oAuth2AccessToken = this.tokenStore.readAccessToken(accessToken);
//根据access_token从数据库获取不到OAuth2AccessToken
if(oAuth2AccessToken == null){
return Mono.error(new InvalidTokenException("invalid access token,please check"));
}else if(oAuth2AccessToken.isExpired()){
return Mono.error(new InvalidTokenException("access token has expired,please reacquire token"));
}
OAuth2Authentication oAuth2Authentication =this.tokenStore.readAuthentication(accessToken);
if(oAuth2Authentication == null){
return Mono.error(new InvalidTokenException("Access Token 无效!"));
}else {
return Mono.just(oAuth2Authentication);
}
})).cast(Authentication.class);
}
}
@Configuration
public class SecurityConfig {
private static final String MAX_AGE = "18000L";
@Autowired
private DataSource dataSource;
@Autowired
private AccessManager accessManager;
/**
* 跨域配置
*/
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
headers.add(HttpHeaders.ACCESS_CONTROL_MAX_AGE, MAX_AGE);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
@Bean
SecurityWebFilterChain webFluxSecurityFilterChain(ServerHttpSecurity http) throws Exception{
//token管理器
ReactiveAuthenticationManager tokenAuthenticationManager = new ReactiveJdbcAuthenticationManager(new JdbcTokenStore(dataSource));
//认证过滤器
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(tokenAuthenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(new ServerBearerTokenAuthenticationConverter());
http
.httpBasic().disable()
.csrf().disable()
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS).permitAll()
.anyExchange().access(accessManager)
.and()
// 跨域过滤器
.addFilterAt(corsFilter(), SecurityWebFiltersOrder.CORS)
//oauth2认证过滤器
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION);
return http.build();
}
}
这个类是SpringCloug Gateway 与 Oauth2整合的关键,通过构建认证过滤器 AuthenticationWebFilter
完成Oauth2.0的token校验。AuthenticationWebFilter
通过我们自定义的 ReactiveJdbcAuthenticationManager
完成token校验。我们在这里还加入了CORS
过滤器,以及权限管理器 AccessManager
@Slf4j
@Component
public class AccessManager implements ReactiveAuthorizationManager {
private Set permitAll = new ConcurrentHashSet<>();
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
public AccessManager (){
permitAll.add("/");
permitAll.add("/error");
permitAll.add("/favicon.ico");
permitAll.add("/**/v2/api-docs/**");
permitAll.add("/**/swagger-resources/**");
permitAll.add("/webjars/**");
permitAll.add("/doc.html");
permitAll.add("/swagger-ui.html");
permitAll.add("/**/oauth/**");
permitAll.add("/**/current/get");
}
/**
* 实现权限验证判断
*/
@Override
public Mono check(Mono authenticationMono, AuthorizationContext authorizationContext) {
ServerWebExchange exchange = authorizationContext.getExchange();
//请求资源
String requestPath = exchange.getRequest().getURI().getPath();
// 是否直接放行
if (permitAll(requestPath)) {
return Mono.just(new AuthorizationDecision(true));
}
return authenticationMono.map(auth -> {
return new AuthorizationDecision(checkAuthorities(exchange, auth, requestPath));
}).defaultIfEmpty(new AuthorizationDecision(false));
}
/**
* 校验是否属于静态资源
* @param requestPath 请求路径
* @return
*/
private boolean permitAll(String requestPath) {
return permitAll.stream()
.filter(r -> antPathMatcher.match(r, requestPath)).findFirst().isPresent();
}
//权限校验
private boolean checkAuthorities(ServerWebExchange exchange, Authentication auth, String requestPath) {
if(auth instanceof OAuth2Authentication){
OAuth2Authentication athentication = (OAuth2Authentication) auth;
String clientId = athentication.getOAuth2Request().getClientId();
log.info("clientId is {}",clientId);
}
Object principal = auth.getPrincipal();
log.info("用户信息:{}",principal.toString());
return true;
}
}
主要是过滤掉静态资源,将来一些接口权限校验也可以放在这里。
401 Unauthorized
通过以上几步我们将SpringCloud Gateway整合好了Oauth2.0,这样我们整个项目也基本完成了,后面几期再来对项目进行优化,欢迎持续关注。