zxpnet网站 zxpnet网站
首页
前端
后端服务器
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

zxpnet

一个爱学习的java开发攻城狮
首页
前端
后端服务器
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 大后端课程视频归档
  • 南航面试题
  • 并发编程

  • 性能调优

  • java8语法

  • lombok

  • 日志

  • 工具类

  • spring

  • mybatis

  • springboot

  • redis

  • zookeeper

  • springcloud

  • dubbo

  • netty

  • springsecurity

    • springsecurity基本使用及个性化登录配置
    • 微服务安全与实战

    • springsecurity认证流程分析
    • springsecurity过滤器链分析
    • springsecurity图形验证码
    • springsecurity记住我RememberMe功能
    • springsecurity手机验证码登陆
    • springsecurity httpFirewall
    • springsecurity漏洞保护
    • Untitled
    • 跨域问题
    • Springsecurity Session会话管理
    • springsecurity控制授权
    • 权限模型
    • springseuciryt oauth2基础
      • 一、开发框架演进
      • 二、Spring Security OAuth 简介
        • 认证服务器:
        • 资源服务器:
        • 自己实现服务提供商:
        • 参考文章:
      • 三、开发认证服务器
        • starter分析
        • 1、授权码模式
        • 实现授权码模式第一步:获取授权码
        • 第二步授权码换取access_token:
        • 2、密码模式
        • 3、客户端授权模式
        • 3、token自动刷新
        • 将token保存到数据库
        • 1、建表
        • 2、写配置
      • 四、资源服务器
      • 五、springsecurity oauth2核心源码
        • 一、 @EnableAuthorizationServer 解析
        • AuthorizationServerEndpointsConfiguration:
        • AuthorizationServerSecurityConfiguration
        • 二、 @EnableResourceServer 解析
        • 三、 AuthorizationEndpoint 解析
        • 四、 TokenEndpoint 解析
        • 五、 OAuth2AuthenticationProcessingFilter (资源服务器认证)解析
        • 六、 重写登陆,实现登录接口直接返回jwtToken
      • 六、重构登陆后发令牌
        • 修改认证成功处理器
        • 写资源服务器配置
        • 重写验证码的保存逻辑
      • 七、Token定制化处理
        • 1、Token参数配置
        • 将token保存到redis当中
        • 2、设置保存的位置、过期时间、授权模式等
        • 2、JWT替换默认Token
        • 自包含:
        • 密签:
        • 可扩展:可以自定义相关参数
        • 3、扩展及解析Token
      • 八、基于jwt实现sso单点登陆
      • 九、整合gateway
    • 基于JWT实现SSO单点登录
    • springsocial第三方登陆
    • springsecurity退出登陆
    • Spring security 集成 JustAuth 实现第三方授权登录
    • Spring Boot+CAS 单点登录
    • Springsecurity常见错误
  • mq消息中间件

  • shiro

  • beetle

  • 模板引擎

  • jpa

  • 数据结构与算法

  • 数据库知识与设计

  • gradle

  • maven

  • bus

  • 定时任务

  • docker

  • centos

  • 加解密

  • biz业务

  • pigx项目

  • 开源项目

  • 品达通用权限项目-黑马

  • 货币交易项目coin-尚学堂

  • php

  • backend
  • springsecurity
shollin
2021-06-06
目录

springseuciryt oauth2基础

  • 一、开发框架演进
  • 二、Spring Security OAuth 简介
    • 认证服务器:
    • 资源服务器:
    • 自己实现服务提供商:
  • 三、开发认证服务器
    • starter分析
    • 1、授权码模式
    • 2、密码模式
    • 3、客户端授权模式
    • 3、token自动刷新
    • 将token保存到数据库
  • 四、资源服务器
  • 五、springsecurity oauth2核心源码
    • 一、 @EnableAuthorizationServer 解析
    • 二、 @EnableResourceServer 解析
    • 三、 AuthorizationEndpoint 解析
    • 四、 TokenEndpoint 解析
    • 五、 OAuth2AuthenticationProcessingFilter (资源服务器认证)解析
    • 六、 重写登陆,实现登录接口直接返回jwtToken
  • 六、重构登陆后发令牌
    • 修改认证成功处理器
    • 写资源服务器配置
    • 重写验证码的保存逻辑
  • 七、Token定制化处理
    • 1、Token参数配置
    • 2、JWT替换默认Token
    • 3、扩展及解析Token
  • 八、基于jwt实现sso单点登陆
  • 九、整合gateway

# 一、开发框架演进

image-20210709162816333

随着技术的发展,新的前端渠道app出现了,而且随着应用部署方式的改变,前后端分离现在也很流行,前后端分离模式下,html就是一种前端的资源,不在和应用服务器部署在一起了,而是单独部署在WebServer上,比如nodejs。前后端分离模式用户访问的是WebServer,由WebServer访问Application Server,WebServer处理ajax请求和渲染返回的数据。这种模式下,访问应用服务器的不再是用户了而是第三方的应用。

发给用户一个令牌token,用户每次访问都拿着令牌,来判断用户登录信息权限等,token表现形式就是一个字符串。这样上边说的三个问题就可以解决了,token 不是通过cookie来携带的,而是http请求的参数,在请求头或者普通的参数都行;基于session的JESSIONID是服务器自己生成的,校验也是他自己校验,但是token的生成、校验我们可以自己控制,可以在token上加技术手段来增强安全性,可以实现token 刷新的方式而不会出现用户重复登录,缩短token有效时间增强安全性还保证了用户体验。

用令牌的方式在应用服务器和其他应用之间的认证和授权。这就很自然的联系到了OAuth协议了。OAuth协议就是用token的方式来做认证授权的:

image-20210709163723984 image-20210709164049546

img

image-20210709164508091

绿色块,springsecurity已帮助我们完成,我们需要做的就是资源和自定义认证(图片验证码表单登陆、手机号表单登陆、oauth登陆)

# 二、Spring Security OAuth 简介

Spring Social 实际上是封装了第三方应用(Client)所要做的大多数事情,拿着Spring Social可以很快开发一个第三方应用来连接想要连接的服务提供商,而Spring Security OAuth和Spring Social 正好相反,Spring Security OAuth 封装了服务提供商的大部分行为,用来快速搭建服务提供商的程序,发放令牌、校验令牌。

要实现服务提供商,其实就是要实现两个服务器 :认证服务器、资源服务器

# 认证服务器:

**实现四种授权模式:**来确认用户身份以及拥有的权限。Spring Security OAuth 里已实现了四种授权模式

**token的生成和存储:**根据信息生成令牌token,资源服务器根据token拿资源。OAuth协议没有规定token的具体生成方式,Spring Security OAuth 提供了默认的实现。

# 资源服务器:

保护资源,在我们的场景,资源就是Rest 服务。Spring Security 是一系列的过滤器链,Spring Security OAuth就是加了个OAuth2AuthenticationFilter的过滤器,从请求中拿出来发出去的token,根据配置的存储策略从存储里根据token拿出用户信息,根据用户信息是否存在、是否有权限等来判断是否能访问要访问的 服务。

# 自己实现服务提供商:

要处理的问题是,我们不希望用户走标准的四种授权模式的,如手机号+短信验证码登录方式和标准四种授权模式是对应不上的。我们需要做的是让自定义的认证方式可以嫁接到认证服务器上,让用户通过自己的认证方式也可以调用token 生成的机制生成token发给第三方应用,第三方应用存储这个token ,每次访问服务带上这个token 经过过滤器链上的过滤器通过认证、授权来访问服务。

1、实现一个标准的OAuth2协议中Provider(服务提供商:认证服务器、资源服务器)角色的主要功能

2、重构之前的认证方式,使其支持token

3、自定义 token的生成方式

# 参考文章:

Spring Security构建Rest服务-1200-SpringSecurity OAuth开发APP认证框架 - 我俩绝配 - 博客园 (cnblogs.com) (opens new window)

# 三、开发认证服务器

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
1
2
3
4
@Configuration
@EnableAuthorizationServer //这个注解就是实现了一个认证服务器
public class ImoocAuthenticationServerConfig {

}
1
2
3
4
5

image-20210801134905141

@EnableAuthorizationServer 注解就说明该项目是一个 认证服务器,项目启动时,会打印出client-id,/oauth/token、/oauth/token_key、 /oauth/check_token

2021-07-09 17:22:30.427  INFO 10104 --- [           main] a.OAuth2AuthorizationServerConfiguration : Initialized OAuth2 Client

security.oauth2.client.client-id = f6e825b7-b51a-4646-abcc-ec36baca0394
security.oauth2.client.client-secret = d06ebb34-ad51-462d-b6cf-81784d51fa83

 o.s.s.web.DefaultSecurityFilterChain     : Will secure Or [Ant [pattern='/oauth/token'], Ant [pattern='/oauth/token_key'], Ant [pattern='/oauth/check_token']] with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7316742f, org.springframework.security.web.context.SecurityContextPersistenceFilter@7e667bfb, org.springframework.security.web.header.HeaderWriterFilter@5640a424, org.springframework.security.web.authentication.logout.LogoutFilter@1775ff4a, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@4c48564d, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1124fd8, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@18ae3583, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@6c73d88c, org.springframework.security.web.session.SessionManagementFilter@7a0eca26, org.springframework.security.web.access.ExceptionTranslationFilter@30d14f84, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@523c4a6d]
1
2
3
4
5
6

服务提供商需要提供两个服务:

1,用户调过来点击授权的地址 (如qq微信登录的授权页)/oauth/authorize

2,点完授权后带着授权码换取access_token的地址(对用户不可见)

加上了@EnableAuthorizationServer 注解的项目,启动后控制台会打印如下的信息:

img

/oauth/authorize 就是让用户授权的地址,启动应用,访问/oauth/authorize,需要一些参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"

  • client_id:表示客户端的ID,必选项

  • redirect_uri:表示重定向URI,可选项

  • scope:表示申请的权限范围,自定义,可选项

  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。

    完整地址如:localhost:18080/oauth/authorize?response_type=code&client_id=zxp&redirect_uri=http://www.baidu.com&scope=all (opens new window)

# starter分析

image-20210713085717881

OAuth2AutoConfiguration: 注册了OAuth2AuthorizationServerConfiguration、OAuth2ResourceServerConfiguration、OAuth2ClientProperties、ResourceServerProperties

@Configuration
@ConditionalOnClass({ OAuth2AccessToken.class, WebMvcConfigurer.class })
@Import({ OAuth2AuthorizationServerConfiguration.class,
      OAuth2MethodSecurityConfiguration.class, OAuth2ResourceServerConfiguration.class,
      OAuth2RestOperationsConfiguration.class })
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
@EnableConfigurationProperties(OAuth2ClientProperties.class)
public class OAuth2AutoConfiguration {

   private final OAuth2ClientProperties credentials;

   public OAuth2AutoConfiguration(OAuth2ClientProperties credentials) {
      this.credentials = credentials;
   }

   @Bean
   public ResourceServerProperties resourceServerProperties() {
      return new ResourceServerProperties(this.credentials.getClientId(),
            this.credentials.getClientSecret());
   }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 1、授权码模式

# 实现授权码模式第一步:获取授权码

配置client-id, 允许哪些授权模式,scopes等

@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerServerConfig extends AuthorizationServerConfigurerAdapter {
   
    //配置客户端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                //client的id和密码
                .withClient("client1")
                .secret(passwordEncoder.encode("123123"))

                //给client一个id,这个在client的配置里要用的
                .resourceIds("resource1")

                //允许的申请token的方式,测试用例在test项目里都有.
                //authorization_code授权码模式,这个是标准模式
                //implicit简单模式,这个主要是给无后台的纯前端项目用的
                //password密码模式,直接拿用户的账号密码授权,不安全
                //client_credentials客户端模式,用clientid和密码授权,和用户无关的授权方式
                //refresh_token使用有效的refresh_token去重新生成一个token,之前的会失效
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")

                //授权的范围,每个resource会设置自己的范围.
                .scopes("scope1")

                //这个是设置要不要弹出确认授权页面的.
                .autoApprove(false)

                //这个相当于是client的域名,重定向给code的时候会跳转这个域名
                .redirectUris("http://www.baidu.com")

                .and()

                //在spring cloud的测试中,我们有两个资源服务,这里也给他们配置两个client,并分配不同的scope.
                .withClient("client2")
                .secret(passwordEncoder.encode("123123"))
                .resourceIds("resource2")
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                .scopes("scope2")
                .autoApprove(false)
                .redirectUris("http://www.sogou.com");
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

get请求:localhost:18080/oauth/authorize?response_type=code&client_id=zxp&redirect_uri=http://www.baidu.com&scope=all (opens new window)

会要求先登陆,跳到/login, 登陆后,到是否允许授权页,如图。默认情况下,认证服务器上请求需要有ROLE_USER的角色才能访问授权页。

image-20210709182454730

# 第二步授权码换取access_token:

此时就该拿着授权码去获取access_token了,post请求 /oauth/token ,使用Resetlet Client,在请求头里新建一个Authentication(Authorization 设置HTTP身份验证的凭证https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Authorization),用户名输入imooc,密码输入 imoocsecret

客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。

  • code:表示上一步获得的授权码,必选项。

  • redirect_uri:表示重定向URI,必选项,且必须与上边步骤中的该参数值保持一致。

  • client_id:表示客户端ID,必选项。

  • client_secret:客户端密钥

  • 同时header里面,需要加上 client_id和client_secret组成的 Basic Auth, Authorization里面的username就填client_id

完整的请求:

2021-07-09_200745

参考文章:

oauth2授权码模式官方英文介绍 (ietf.org) (opens new window)

# 2、密码模式

/oauth/token 参数是用户名密码,其实是用户把在服务提供商上的用户名/密码告诉了第三方,第三方拿着用户名密码去给服务提供商说用户已经授权了。

在app场景下是可用的,因为第三方是app,是公司前端,提供商是服务端,都是一伙的,不涉及安全问题。 请求参数如下

  • grant_type:表示使用的授权模式,必选项,此处的值固定为password。

  • username 数据库用户登陆用户名

  • password 用户登陆密码

  • scope

同时header里面,需要加上 client_id和client_secret组成的 Basic Auth , Authorization里面的username就填client_id

{
    "error": "unsupported_grant_type",
    "error_description": "Unsupported grant type: password"
}
1
2
3
4

返回Unsupported grant type: password, 则需要在认证服务器中设置 中配置AuthenticationManager

@Configuration
@EnableAuthorizationServer
@RequiredArgsConstructor
public class MyAuthorizationServerServerConfig extends AuthorizationServerConfigurerAdapter {

    private final AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //添加客户端信息
        clients.inMemory()
                .withClient("zxp")
                .secret("zxp123")
                .scopes("all")
                .redirectUris("http://www.baidu.com")
                .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit");
    }

    /**
     * Spring security5中新增加了加密方式,并把原有的spring security的密码存储格式改了
     * @param endpoints
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	... 省略了
	
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
1
2
3
4
5
6
7
8
9
10
11

参考文章:

OAuth2密码模式提示Unsupported grant type: password - 灰信网(软件开发博客聚合) (freesion.com) (opens new window)

# 3、客户端授权模式

只需要传客户端id和client_secret即可,不需要用户名和密码

# 3、token自动刷新

# 将token保存到数据库

# 1、建表

官方给了个sql文件:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql,这个sql文件不能在mysql中直接执行,要把sql中的256修改为128,LONGVARBINARY修改为BLOB

    create table oauth_client_details (
      client_id VARCHAR(128) PRIMARY KEY,
      resource_ids VARCHAR(128),
      client_secret VARCHAR(128),
      scope VARCHAR(128),
      authorized_grant_types VARCHAR(128),
      web_server_redirect_uri VARCHAR(128),
      authorities VARCHAR(128),
      access_token_validity INTEGER,
      refresh_token_validity INTEGER,
      additional_information VARCHAR(4096),
      autoapprove VARCHAR(128)
    );
    create table oauth_client_token (
      token_id VARCHAR(128),
      token BLOB,
      authentication_id VARCHAR(128) PRIMARY KEY,
      user_name VARCHAR(128),
      client_id VARCHAR(128)
    );
    create table oauth_access_token (
      token_id VARCHAR(128),
      token BLOB,
      authentication_id VARCHAR(128) PRIMARY KEY,
      user_name VARCHAR(128),
      client_id VARCHAR(128),
      authentication BLOB,
      refresh_token VARCHAR(128)
    );
    create table oauth_refresh_token (
      token_id VARCHAR(128),
      token BLOB,
      authentication BLOB
    );
    create table oauth_code (
      code VARCHAR(128), authentication BLOB
    );
    create table oauth_approvals (
        userId VARCHAR(128),
        clientId VARCHAR(128),
        scope VARCHAR(128),
        status VARCHAR(10),
        expiresAt TIMESTAMP,
        lastModifiedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    );
    -- customized oauth_client_details table
    create table ClientDetails (
      appId VARCHAR(128) PRIMARY KEY,
      resourceIds VARCHAR(128),
      appSecret VARCHAR(128),
      scope VARCHAR(128),
      grantTypes VARCHAR(128),
      redirectUrl VARCHAR(128),
      authorities VARCHAR(128),
      access_token_validity INTEGER,
      refresh_token_validity INTEGER,
      additionalInformation VARCHAR(4096),
      autoApproveScopes VARCHAR(128)
    );	
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

插入客户端数据,密码为123123

INSERT INTO `oauth_client_details` VALUES ('client1', 'resource1', '$2a$10$YEpRG0cFXz5yfC/lKoCHJ.83r/K3vaXLas5zCeLc.EJsQ/gL5Jvum', 'scope1', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.baidu.com', null, '300', '1500', null, 'false');

INSERT INTO `oauth_client_details` VALUES ('client2', 'resource2', '$2a$10$YEpRG0cFXz5yfC/lKoCHJ.83r/K3vaXLas5zCeLc.EJsQ/gL5Jvum', 'scope2', 'authorization_code,password,client_credentials,implicit,refresh_token', 'http://www.163.com', null, '300', '1500', null, 'false');
1
2
3

# 2、写配置

@Configuration

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

    //首先要建表,官方给了个sql文件:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
    //这个sql文件不能在mysql中直接执行,要把sql中的256修改为128,LONGVARBINARY修改为BLOB
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    @Autowired
    private ClientDetailsService clientDetailsService;
    
    //密码模式才需要配置,认证管理器
    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private DataSource dataSource;

    //配置客户端
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置客户端存储到db
        JdbcClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
        clientDetailsService.setPasswordEncoder(passwordEncoder);
        clients.withClientDetails(clientDetailsService);
    }

    //配置token管理服务
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setClientDetailsService(clientDetailsService);
        defaultTokenServices.setSupportRefreshToken(true);

        //配置token的存储方法
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setAccessTokenValiditySeconds(300);
        defaultTokenServices.setRefreshTokenValiditySeconds(1500);
        
        //配置token增加,把一般token转换为jwt token
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenConverter()));
        defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
        
        return defaultTokenServices;
    }


    //把上面的各个组件组合在一起
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.authenticationManager(authenticationManager)//认证管理器
                //配置授权码存储到db
                .authorizationCodeServices(new JdbcAuthorizationCodeServices(dataSource))//授权码管理
                .tokenServices(tokenServices())//token管理
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    //配置哪些接口可以被访问
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.tokenKeyAccess("permitAll()")///oauth/token_key公开
                .checkTokenAccess("permitAll()")///oauth/check_token公开
                .allowFormAuthenticationForClients();//允许表单认证
    }
    
    //配置如何把普通token转换成jwt token
    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

        //使用对称秘钥加密token,resource那边会用这个秘钥校验token
        converter.setSigningKey("uaa123");
        return converter;
    }
    
    //配置token的存储方法
    @Bean
    public TokenStore tokenStore() {
        //配置token存储在内存中,这种是普通token,每次都需要远程校验,性能较差
        //return new JdbcTokenStore(dataSource);
        
        //把用户信息都存储在token当中,相当于存储在客户端,性能好很多
        return new JwtTokenStore(tokenConverter());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

# 四、资源服务器

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
1
2
3
4
5
6
7
8

只需要添加注解@EnableResourceServer,即可变成资源服务器,请求过来的时候,利用授权模式拿 到token, 再在请求头中加上token, 即可访问到资源服务器里面的资源

  • token默认放在内存中,重启应用会失效,也可以存放在数据库当中,

image-20210709220752199

image-20210801131614250

@Configuration
//开启oauth2,reousrce server模式
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {

        //远程token验证, 普通token必须远程校验
        RemoteTokenServices tokenServices = new RemoteTokenServices();
        //配置去哪里验证token
        tokenServices.setCheckTokenEndpointUrl("http://127.0.0.1:3001/oauth/check_token");

        //配置组件的clientid和密码,这个也是在auth中配置好的
        tokenServices.setClientId("client1");
        tokenServices.setClientSecret("123123");

        resources
                //设置我这个resource的id, 这个在auth中配置, 这里必须照抄
                .resourceId("resource1")
                .tokenServices(tokenServices)

                //这个貌似是配置要不要把token信息记录在session中
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()

                //本项目所需要的授权范围,这个scope是写在auth服务的配置里的
                .antMatchers("/**").access("#oauth2.hasScope('scope1')")

                .and()

                //这个貌似是配置要不要把token信息记录在session中
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

因为每次都要到认证服务器进行token验证,这里更改成jwt token

image-20210801131732178

jwt token不需要保存到数据库中

@Configuration

//开启oauth2,reousrce server模式
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    //配置如何把普通token转换成jwt token
    @Bean
    public JwtAccessTokenConverter tokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();

        //使用对称秘钥加密token,resource那边会用这个秘钥校验token
        converter.setSigningKey("uaa123");
        return converter;
    }

    //配置token的存储方法
    @Bean
    public TokenStore tokenStore() {
        //把用户信息都存储在token当中,相当于存储在客户端,性能好很多
        return new JwtTokenStore(tokenConverter());
    }

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources
                //设置我这个resource的id, 这个在auth中配置, 这里必须照抄
                .resourceId("resource1")
                .tokenStore(tokenStore())

                //这个貌似是配置要不要把token信息记录在session中
                .stateless(true);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()

                //由于在zuul已经做了scope的校验,这里可以不写了.当然你想写上也是没有问题的
                .antMatchers("/**").permitAll()//.access("#oauth2.hasScope('scope1')")

                .and()

                //这个貌似是配置要不要把token信息记录在session中
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

# 五、springsecurity oauth2核心源码

image-20210709221753326

实现OAuth 2.0授权服务器,Spring Security过滤器链中需要以下端点:

  • AuthorizationEndpoint 用于服务于授权请求。预设地址:/oauth/authorize。
  • TokenEndpoint 用于服务访问令牌的请求。预设地址:/oauth/token。

实现OAuth 2.0资源服务器,需要以下过滤器:

  • OAuth2AuthenticationProcessingFilter 用于加载给定的认证访问令牌请求的认证。

# 一、 @EnableAuthorizationServer 解析

我们都知道 一个授权认证服务器最最核心的就是 @EnableAuthorizationServer , 那么 @EnableAuthorizationServer 主要做了什么呢? 我们看下 @EnableAuthorizationServer 源码:

@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
1
2
3

# AuthorizationServerEndpointsConfiguration:

注册了TokenKeyEndpointRegistrar、AuthorizationEndpoint、TokenEndpoint、CheckTokenEndpoint、FactoryBean、FrameworkEndpointHandlerMapping等bean

# AuthorizationServerSecurityConfiguration

注册了ClientDetailsServiceConfiguration (里面又注册了ClientDetailsServiceConfigurer、ClientDetailsService)

# 二、 @EnableResourceServer 解析

像授权认证服务器一样,资源服务器也有一个最核心的配置 @EnableResourceServer , 那么 @EnableResourceServer 主要做了什么呢? 我们 一样先看下 @EnableResourceServer 源码:

# 三、 AuthorizationEndpoint 解析

正如前面介绍一样,AuthorizationEndpoint 本身 最大的功能点就是实现了 /oauth/authorize , 那么我们这次就来看看它是如何实现的:

# 四、 TokenEndpoint 解析

对于使用oauth2 的用户来说,最最不可避免的就是token 的获取,/oauth/token 请求就会到这个类上,话不多说,核心源码解析贴上:

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {

	private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();

	private Set<HttpMethod> allowedRequestMethods = new HashSet<HttpMethod>(Arrays.asList(HttpMethod.POST));

	@RequestMapping(value = "/oauth/token", method=RequestMethod.GET)
	public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
		if (!allowedRequestMethods.contains(HttpMethod.GET)) {
			throw new HttpRequestMethodNotSupportedException("GET");
		}
		return postAccessToken(principal, parameters);
	}
	
	@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
	public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
	Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {

		if (!(principal instanceof Authentication)) {
			throw new InsufficientAuthenticationException(
					"There is no client authentication. Try adding an appropriate authentication filter.");
		}

		String clientId = getClientId(principal);
		ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

		TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

		if (clientId != null && !clientId.equals("")) {
			// Only validate the client details if a client authenticated during this
			// request.
			if (!clientId.equals(tokenRequest.getClientId())) {
				// double check to make sure that the client ID in the token request is the same as that in the
				// authenticated client
				throw new InvalidClientException("Given client ID does not match authenticated client");
			}
		}
		if (authenticatedClient != null) {
			oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
		}
		if (!StringUtils.hasText(tokenRequest.getGrantType())) {
			throw new InvalidRequestException("Missing grant type");
		}
		if (tokenRequest.getGrantType().equals("implicit")) {
			throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
		}

		if (isAuthCodeRequest(parameters)) {
			// The scope was requested or determined during the authorization step
			if (!tokenRequest.getScope().isEmpty()) {
				logger.debug("Clearing scope of incoming token request");
				tokenRequest.setScope(Collections.<String> emptySet());
			}
		}

		if (isRefreshTokenRequest(parameters)) {
			// A refresh token has its own default scopes, so we should ignore any added by the factory here.
			tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
		}

		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
		if (token == null) {
			throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
		}

		return getResponse(token);

	}
	... 省略
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

1、核心逻辑就一行 OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

image-20210710073922842

2、找到grant方法的实现在抽象类AbstractTokenGranter当中,其又调用 getAccessToken(client, tokenRequest); 拆解里面的两个方法getOAuth2Authentication()交由子类实现,不同授权模式实现不一样; 和 createAccessToken()

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
1
2
3

3、getOAuth2Authentication(),不同的授权模式,获取的方式不一样,会调用authenticationManager,最终调用UserDetailService方法拿到用户数据,见ResourceOwnerPasswordTokenGranter源码62行左右

4、tokenServices即为AuthorizationServerTokenServices (实现类DefaultTokenServices)进行创建OAuth2AccessToken,默认为uuid,也可以用TokenEnhancer增强器生成

@Transactional
	public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 先从tokenStore里面去拿令牌,如果有发过,就不用重新生成了
		OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
... 省略了部分代码
		OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
		tokenStore.storeAccessToken(accessToken, authentication);
		// In case it was modified
		refreshToken = accessToken.getRefreshToken();
		if (refreshToken != null) {
			tokenStore.storeRefreshToken(refreshToken, authentication);
		}
		return accessToken;

	}
	
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
// 默认生成的token为uuid
		DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
		int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
		if (validitySeconds > 0) {
			token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
		}
		token.setRefreshToken(refreshToken);
		token.setScope(authentication.getOAuth2Request().getScope());
//可以使用token增强器生成token
		return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
	}	
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

image-20210710080305431

image-20210710080430924

简单概括下来,整个生成token 的逻辑如下:

  • 1、 验证用户信息 (正常情况下会经过 ClientCredentialsTokenEndpointFilter 过滤器认证后获取到用户信息 )
  • 2、 通过 ClientDetailsService().loadClientByClientId() 获取系统配置的客户端信息
  • 3、 通过客户端信息生成 TokenRequest 对象
  • 4、 将步骤3获取到的 TokenRequest 作为TokenGranter.grant() 方法参照 生成 OAuth2AccessToken 对象(即token)
  • 5、 返回 token

# 五、 OAuth2AuthenticationProcessingFilter (资源服务器认证)解析

通过前面的解析我们最终获取到了token,但获取token 不是我们最终目的,我们最终的目的时拿到资源信息,所以我们还得通过获取到的token去调用资源服务器接口获取资源数据。那么接下来我们就来解析资源服务器是如何通过传入token去辨别用户并允许返回资源信息的。我们知道资源服务器在过滤器链新增了 OAuth2AuthenticationProcessingFilter 来拦截请求并认证,那就这个过滤器的实现:

整个filter步骤最核心的是下面2个:

  • 1、 调用 tokenExtractor.extract() 方法从请求中解析出token信息并存放到 authentication 的 principal 字段 中
  • **2、 调用 authenticationManager.authenticate() 认证过程: 注意此时的 authenticationManager 是 OAuth2AuthenticationManager **

在解析@EnableResourceServer 时我们讲过 OAuth2AuthenticationManager 与 OAuth2AuthenticationProcessingFilter 的关系,这里不再重述,我们直接看下 OAuth2AuthenticationManager 的 authenticate() 方法实现:

整个 认证逻辑分4步:

  • 1、 从 authentication 中获取 token
  • 2、 调用 tokenServices.loadAuthentication() 方法 通过 token 参数获取到 OAuth2Authentication 对象 ,这里的tokenServices 就是我们资源服务器配置的。
  • 3、 检测客户端信息,由于我们采用授权服务器和资源服务器分离的设计,所以这个检测方法实际没有检测
  • 4、 设置认证成功标识并返回 ,注意返回的是 OAuth2Authentication (Authentication 子类)。

后面的授权过程就是原汁原味的Security授权,所以至此整个资源服务器 通过获取到的token去调用接口获取资源数据 的解析完成。

# 六、 重写登陆,实现登录接口直接返回jwtToken

前面,我们花了大量时间讲解,那么肯定得实践实践一把。 相信大家平时的登录接口都是直接返回token的,但是由于Security 最原本的设计原因,登陆后都是跳转回到之前求情的接口,这种方式仅仅适用于PC端,那如果是APP呢?所以我们想要在原有的登陆接口上实现当非PC请求时返回token的功能。还记得之前提到过的 AuthenticationSuccessHandler 认证成功处理器,我们的功能实现就在这里面。

我们重新回顾下 /oauth/authorize 实现 token,模仿实现后的代码如下:

顾下创建token 需要的 几个必要类:clientDetailsService 、 authorizationServerTokenServices、 ClientDetails 、 TokenRequest 、OAuth2Request、 authentication、OAuth2Authentication 。 了解这几个类之间的关系很有必要。对于clientDetailsService 、 authorizationServerTokenServices 我们可以直接从Spring 容器中获取,ClientDetails 我们可以从请求参数中获取,有了 ClientDetails 就有了 TokenRequest,有了 TokenRequest 和 authentication(认证后肯定有的) 就有了 OAuth2Authentication ,有了OAuth2Authentication 就能够生成 OAuth2AccessToken。 至此,我们通过直接请求登陆接口(注意在请求头中添加ClientDetails信息)就可以实现获取到token了,那么有同学会问,如果我是手机登陆方式呢?其实不管你什么登陆方式,只要你设置的登陆成功处理器是上面那个就可支持

# 六、重构登陆后发令牌

授权码模式进入TokenGranter那一部分无法修改代码 ,因此从逻辑链的后边入手。

image-20210710081746150

image-20210710082030068

# 修改认证成功处理器

/**
 * 认证成功处理器
 * @author: shollin
 * @date: 2021/7/6/006 6:52
 */
//@Component
@Slf4j
@RequiredArgsConstructor
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private  final ObjectMapper objectMapper;
    private final SecurityProperties securityProperties;
    private final ClientDetailsService clientDetailsService;
    private final AuthorizationServerTokenServices authorizationServerTokenServices;

    private BasicAuthenticationConverter authenticationConverter = new BasicAuthenticationConverter();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        String contentType = request.getContentType();
        log.info("验证成功!{}",contentType);
        

        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (StrUtil.isNotBlank(header)) {
            // 发放令牌
            // 第一步:从header里面解析出client_id 和client_secret,参考BasicAuthenticationFilter
            UsernamePasswordAuthenticationToken authRequest = authenticationConverter.convert(request);

            if (authRequest == null) {
                this.logger.trace("Did not process authentication request since failed to find username and password in Basic Authorization header");
                throw new UnapprovedClientAuthenticationException("client_id不存在");
            }
            String clientId = authRequest.getName();
            String clientSecret = (String) authRequest.getCredentials();
            ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
            if(clientDetails==null){
                throw new UnapprovedClientAuthenticationException("client_id对应的匹配令牌不存在:"+clientId);
            }else if(!clientSecret.equals(clientDetails.getClientSecret())){
                throw new UnapprovedClientAuthenticationException("client_secret不匹配"+clientId);
            }

            // 第二步: 生成OAuth2Authentication,参考源码:TokenEndpoint、ResourceOwnerPasswordTokenGranter
            String grantType="custom";
            TokenRequest tokenRequest = new TokenRequest(MapUtil.empty(), clientId, clientDetails.getScope(),grantType);
            OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
            OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);

            // 第三步:调用AuthorizationServerTokenServices创建令牌,参考源码: AbstractTokenGranter
            OAuth2AccessToken accessToken = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
            WebUtil.renderJson(response,R.data(accessToken));
            return;
        }


        if (WebUtil.isAjaxRequest(request)) {
            WebUtil.renderJson(response,R.data(authentication));
        } else {
            // 存到session里,方便取值。
            request.getSession().setAttribute("authentication", authentication);
            // 支持跳转到自定义页面

            // 会帮我们跳转到上一次请求的页面上
            super.onAuthenticationSuccess(request, response, authentication);

        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

# 写资源服务器配置

@Configuration
@RequiredArgsConstructor
@EnableResourceServer
public class MyResouceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private ValidateCodeFilter validateCodeFilter;

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Autowired
    private MobileAuthenticationSecurityConfig mobileAuthenticationSecurityConfig;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        // 所有的请求都需要认证
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .antMatchers("/user/**").hasRole("USER")
                .anyRequest().authenticated();

        // 表单登陆配置
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);

        http.formLogin()
                .loginPage("/login") // 登陆页get请求login, 登陆post请求login
                .permitAll()
                .successHandler(authenticationSuccessHandler) // 表单登陆成功、失败处理器
                .failureHandler(authenticationFailureHandler);

        // 安全策略配置
        http.csrf().disable();
        http.cors();

        http.sessionManagement().disable();

        http.apply(mobileAuthenticationSecurityConfig);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

可以看到FilterChainProxy过滤器链上加多了OAuth2AuthenticationProcessingFilter

image-20210711180606086

# 重写验证码的保存逻辑

验证码需要保存到redis当中,而不能放在session里面,用curl工具命令行(git命令窗口)发送请求进行测试

image-20210711182016968

image-20210711181930453

# 七、Token定制化处理

在认证服务器里面进行配置修改

# 1、Token参数配置

# 将token保存到redis当中

@Configuration()
@Slf4j
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class TokenConfig {
    /**
     * redis工厂,默认使用lettue
     */
    @Autowired
    public RedisConnectionFactory redisConnectionFactory;

    @Autowired
    public RedisProperties redisProperties;

    @Bean
    public TokenStore tokenStore() {
        // 这里是RedissonConnection
        log.info("redisConnectionFactory: "+redisConnectionFactory.getConnection());
        //使用redis存储token
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        //设置redis token存储中的前缀
        redisTokenStore.setPrefix("zxpAuth-token:");
        return redisTokenStore;
    }   

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Autowired
private TokenStore tokenStore;
 
 @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);
    }
1
2
3
4
5
6
7
8
9

因为项目里面用了redisson锁,其starter用的RedisConnectionFactory,就是RedissonConnectionFactory

# 2、设置保存的位置、过期时间、授权模式等

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //添加客户端信息
    clients.inMemory()
            .withClient("zxp")
            .secret("zxp123")
            .scopes("all","read","write")
            .redirectUris("http://www.baidu.com")
            .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
            .accessTokenValiditySeconds(7200); // 过期时间,单位秒
}
1
2
3
4
5
6
7
8
9
10
11

# 2、JWT替换默认Token

默认生成的token为没有意义的uuid,见源码 DefaultTokenServices 293行。

JWT特点:

# 自包含:

可以包含相关的用户信息,最基本的数据关联,用户执行相关操作时需要有对应的权限,此时请求头中会携带Token,对Token进行解析就可以获取自己定义的相关信息,例如userId,而不必根据Token再去查询相关联的数据。

# 密签:

签名用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。

# 可扩展:可以自定义相关参数

@Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    //处理token生成逻辑
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 设置签名密钥
        jwtAccessTokenConverter.setSigningKey(securityProperties.getJwt().getSecret());
        return jwtAccessTokenConverter;
    }

    // 自定义增强器,添加一些额外的token数据
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new MyJwtTokenEnhancer();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    log.info("注入的tokenStore:"+tokenStore);
    endpoints.tokenStore(tokenStore)
            .authenticationManager(authenticationManager)
            .userDetailsService(userDetailsService);

    // 如果配置了jwt方式生成token,则加上这个
    if(jwtAccessTokenConverter!=null){
        endpoints.accessTokenConverter(jwtAccessTokenConverter);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

生成的jwt token可以在线解析: JSON Web Tokens - jwt.io (opens new window)

// jwt token的话,@AuthenticationPrincipal UserDetails拿不到
@GetMapping("/me")
public Object me(Authentication user) {
    return user;
}
1
2
3
4
5

用jwt获取到的数据,不能用 @AuthenticationPrincipal UserDetails user 注解拿

# 3、扩展及解析Token

jwt的token里面可以添加自定义的信息,但是security在解析成Authentication时,并不认识新添加的信息,需要自己去解析token.

public class MyJwtTokenEnhancer implements TokenEnhancer {

	@Override
	public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
		Map<String, Object> map = new HashMap<>();
		MySocialUser mySocialUser = (MySocialUser) authentication.getPrincipal();
		// 从数据库中根据手机号之类信息查询出
		map.put("userId", mySocialUser.getId());
		((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(map);
		return accessToken;
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        log.info("注入的tokenStore:"+tokenStore);
        endpoints.tokenStore(tokenStore)
                .authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService);

        // 如果配置了jwt方式生成token,则加上这个
        if(jwtAccessTokenConverter!=null && tokenEnhancer!=null){
            TokenEnhancerChain chain = new TokenEnhancerChain();
            ArrayList<TokenEnhancer> list = Lists.newArrayList();
            list.add(jwtAccessTokenConverter);
            list.add(tokenEnhancer);
            chain.setTokenEnhancers(list);
            endpoints
                    .tokenEnhancer(chain)
                    .accessTokenConverter(jwtAccessTokenConverter);
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
	 * 从token中解析出当前账号标识
	 */
	public static String getUserId() {
		ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
		HttpServletRequest request = attributes.getRequest();
		String header = request.getHeader("Authorization");
		String token = StringUtils.substringAfter(header, "bearer ");
		Claims claims = null;
		// 用户在系统内标识
		String userId = null;
		try {
			claims = Jwts.parser().setSigningKey("mydev".getBytes("UTF-8")).parseClaimsJws(token).getBody();
			userId = (String) claims.get("userId");
		} catch (Exception e) {
			userId = null;
		}
		return userId;
	}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

image-20210711233804921

// jwt token的话,@AuthenticationPrincipal UserDetails拿不到
@GetMapping("/me")
public Object me(Authentication user, HttpServletRequest request) throws Exception {

    String authorization = request.getHeader(Header.AUTHORIZATION.getValue());
    if(StrUtil.isNotBlank(authorization)){
        String token = StrUtil.subAfter(authorization,"bearer ", true);
        Claims claims = Jwts.parser().setSigningKey(securityProperties.getJwt().getSecret().getBytes(CharsetUtil.UTF_8))
                .parseClaimsJws(token).getBody();
        Object author = claims.get("author");
        log.info("作者:"+author);
    }
    return user;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

image-20210801110335498

image-20210801110247592

如果是普通uuid的token, 资源服务器需要将token发送到认证服务器进行token的验证,会比较影响性能。 改造成Jwt形式的token后,因为token中就包括了用户数据,不需要进行验证。

源码参考: lansinuote/Spring-Oauth2-Toturials: 使用Spring Oauth2做分布式鉴权 (github.com) (opens new window)

参考文章:

Spring Security(十五):Token配置_monday的博客-CSDN博客 (opens new window)

SpringSecurity之令牌配置_single_cong的博客-CSDN博客 (opens new window)

# 八、基于jwt实现sso单点登陆

jwt格式:以点.进行分割成三段,第一段为头,第二段为内容,第三段为签名信息(防止篡改)。

参考文章:

基于JWT实现SSO单点登录 - 明月之诗 - 博客园 (cnblogs.com) (opens new window)

# 九、整合gateway

参考文章:

SpringCloud Alibaba微服务实战十四 - Gateway集成Oauth2.0 - 知乎 (zhihu.com) (opens new window)

微服务权限终极解决方案,Spring Cloud Gateway + Oauth2 实现统一认证和鉴权! (juejin.cn) (opens new window)

参考文章:

Spring Security OAuth (opens new window)

Spring Security 解析(七) —— Spring Security Oauth2 源码解析 - 想飞的猿 - 博客园 (cnblogs.com) (opens new window)

参考文章:

SpringSecurityOAuth开发app认证框架_whaleluo的博客-CSDN博客_认证框架 (opens new window)

Spring Security构建Rest服务-1200-SpringSecurity OAuth开发APP认证框架 - 我俩绝配 - 博客园 (cnblogs.com) (opens new window)

SpringSecurity结合OAuth2实现第三方授权_攻城老湿的博客-CSDN博客 (opens new window)

#springsecurity#oauth2
权限模型
基于JWT实现SSO单点登录

← 权限模型 基于JWT实现SSO单点登录→

最近更新
01
国际象棋
09-15
02
成语
09-15
03
自然拼读
09-15
更多文章>
Theme by Vdoing | Copyright © 2019-2023 zxpnet | 粤ICP备14079330号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式