认证鉴权与API权限控制在微服务架构中的设计与实现:授权码模式

引言: 之前系列文章《认证鉴权与API权限控制在微服务架构中的设计与实现》,前面文章已经将认证鉴权与API权限控制的流程和主要细节讲解完。由于有些同学想了解下授权码模式,本文特地补充讲解。

授权码类型介绍

授权码类型(authorization code)通过重定向的方式让资源所有者直接与授权服务器进行交互来进行授权,避免了资源所有者信息泄漏给客户端,是功能最完整、流程最严密的授权类型,但是需要客户端必须能与资源所有者的代理(通常是Web浏览器)进行交互,和可从授权服务器中接受请求(重定向给予授权码),授权流程如下:

 1 +----------+
 2 | Resource |
 3 |   Owner  |
 4 |          |
 5 +----------+
 6      ^
 7      |
 8     (B)
 9 +----|-----+          Client Identifier      +---------------+
10 |         -+----(A)-- & Redirection URI ---->|               |
11 |  User-   |                                 | Authorization |
12 |  Agent  -+----(B)-- User authenticates --->|     Server    |
13 |          |                                 |               |
14 |         -+----(C)-- Authorization Code ---<|               |
15 +-|----|---+                                 +---------------+
16   |    |                                         ^      v
17  (A)  (C)                                        |      |
18   |    |                                         |      |
19   ^    v                                         |      |
20 +---------+                                      |      |
21 |         |>---(D)-- Authorization Code ---------'      |
22 |  Client |          & Redirection URI                  |
23 |         |                                             |
24 |         |<---(E)----- Access Token -------------------'
25 +---------+       (w/ Optional Refresh Token)
26
  1. 客户端引导资源所有者的用户代理到授权服务器的endpoint,一般通过重定向的方式。客户端提交的信息应包含客户端标识(client identifier)、请求范围(requested scope)、本地状态(local state)和用于返回授权码的重定向地址(redirection URI)
  2. 授权服务器认证资源所有者(通过用户代理),并确认资源所有者允许还是拒绝客户端的访问请求
  3. 如果资源所有者授予客户端访问权限,授权服务器通过重定向用户代理的方式回调客户端提供的重定向地址,并在重定向地址中添加授权码和客户端先前提供的任何本地状态
  4. 客户端携带上一步获得的授权码向授权服务器请求访问令牌。在这一步中授权码和客户端都要被授权服务器进行认证。客户端需要提交用于获取授权码的重定向地址
  5. 授权服务器对客户端进行身份验证,和认证授权码,确保接收到的重定向地址与第三步中用于的获取授权码的重定向地址相匹配。如果有效,返回访问令牌,可能会有刷新令牌(Refresh Token)

快速入门

Spring-Securiy 配置

由于授权码模式需要登录用户给请求access_token的客户端授权,所以auth-server需要添加Spring-Security的相关配置用于引导用户进行登录。

在原来的基础上,进行Spring-Securiy相关配置,允许用户进行表单登录:

 1@Configuration
 2public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 3
 4    @Autowired
 5    CustomLogoutHandler customLogoutHandler;
 6
 7
 8    @Override
 9    protected void configure(HttpSecurity http) throws Exception {
10
11
12        http.csrf().disable()
13                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
14                .and()
15                .requestMatchers().antMatchers("/**")
16                .and().authorizeRequests()
17                .antMatchers("/**").permitAll()
18                .anyRequest().authenticated()
19                .and().formLogin()
20                .permitAll()
21                .and().logout()
22                .logoutUrl("/logout")
23                .clearAuthentication(true)
24                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
25                .addLogoutHandler(customLogoutHandler);
26
27    }
28
29}

同时需要把ResourceServerConfig中的资源服务器中的对于登出端口的处理迁移到WebSecurityConfig中,注释掉ResourceServerConfigHttpSecurity配置:

 1public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
 2
 3//    @Override
 4//    public void configure(HttpSecurity http) throws Exception {
 5//        http.csrf().disable()
 6//                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
 7//                .and()
 8//                .requestMatchers().antMatchers("/**")
 9//                .and().authorizeRequests()
10//                .antMatchers("/**").permitAll()
11//                .anyRequest().authenticated()
12//                .and().logout()
13//                .logoutUrl("/logout")
14//                .clearAuthentication(true)
15//                .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
16//                .addLogoutHandler(customLogoutHandler());
17//
18//        //http.antMatcher("/api/**").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);
19//
20//    }
21
22 /*   @Bean
23    public CustomSecurityFilter customSecurityFilter() {
24        return new CustomSecurityFilter();
25    }
26*/
27.....
28}

AuthenticationProvider

由于用户表单登录的认证过程可能有所不同,为此再添加一个CustomSecurityAuthenticationProvider,基本上与CustomAuthenticationProvider一致,只是忽略对client客户端的认证和处理。

 1@Component
 2public class CustomSecurityAuthenticationProvider implements AuthenticationProvider {
 3
 4    @Autowired
 5    private UserClient userClient;
 6
 7    @Override
 8    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
 9        String username = authentication.getName();
10        String password;
11
12        Map map;
13
14        password = (String) authentication.getCredentials();
15        //如果你是调用user服务,这边不用注掉
16        //map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
17        map = checkUsernameAndPassword(getUserServicePostObject(username, password));
18
19
20        String userId = (String) map.get("userId");
21        if (StringUtils.isBlank(userId)) {
22            String errorCode = (String) map.get("code");
23            throw new BadCredentialsException(errorCode);
24        }
25        CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId);
26        return new CustomAuthenticationToken(customUserDetails);
27    }
28
29    private CustomUserDetails buildCustomUserDetails(String username, String password, String userId) {
30        CustomUserDetails customUserDetails = new CustomUserDetails.CustomUserDetailsBuilder()
31                .withUserId(userId)
32                .withPassword(password)
33                .withUsername(username)
34                .withClientId("for Security")
35                .build();
36        return customUserDetails;
37    }
38
39    private Map<String, String> getUserServicePostObject(String username, String password) {
40        Map<String, String> requestParam = new HashMap<String, String>();
41        requestParam.put("userName", username);
42        requestParam.put("password", password);
43        return requestParam;
44    }
45
46    //模拟调用user服务的方法
47    private Map checkUsernameAndPassword(Map map) {
48
49        //checkUsernameAndPassword
50        Map ret = new HashMap();
51        ret.put("userId", UUID.randomUUID().toString());
52
53        return ret;
54    }
55
56    @Override
57    public boolean supports(Class<?> aClass) {
58        return true;
59    }
60
61}

AuthenticationManagerConfig添加CustomSecurityAuthenticationProvider配置:

 1@Configuration
 2public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
 3
 4    @Autowired
 5    CustomAuthenticationProvider customAuthenticationProvider;
 6    @Autowired
 7    CustomSecurityAuthenticationProvider securityAuthenticationProvider;
 8
 9    @Override
10    public void configure(AuthenticationManagerBuilder auth) throws Exception {
11        auth.authenticationProvider(customAuthenticationProvider)
12                .authenticationProvider(securityAuthenticationProvider);
13    }
14
15}

保证数据库中的请求客户端存在授权码的请求授权和具备回调地址,回调地址是用来接受授权码的。

测试使用

启动服务,浏览器访问地址http://localhost:9091/oauth/authorize?response_type=code&client_id=frontend& scope=all&redirect_uri=http://localhost:8080

重定向到登录界面,引导用户登录:

登录成功,授权客户端获取授权码。

授权之后,从回调地址中获取到授权码:

1http://localhost:8080/?code=7OglOJ

携带授权码获取对应的token:

源码详解

AuthorizationServerTokenServices是授权服务器中进行token操作的接口,提供了以下的三个接口:

 1public interface AuthorizationServerTokenServices {
 2
 3    // 生成与OAuth2认证绑定的access_token
 4    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
 5
 6    // 根据refresh_token刷新access_token
 7    OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
 8            throws AuthenticationException;
 9
10    // 获取OAuth2认证的access_token,如果access_token存在的话
11    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
12
13}

请注意,生成的token都是与授权的用户进行绑定的。

AuthorizationServerTokenServices接口的默认实现是DefaultTokenServices,注意token通过TokenStore进行保存管理。

生成token:

 1//DefaultTokenServices
 2@Transactional
 3public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
 4    // 从TokenStore获取access_token
 5    OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
 6    OAuth2RefreshToken refreshToken = null;
 7    if (existingAccessToken != null) {
 8        if (existingAccessToken.isExpired()) {
 9            // 如果access_token已经存在但是过期了
10            // 删除对应的access_token和refresh_token
11            if (existingAccessToken.getRefreshToken() != null) {
12                refreshToken = existingAccessToken.getRefreshToken();
13                tokenStore.removeRefreshToken(refreshToken);
14            }
15            tokenStore.removeAccessToken(existingAccessToken);
16        }
17        else {
18            // 如果access_token已经存在并且没有过期
19            // 重新保存一下防止authentication改变,并且返回该access_token
20            tokenStore.storeAccessToken(existingAccessToken, authentication);
21            return existingAccessToken;
22        }
23    }
24
25    // 只有当refresh_token为null时,才重新创建一个新的refresh_token
26    // 这样可以使持有过期access_token的客户端可以根据以前拿到refresh_token拿到重新创建的access_token
27    // 因为创建的access_token需要绑定refresh_token
28    if (refreshToken == null) {
29        refreshToken = createRefreshToken(authentication);
30    }else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
31        // 如果refresh_token也有期限并且过期,重新创建
32        ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
33        if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
34            refreshToken = createRefreshToken(authentication);
35        }
36    }
37    // 绑定授权用户和refresh_token创建新的access_token
38    OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
39    // 将access_token与授权用户对应保存
40    tokenStore.storeAccessToken(accessToken, authentication);
41    // In case it was modified
42    refreshToken = accessToken.getRefreshToken();
43    if (refreshToken != null) {
44        // 将refresh_token与授权用户对应保存
45        tokenStore.storeRefreshToken(refreshToken, authentication);
46    }
47    return accessToken;
48}

需要注意到,在创建token的过程中,会根据该授权用户去查询是否存在未过期的access_token,有就直接返回,没有的话才会重新创建新的access_token,同时也应该注意到是先创建refresh_token,再去创建access_token,这是为了防止持有过期的access_token能够通过refresh_token重新获得access_token,因为前后创建access_token绑定了同一个refresh_token。

DefaultTokenServices中刷新token的refreshAccessToken()以及获取token的getAccessToken()方法就留给读者们自己去查看,在此不介绍。

小结

本文主要讲了授权码模式,在授权码模式需要用户登录之后进行授权才获取获取授权码,再携带授权码去向TokenEndpoint请求访问令牌,当然也可以在请求中设置response_token=token通过隐式类型直接获取到access_token。这里需要注意一个问题,在到达AuthorizationEndpoint端点时,并没有对客户端进行验证,但是必须要经过用户认证的请求才能被接受。

原文发布于微信公众号 - aoho求索(aohoBlog)

原文发表时间:2018-04-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏与神兽党一起成长

我的解决stackoverflow无法正常登陆问题过程

1.从stackoverflow.com中点击login链接,查看network情况,可以发现几处错误。

1212
来自专栏Kirito的技术分享

Re:从零开始的Spring Session(一)

Session和Cookie这两个概念,在学习java web开发之初,大多数人就已经接触过了。最近在研究跨域单点登录的实现时,发现对于Session和Cook...

3447
来自专栏java思维导图

http超文本协议,让http不再难懂(二)

一张导图 ? 导图内容解析 http请求 请求行+请求头(多个key-value对象)+一个空行+实体内容 请求行 请求方法 常见方法:get post he...

3245
来自专栏Netkiller

Elasticsearch Cluster 安装与配置

本文节选自《Netkiller Database 手札》作者:netkiller 网站: http://www.netkiller.cn 23.1.2. Ela...

3565
来自专栏noteless

springmvc 项目完整示例08 前台页面以及知识点总结

<%@ page language="java" contentType="text/html; charset=UTF-8"

680
来自专栏linux运维学习

linux学习第四十篇:访问日志不记录静态文件,访问日志切割,静态元素过期时间

访问日志不记录静态文件 网站大多元素为静态文件,如图片、css、js等,这些元素可以不用记录 。如果不去做限制,每个请求都包含很多图片,每个请求都会记录日志,...

19910
来自专栏草根专栏

Identity Server 4 - Hybrid Flow - Claims

前一篇 Identity Server 4 - Hybrid Flow - MVC客户端身份验证: https://www.cnblogs.com/cgzl/p...

1313
来自专栏圣杰的专栏

ABP入门系列(20)——使用后台作业和工作者

源码路径:Github-LearningMpaAbp 1.引言 说到后台作业,你可能条件反射的想到BackgroundWorker,但后台作业并非是后台任务,...

8447
来自专栏Python

web框架

http协议 HTTP简介 HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World ...

4286
来自专栏Pythonista

Django之COOKIE与SESSION

1、cookie不属于http协议范围,由于http协议无法保持状态,但实际情况,我们却又需要‘保持状态’,因此cookie就是在这个场景下诞生。

1542

扫码关注云+社区

领取腾讯云代金券