当前位置:   article > 正文

微服务统一认证中心_微服务认证中心

微服务认证中心

       在微服务架构中,由于不同的业务会拆分成不同的微服务,传统的单体项目一般是通过过滤器进行拦截校验,而微服务显然不可能分发到各个服务进行用户认证,这就需要由一个统一的地方来管理所有服务的认证信息,实现只登录一次,即可在各个服务的授权范围内进行操作;本文采用springcloud-oauth2来实现多个微服务的统一认证,废话不多说,先来个架构图:

OAuth 2 有四种授权模式:

什么情况下需要用 OAuth2

        首先大家最熟悉的就是几乎每个人都用过的,比如用微信登录、用 QQ 登录、用微博登录、用 Google 账号登录、用 github 授权登录等等,这些都是典型的 OAuth2 使用场景。假设我们做了一个自己的服务平台,如果不使用 OAuth2 登录方式,那么我们需要用户先完成注册,然后用注册号的账号密码或者用手机验证码登录。而使用了 OAuth2 之后,相信很多人使用过、甚至开发过公众号网页服务、小程序,当我们进入网页、小程序界面,第一次使用就无需注册,直接使用微信授权登录即可,大大提高了使用效率。因为每个人都有微信号,有了微信就可以马上使用第三方服务,这体验不要太好了。而对于我们的服务来说,我们也不需要存储用户的密码,只要存储认证平台返回的唯一ID 和用户信息即可。 
        以上是使用了 OAuth2 的授权码模式,利用第三方的权威平台实现用户身份的认证。当然了,如果你的公司内部有很多个服务,可以专门提取出一个认证中心,这个认证中心就充当上面所说的权威认证平台的角色,所有的服务都要到这个认证中心做认证

这样一说,发现没,这其实就是个单点登录的功能。这就是另外一种使用场景,对于多服务的平台,可以使用 OAuth2 实现服务的单点登录,只做一次登录,就可以在多个服务中自由穿行,当然仅限于授权范围内的服务和接口。


        OAuth2 其实是一个关于授权的网络标准,它制定了设计思路和运行流程,利用这个标准我们其实是可以自己实现 OAuth2 的认证过程的。今天要介绍的 spring-cloud-starter-oauth2 ,其实是 Spring Cloud 按照 OAuth2 的标准并结合 spring-security 封装好的一个具体实现。接下来看一下系统架构说明:

【登录时序图】

【接口调用时序图】

 

认证服务:OAuth2 主要实现端,Token 的生成、刷新、验证都在认证中心完成。

后台服务: 接收到请求后会到认证中心验证(微服务入口一般是网关)

前端:认证服务、后台服务之间的联调

        上图描述了使用了 前端与OAuth2 认证服务、微服务间的请求过程。大致的过程就是前端用用户名和密码到后台服务登录,成功后后台服务到认证服务端换取 token,返回给前端,前端拿着 token 去各个微服务请求数据接口,一般这个 token 是放到 header 中的。当微服务接到请求后,先要拿着 token 去认证服务端检查 token 的合法性,如果合法,再根据用户所属的角色及具有的权限动态的返回数据。

        接下来,正式进入实战阶段 !~

1. 搭建认证中心auth-center

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-oauth2</artifactId>
  4. <version>2.2.2.RELEASE</version>
  5. </dependency>

启动类开启feign和服务发现注解:

  1. @MapperScan("com.example.dao")
  2. @SpringBootApplication
  3. @EnableFeignClients
  4. @EnableDiscoveryClient
  5. public class AppAuthCenter {
  6. public static void main(String[] args) {
  7. SpringApplication.run(AppAuthCenter.class, args);
  8. }
  9. }
  1. server:
  2. port: 8080
  3. spring:
  4. application:
  5. name: auth-center
  6. cloud:
  7. nacos:
  8. discovery:
  9. server-addr: 127.0.0.1:8848
  10. redis:
  11. database: 2
  12. host: 127.0.0.1
  13. port: 6379
  14. password: 123456
  15. datasource:
  16. type: com.zaxxer.hikari.HikariDataSource
  17. driver-class-name: "com.mysql.cj.jdbc.Driver"
  18. url: jdbc:mysql://127.0.0.1:3306/security?characterEncoding=utf8&characterSetResults=utf8&autoReconnect=true&failOverReadOnly=false
  19. username: root
  20. password: 123456
  21. hikari:
  22. minimum-idle: "0"
  23. auto-commit: "true" # 此属性控制从池返回的连接的默认自动提交行为,默认值:true
  24. pool-name: "springcloud-security-oauth2-jwt" # 连接池名称
  25. max-lifetime: "1800000" # 此属性控制池中连接的最长生命周期,值0表示无限生命周期,默认180000030分钟
  26. connection-timeout: "30000" # 数据库连接超时时间,默认30秒,即30000
  27. connection-test-query: "SELECT 1"
  28. mybatis-plus:
  29. enabled: true #mybatis plus开启 如果没有此配置那么com.cnnho.redfish.common.config.MybatisPlusConfig 配置不起作用
  30. mapper-locations: classpath:mapper/*Mapper.xml
  31. global-config:
  32. db-config:
  33. field-strategy: not-empty #字段策略 0:"忽略判断",1:"非 NULL 判断"),2:"非空判断"
  34. id-type: auto
  35. db-type: mysql
  36. refresh-mapper: "true"
  37. configuration:
  38. cache-enabled: true
  39. map-underscore-to-camel-case: true
  40. log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  41. auto-mapping-unknown-column-behavior: none

1.1 spring security 基础配置:

  1. package com.example.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.security.authentication.AuthenticationManager;
  4. import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  5. import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
  6. import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
  7. import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
  8. import org.springframework.security.crypto.password.PasswordEncoder;
  9. /**
  10. * @author: joybinny
  11. */
  12. @EnableWebSecurity
  13. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  14. @Bean
  15. public PasswordEncoder passwordEncoder() {
  16. return new BCryptPasswordEncoder();
  17. }
  18. @Bean
  19. @Override
  20. public AuthenticationManager authenticationManagerBean() throws Exception {
  21. return super.authenticationManagerBean();
  22. }
  23. /**
  24. * 允许匿名访问所有接口 主要是 oauth 接口
  25. * @param http
  26. * @throws Exception
  27. */
  28. @Override
  29. protected void configure(HttpSecurity http) throws Exception {
  30. http.authorizeRequests()
  31. .antMatchers("/**").permitAll();
  32. }
  33. }

使用 @EnableWebSecurity 注解修饰,并继承自 WebSecurityConfigurerAdapter 类。 这个类的重点就是声明 PasswordEncoder AuthenticationManager两个 Bean。稍后会用到。其中 BCryptPasswordEncoder是一个密码加密工具类,它可以实现不可逆的加密,AuthenticationManager是为了实现 OAuth2 的 password 模式必须要指定的授权管理 Bean。

1.2 实现 UserDetailsService

如果你之前用过 Security 的话,那肯定对这个类很熟悉,它是实现用户身份验证的一种方式,也是最简单方便的一种。另外还有结合 AuthenticationProvider的方式,有机会讲 Security 的时候再展开来讲吧。 UserDetailsService的核心就是 loadUserByUsername方法,它要接收一个字符串参数,也就是传过来的用户名,返回一个 UserDetails对象。

  1. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  2. import com.example.dao.UserDao;
  3. import com.example.entity.UserPo;
  4. import io.micrometer.core.instrument.util.StringUtils;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.security.core.userdetails.UserDetails;
  7. import org.springframework.security.core.userdetails.UserDetailsService;
  8. import org.springframework.security.core.userdetails.UsernameNotFoundException;
  9. import org.springframework.stereotype.Component;
  10. import java.util.ArrayList;
  11. @Component(value = "kiteUserDetailsService")
  12. public class KiteUserDetailsService implements UserDetailsService {
  13. @Autowired
  14. private UserDao userDao;
  15. /**
  16. * Security的登录,User赋予权限
  17. *
  18. * @param username
  19. * @return
  20. * @throws UsernameNotFoundException
  21. */
  22. @Override
  23. public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  24. if (StringUtils.isBlank(username)) {
  25. throw new UsernameNotFoundException("Username is not null");
  26. }
  27. QueryWrapper queryWrapper = new QueryWrapper();
  28. queryWrapper.eq("user_name", username);
  29. /** 只做认证,不做鉴权(如做鉴权,参考下面注释的代码..) */
  30. UserPo user = userDao.selectOne(queryWrapper);
  31. if (null == user) { //校验用户是否存在
  32. throw new UsernameNotFoundException("User is not exist");
  33. }
  34. return new org.springframework.security.core.userdetails.User(username, user.getOauthPassword(), new ArrayList<>());//返回null访问/oauth/token会报错Unauthorized
  35. /* 认证 + 鉴权
  36. String role = user.getRole();
  37. List<SimpleGrantedAuthority> authorities = new ArrayList<>();
  38. authorities.add(new SimpleGrantedAuthority(role));
  39. return new org.springframework.security.core.userdetails.User(username, user.getOauthPassword(), authorities);*/
  40. }
  41. }

1.3 Oauth2配置文件

  1. package com.example.config;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.authentication.AuthenticationManager;
  5. import org.springframework.security.core.userdetails.UserDetailsService;
  6. import org.springframework.security.crypto.password.PasswordEncoder;
  7. import org.springframework.security.oauth2.config.annotation.builders.JdbcClientDetailsServiceBuilder;
  8. import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
  9. import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
  10. import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
  11. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
  12. import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
  13. import org.springframework.security.oauth2.provider.token.TokenEnhancer;
  14. import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
  15. import org.springframework.security.oauth2.provider.token.TokenStore;
  16. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  17. import javax.sql.DataSource;
  18. import java.util.ArrayList;
  19. import java.util.List;
  20. @Configuration
  21. @EnableAuthorizationServer
  22. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  23. /**
  24. * 指定密码的加密方式
  25. */
  26. @Autowired
  27. public PasswordEncoder passwordEncoder;
  28. /**
  29. * 该对象为刷新token提供支持
  30. */
  31. @Autowired
  32. public UserDetailsService kiteUserDetailsService;
  33. /**
  34. * 该对象用来支持password模式
  35. */
  36. @Autowired
  37. private AuthenticationManager authenticationManager;
  38. @Autowired
  39. private TokenStore jwtTokenStore;
  40. @Autowired
  41. private JwtAccessTokenConverter jwtAccessTokenConverter;
  42. @Autowired
  43. private TokenEnhancer jwtTokenEnhancer;
  44. @Autowired
  45. private DataSource dataSource;
  46. /**
  47. * 密码模式下配置认证管理器 AuthenticationManager,并且设置 AccessToken的存储介质tokenStore,如 果不设置,则会默认使用内存当做存储介质。
  48. * 而该AuthenticationManager将会注入 2个Bean对象用以检查(认证)
  49. * 1、ClientDetailsService的实现类 JdbcClientDetailsService (检查 ClientDetails 对象)
  50. * 2、UserDetailsService的实现类 KiteUserDetailsService (检查 UserDetails 对象)
  51. */
  52. @Override
  53. public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  54. /** jwt 增强模式 */
  55. TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
  56. List<TokenEnhancer> enhancerList = new ArrayList<>();
  57. enhancerList.add(jwtTokenEnhancer);
  58. enhancerList.add(jwtAccessTokenConverter);
  59. enhancerChain.setTokenEnhancers(enhancerList);
  60. endpoints.tokenStore(jwtTokenStore)
  61. .userDetailsService(kiteUserDetailsService)
  62. // 支持 password 模式
  63. .authenticationManager(authenticationManager)
  64. .tokenEnhancer(enhancerChain)
  65. .accessTokenConverter(jwtAccessTokenConverter);
  66. }
  67. /**
  68. * 配置 oauth_client_details【client_id和client_secret等】信息的认证【检查ClientDetails的合 法性】服务
  69. * 设置 认证信息的来源:数据库 (可选项:数据库和内存,使用内存一般用来作测试)
  70. * 自动注入:ClientDetailsService的实现类 JdbcClientDetailsService (检查 ClientDetails 对 象)
  71. * 1.inMemory 方式存储的,将配置保存到内存中,相当于硬编码了。正式环境下的做法是持久化到数据库中,比如 mysql 中。
  72. * 2. secret加密是client_id:secret 然后通过base64编码后的字符串
  73. */
  74. @Override
  75. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  76. JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
  77. jcsb.passwordEncoder(passwordEncoder);
  78. }
  79. // @Override
  80. // public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  81. // //添加客户端信息
  82. // //使用内存存储OAuth客服端信息
  83. // clients.inMemory()
  84. // // client_id 客户单ID
  85. // .withClient("order_client")
  86. // // client_secret 客户单秘钥
  87. // .secret(passwordEncoder.encode("order6666"))
  88. // // 该客户端允许的授权类型,不同的类型,则获取token的方式不一样
  89. // .authorizedGrantTypes("refresh_token", "authorization_code", "password")
  90. // // token 有效期
  91. // .accessTokenValiditySeconds(EXPIRE_TIME)
  92. // // 允许的授权范围
  93. // .scopes("all")
  94. // .and()
  95. // .withClient("user_client")
  96. // .secret(passwordEncoder.encode("user8888"))
  97. // .authorizedGrantTypes("refresh_token", "authorization_code", "password")
  98. // .accessTokenValiditySeconds(EXPIRE_TIME)
  99. // .scopes("all");
  100. // }
  101. /**
  102. * 配置:安全检查流程
  103. * 默认过滤器:BasicAuthenticationFilter
  104. * 1、oauth_client_details表中clientSecret字段加密【ClientDetails属性secret】
  105. * 2、CheckEndpoint类的接口 oauth/check_token 无需经过过滤器过滤,默认值:denyAll()
  106. */
  107. @Override
  108. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  109. ///允许客户表单认证
  110. security.allowFormAuthenticationForClients();
  111. //对于CheckEndpoint控制器[框架自带的校验]的/oauth/check端点允许所有客户端发送器请求而不会被 Spring-security拦截
  112. security.checkTokenAccess("permitAll()");
  113. security.tokenKeyAccess("permitAll()");
  114. }
  115. }

认证信息的来源采用数据库的方式,放弃内存模式;这就需要我们提前在数据库生成一张固定模板的表结构:

  1. CREATE TABLE `oauth_client_details` (
  2. `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
  3. `resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  4. `client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  5. `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  6. `authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  7. `web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  8. `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  9. `access_token_validity` int(11) NULL DEFAULT NULL,
  10. `refresh_token_validity` int(11) NULL DEFAULT NULL,
  11. `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  12. `autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  13. PRIMARY KEY (`client_id`) USING BTREE
  14. ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'https://blog.csdn.net/wangxuelei036/article/details/109491215' ROW_FORMAT = Dynamic;
  15. -- ----------------------------
  16. -- Records of oauth_client_details
  17. -- ----------------------------
  18. INSERT INTO `oauth_client_details` VALUES ('order-client', NULL, '$2a$10$lU.YisICL1MQORkGMX6OUuggYZVj2PKZetd8j7PfJgEnQJXZzw9dS', 'all', 'authorization_code,refresh_token,password', NULL, NULL, 60, 36000, NULL, '1');
  19. INSERT INTO `oauth_client_details` VALUES ('user-client', NULL, '$2a$10$ZgTwua6DPOhnI6Q1519AP.YkZsDZThST5qlqu5Wa1kJ7biXzXERvO', 'all', 'authorization_code,refresh_token,password', NULL, NULL, 120, 36000, NULL, '1');

其中密码是用 PasswordEncoder 加密生成,关于该表的详细说明可参考该文章:https://blog.csdn.net/wangxuelei036/article/details/109491215

配置jwt增强器,通过 oAuth2Authentication 可以拿到用户名等信息,通过这些我们可以在这里查询数据库或者缓存获取更多的信息,而这些信息都可以作为 JWT 扩展信息加入其中。

  1. package com.example.config.jwt;
  2. import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
  3. import com.example.dao.UserDao;
  4. import com.example.entity.UserPo;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.security.core.userdetails.User;
  7. import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
  8. import org.springframework.security.oauth2.common.OAuth2AccessToken;
  9. import org.springframework.security.oauth2.provider.OAuth2Authentication;
  10. import org.springframework.security.oauth2.provider.token.TokenEnhancer;
  11. import java.util.HashMap;
  12. import java.util.Map;
  13. public class JWTokenEnhancer implements TokenEnhancer {
  14. @Autowired
  15. private UserDao userDao;
  16. /**
  17. *
  18. * @param oAuth2AccessToken
  19. * @param oAuth2Authentication 根据它获取用户token
  20. * @return
  21. */
  22. @Override
  23. public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
  24. Map<String, Object> info = new HashMap<>();
  25. info.put("jwt-ext", "JWT 扩展信息");
  26. User user = (User) oAuth2Authentication.getPrincipal();
  27. if(user != null){
  28. QueryWrapper queryWrapper = new QueryWrapper();
  29. queryWrapper.eq("user_name", user.getUsername());
  30. /** 只做认证,不做鉴权 */
  31. UserPo userEntity = userDao.selectOne(queryWrapper);
  32. info.put("userPo", userEntity); //也可以只把userId放在附加信息里面
  33. }
  34. ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
  35. return oAuth2AccessToken;
  36. }
  37. }

添加JwtConfig配置类

  1. package com.example.config.jwt;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.security.oauth2.provider.token.TokenStore;
  5. import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
  6. import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
  7. @Configuration
  8. public class JwtTokenConfig {
  9. @Bean
  10. public TokenStore jwtTokenStore() {
  11. return new JwtTokenStore(jwtAccessTokenConverter());
  12. }
  13. @Bean
  14. public JwtAccessTokenConverter jwtAccessTokenConverter() {
  15. JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
  16. accessTokenConverter.setSigningKey("mysecret"); //签名秘钥
  17. return accessTokenConverter;
  18. }
  19. }

JwtAccessTokenConverter 是为了做 JWT 数据转换,这样做是因为 JWT 有自身独特的数据格式。如果没有了解过 JWT ,可以参考本人之前博客:https://blog.csdn.net/AkiraNicky/article/details/99307713

1.4 其它配置

配置全局统一异常处理:

  1. @RestControllerAdvice
  2. public class MyExceptionHandler {
  3. @ExceptionHandler(value = InvalidGrantException.class)
  4. public Result exceptionHandler(InvalidGrantException e) {
  5. return Result.error("用户名密码错误");
  6. }
  7. }

重写check_token:

  1. package com.example.controller;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
  6. import org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RequestParam;
  9. import org.springframework.web.bind.annotation.RestController;
  10. import java.util.Map;
  11. /**
  12. * 重写check_token接口
  13. */
  14. @RestController
  15. @RequestMapping("/oauth")
  16. public class TokenEndpointController {
  17. @Autowired
  18. private CheckTokenEndpoint checkTokenEndpoint;
  19. @RequestMapping("/check_token")
  20. public String checkToken(@RequestParam("token") String token) {
  21. Map<String, ?> stringMap;
  22. try {
  23. stringMap = checkTokenEndpoint.checkToken(token);
  24. } catch (InvalidTokenException e) {
  25. JSONObject err = new JSONObject();
  26. err.put("error", "invalid_token");
  27. err.put("error_description", "Token has expired");
  28. return JSON.toJSONString(err);
  29. }
  30. return JSON.toJSONString(stringMap);
  31. }
  32. }

添加切面,/oauth/token端点请求的结果进行拦截封装处理:

  1. package com.example.aspect;
  2. import com.example.entity.Result;
  3. import org.aspectj.lang.ProceedingJoinPoint;
  4. import org.aspectj.lang.annotation.Around;
  5. import org.aspectj.lang.annotation.Aspect;
  6. import org.springframework.http.HttpStatus;
  7. import org.springframework.http.ResponseEntity;
  8. import org.springframework.security.authentication.InsufficientAuthenticationException;
  9. import org.springframework.security.core.Authentication;
  10. import org.springframework.security.oauth2.common.OAuth2AccessToken;
  11. import org.springframework.security.oauth2.common.util.OAuth2Utils;
  12. import org.springframework.stereotype.Component;
  13. import java.security.Principal;
  14. import java.util.Map;
  15. /**
  16. * 原理就是通过切面编程实现对/oauth/token端点请求的结果进行拦截封装处理,由于/oauth/token是Spring Cloud OAuth2的内部端点,因此需要对相关的Spring源码进行分析。最终定位到
  17. * org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken()
  18. */
  19. @Component
  20. @Aspect
  21. public class AccessTokenAspect {
  22. @Around("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
  23. public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {
  24. Object[] args = joinPoint.getArgs();
  25. Principal principal = (Principal) args[0];
  26. if (!(principal instanceof Authentication)) {
  27. throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
  28. }
  29. Map<String, String> parameters = (Map<String, String>) args[1];
  30. String grantType = parameters.get(OAuth2Utils.GRANT_TYPE);
  31. Object proceed = joinPoint.proceed();
  32. if ("authorization_code".equals(grantType)) {
  33. //如果使用 @EnableOAuth2Sso 注解不能修改返回格式,否则授权码模式可以统一改
  34. return proceed;
  35. } else {
  36. ResponseEntity<OAuth2AccessToken> responseEntity = (ResponseEntity<OAuth2AccessToken>) proceed;
  37. OAuth2AccessToken body = responseEntity.getBody();
  38. return ResponseEntity.status(HttpStatus.OK).body(Result.success(body));
  39. }
  40. }
  41. }

2. 搭建网关Gateway

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-gateway</artifactId>
  4. <version>2.2.8.RELEASE</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-openfeign</artifactId>
  9. <version>2.2.2.RELEASE</version>
  10. </dependency>
  1. server:
  2. port: 8090
  3. spring:
  4. application:
  5. name: gateway
  6. cloud:
  7. nacos:
  8. discovery:
  9. server-addr: 127.0.0.1:8848
  10. gateway:
  11. discovery:
  12. locator:
  13. enabled: true
  14. lower-case-service-id: true
  15. routes:
  16. - id: auth
  17. uri: lb://auth-center
  18. predicates:
  19. - Path=/auth/**
  20. filters:
  21. - StripPrefix=1
  22. - id: order
  23. uri: lb://service-order
  24. predicates:
  25. - Path=/order/**
  26. filters:
  27. - StripPrefix=1
  28. whitelist:
  29. token: "/auth/oauth/token,/user/user/getAuthentication,/order/order/testOrderWithoutToken,/auth/oauth/check_token"
  30. param-sign: ""
  31. blacklist:
  32. token: ""
  33. param-sign: ""
  1. package com.example.config;
  2. import org.springframework.beans.factory.ObjectProvider;
  3. import org.springframework.boot.SpringBootConfiguration;
  4. import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
  5. import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.http.converter.HttpMessageConverter;
  8. import java.util.stream.Collectors;
  9. /**
  10. * feign response 返回数据解析配置
  11. */
  12. @SpringBootConfiguration
  13. public class FeignMessageConfig {
  14. @Bean
  15. @ConditionalOnMissingBean
  16. public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
  17. return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
  18. }
  19. }
  1. @FeignClient(value = "auth-center")
  2. @Component
  3. public interface VerifyTokenFeign {
  4. @RequestMapping(value = "/oauth/check_token", method = RequestMethod.POST)
  5. String verifyToken(@RequestParam(value="token") String token);
  6. }
  1. package com.example.filter;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.example.feign.VerifyTokenFeign;
  5. import io.netty.buffer.UnpooledByteBufAllocator;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.beans.factory.annotation.Value;
  9. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  10. import org.springframework.cloud.gateway.filter.GlobalFilter;
  11. import org.springframework.core.Ordered;
  12. import org.springframework.core.io.buffer.NettyDataBufferFactory;
  13. import org.springframework.http.HttpHeaders;
  14. import org.springframework.http.HttpStatus;
  15. import org.springframework.http.server.reactive.ServerHttpRequest;
  16. import org.springframework.http.server.reactive.ServerHttpResponse;
  17. import org.springframework.stereotype.Component;
  18. import org.springframework.util.AntPathMatcher;
  19. import org.springframework.web.server.ServerWebExchange;
  20. import reactor.core.publisher.Flux;
  21. import reactor.core.publisher.Mono;
  22. import java.nio.charset.StandardCharsets;
  23. /**
  24. * 请求token全局过滤
  25. */
  26. @Component
  27. public class GlobalTokenFilter implements GlobalFilter, Ordered {
  28. private AntPathMatcher antPathMatcher = new AntPathMatcher();
  29. @Autowired
  30. private VerifyTokenFeign verifyTokenFeign;
  31. // 白名单
  32. @Value(value = "${whitelist.token}")
  33. private String whitelist;
  34. @Override
  35. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  36. ServerHttpRequest request = exchange.getRequest();
  37. String urlPath = request.getPath().toString();
  38. boolean action = false;
  39. String[] whitelistArray = whitelist.split(",");
  40. for (String url : whitelistArray) {
  41. if (antPathMatcher.match(url, urlPath)) {
  42. action = true;
  43. break;
  44. }
  45. }
  46. if (action) return chain.filter(exchange); //白名单,放行
  47. String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
  48. if (StringUtils.isNotBlank(token)) {
  49. // token = token.substring(7);
  50. String verifyToken = verifyTokenFeign.verifyToken(token);
  51. if (StringUtils.isNotBlank(verifyToken)) {
  52. JSONObject verifyTokenJson = JSON.parseObject(verifyToken);
  53. if (verifyTokenJson.containsKey("active") && verifyTokenJson.getBoolean("active")) { //JWT验证Token固定格式
  54. return chain.filter(exchange);
  55. }
  56. }
  57. }
  58. return unAuthorized(exchange);
  59. }
  60. /**
  61. * 认证未通过
  62. */
  63. public Mono<Void> unAuthorized(ServerWebExchange exchange) {
  64. ServerHttpResponse response = exchange.getResponse();
  65. response.setStatusCode(HttpStatus.OK);
  66. response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
  67. JSONObject message = new JSONObject();
  68. message.put("code", "-2");
  69. message.put("msg", "登录失效,请重新登录");
  70. return response.writeWith(Flux.create(sink -> {
  71. sink.next(new NettyDataBufferFactory(new UnpooledByteBufAllocator(false)).wrap(message.toJSONString().getBytes(StandardCharsets.UTF_8)));
  72. sink.complete();
  73. }));
  74. }
  75. @Override
  76. public int getOrder() {
  77. return 0;
  78. }
  79. }

3. 测试

新开个order服务,写个测试接口 :

  1. package com.example.controller;
  2. import com.example.entity.Result;
  3. import io.swagger.annotations.Api;
  4. import io.swagger.annotations.ApiOperation;
  5. import org.springframework.web.bind.annotation.PostMapping;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. @Api(tags = "订单模块")
  9. @RestController
  10. @RequestMapping("/order/")
  11. public class OrderController {
  12. @ApiOperation("测试订单-验证token")
  13. @PostMapping("testOrder")
  14. public Result testOrder(){
  15. return Result.success("testOrder");
  16. }
  17. @ApiOperation("测试订单-不验证token")
  18. @PostMapping("testOrderWithoutToken")
  19. public Result testOrderWithoutToken(){
  20. return Result.success("testOrderWithoutToken");
  21. }
  22. }

 由于我在数据库设置的order-client过期时间为一分钟,所以一分钟之内携带token是可以随意请求testOrder的,但是token一旦失效则返回“登录失效,请重新登录”。而testOrderWithoutToken接口在白名单中,gateway过滤器不会校验token,所以不需要token也可以访问:

关于用户无感知刷新token

        用户登录,后端验证用户成功之后生成两个token,这两个token分别是access_token(访问接口使用的token)、refresh_token(access_token过期后用于刷续期的token,注意设置refresh_token的过期时间需比access_token的过期时间长),后端将用户信息和这两个token存放到redis中并返回给前端并存储。

为什么需要刷新令牌?

        如果access token超时时间很长,比如14天,由于第三方软件获取受保护资源都要带着access token,这样access token的攻击面就比较大。如果access token超时时间很短,比如1个小时,那其超时之后就需要用户再次授权,这样的频繁授权导致用户体验不好。引入refresh token,就解决了该矛盾。

什么时候使用刷新令牌呢?

定时检测方式

        在第三方软件收到访问令牌的同时,也会收到访问令牌的过期时间expires_in。一个设计良好的第三方应用,应该将expires_in值保存下来并定时检测;如果发现expires_in即将过期,则需要利用refresh_token去重新请求授权服务,以便获取新的、有效的访问令牌。

现场发现方式

        比如第三方软件访问受保护资源的时候,突然收到一个访问令牌失效的响应,此时第三方软件立即使用refresh_token来请求一个访问令牌,以便继续代表用户使用他的数据。

由于order-client在数据库配置的access_token过期时间为60秒,refresh_token过期时间为36000秒,所以token过期后将请求不到后台资源,此时可以用refresh_token去重新获取token,客户端重新保存token即可,如果refresh_token也过期,让用户重新登录即可。

注意:如果不设置access_token_validity和refresh_token_validity,则会采用默认值:access_token_validity默认60 * 60 * 12 秒(12小时),refresh_token_validity默认默认60 *60 * 24 * 30秒 (30天)

在使用刷新令牌的时候,也是需要应用传递它的app_id和app_sercet的。

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号