spring cloud 微服务下统一认证授权

一. 前置知识

1. 认证授权解决方案

  • 理想的解决方案

    1. 我们理想的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
  • 微服务下的三种访问场景

    1. 外部访问通过gateway,需要鉴权,用户登录后可访问的资源
    2. 外部访问通过gateway,不需要鉴权,比如验证码、静态文件等资源,需要将url加入到spring security的白名单中
    3. 内部服务通过Feign访问,不需要鉴权,可单独进行配置,只允许内部互相调用,不允许通过gateway调用
  • 应用架构
    • micro-auth 统一认证服务器,负责对用户登录请求的认证工作
      spring security + spring Oauth2
    • micro-gateway 资源管理服务器,负责对请求的统一鉴权、转发、管理工作
      spring security + spring Oauth2
    • micro-upms 等 普通服务,不整合 安全模块
  • 微服务间鉴权
    微服务间鉴权,feign调用时,增加请求头标识,标识为feign请求,服务拦截器统一拦截feign请求
    请求经过网关时,请求feign标识请求头,防止请求伪造
  • 网关-微服务,用户信息传递
    网关解析token,获取用户信息,将用户信息放置请求头
    微服务拦截器拦截请求头,获取用户信息,放置ThreadLocal线程池内

二. OAuth2 和 JWT相关知识

1. OAuth2

1.1 什么是Oauth2?

  1. OAUth2就是一套广泛流行的认证授权协议,大白话说呢OAuth2这套协议中有两个核心的角色,认证服务器和资源服务器。

2. JWT

2.1 什么是 jwt?

  1. JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。

2.2 jwt 组成 头部、负载、签名

  • header+payload+signature
    • 头部:主要是描述签名算法
    • 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
    • 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token

三. 认证服务器

1. 依赖

  1. <dependency>
  2. <groupId>org.springframework.security.oauth.boot</groupId>
  3. <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.security</groupId>
  7. <artifactId>spring-security-oauth2-jose</artifactId>
  8. </dependency>

2. 认证服务器配置 (初版,后续更改) (详细使用spring security系列文章)

2.1 Spring security配置

  1. @Configuration
  2. @EnableWebSecurity
  3. public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  4. /**
  5. * 认证中心默认忽略验证地址
  6. */
  7. private static final String[] SECURITY_ENDPOINTS = {
  8. "/auth/**",
  9. "/oauth/token",
  10. "/login/*",
  11. "/actuator/**",
  12. "/v2/api-docs",
  13. "/doc.html",
  14. "/webjars/**",
  15. "**/favicon.ico",
  16. "/swagger-resources/**"
  17. };
  18. @Override
  19. protected void configure(HttpSecurity http) throws Exception {
  20. ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config
  21. = http.requestMatchers().anyRequest()
  22. .and()
  23. .formLogin()
  24. .and()
  25. // .apply(smsCodeAuthenticationSecurityConfig)
  26. // .and()
  27. // .apply(socialAuthenticationSecurityConfig)
  28. // .and()
  29. .authorizeRequests();
  30. List<String> list = new ArrayList<>();
  31. Collections.addAll(list, SECURITY_ENDPOINTS);
  32. list.forEach(url -> {
  33. config.antMatchers(url).permitAll();
  34. });
  35. config
  36. //任何请求
  37. .anyRequest()
  38. //都需要身份认证
  39. .authenticated()
  40. //csrf跨站请求
  41. .and()
  42. .csrf().disable();
  43. }
  44. @Bean
  45. @Override
  46. public AuthenticationManager authenticationManagerBean() throws Exception {
  47. return super.authenticationManagerBean();
  48. }
  49. @Bean
  50. public PasswordEncoder passwordEncoder() {
  51. return new BCryptPasswordEncoder();
  52. }
  53. }

2.2 Spring Oauth2配置 (后续修改,测试使用版本)

  1. @AllArgsConstructor
  2. @Configuration
  3. @EnableAuthorizationServer
  4. public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
  5. @Autowired
  6. private PasswordEncoder passwordEncoder;
  7. @Autowired
  8. private UserServiceImpl userDetailsService;
  9. @Autowired
  10. private AuthenticationManager authenticationManager;
  11. @Autowired
  12. private JwtTokenEnhancer jwtTokenEnhancer;
  13. @Autowired
  14. private WebRespExceptionTranslator webRespExceptionTranslator;
  15. /**
  16. * redis 链接工厂
  17. * */
  18. @Autowired
  19. private RedisConnectionFactory redisConnectionFactory;
  20. /**
  21. * Oauth2 与redis链接
  22. * */
  23. @Bean
  24. public TokenStore tokenStore(){
  25. return new RedisTokenStore(redisConnectionFactory);
  26. }
  27. /**
  28. * 配置第三方应用
  29. * 四种授权码模式
  30. * 1. code码授权 authorization_code
  31. * 2. 静默授权 implicit
  32. * 3. 密码授权 特别信任的第三方应用 password
  33. * 4. 客户端授权 client_credentials
  34. * */
  35. @Override
  36. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  37. clients.inMemory()
  38. .withClient("admin-app") // 第三方应用的客户端id
  39. .secret(passwordEncoder.encode("123456")) //配置第三方应用的密码
  40. .scopes("all") // 配置第三方应用的业务作用域
  41. .authorizedGrantTypes("password", "refresh_token") //四种授权模式
  42. .accessTokenValiditySeconds(3600*24)
  43. .refreshTokenValiditySeconds(3600*24*7)
  44. // 授权后跳转的地址
  45. //.redirectUris("http://www.baidu.com")
  46. .and()
  47. .withClient("portal-app")
  48. .secret("123456")
  49. .scopes("all")
  50. .authorizedGrantTypes("password", "refresh_token")
  51. .accessTokenValiditySeconds(3600*24)
  52. .refreshTokenValiditySeconds(3600*24*7);
  53. }
  54. @Override
  55. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  56. TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
  57. List<TokenEnhancer> delegates = new ArrayList<>();
  58. delegates.add(jwtTokenEnhancer);
  59. delegates.add(accessTokenConverter());
  60. enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器
  61. endpoints.tokenStore(tokenStore())
  62. .authenticationManager(authenticationManager)
  63. .userDetailsService(userDetailsService) //配置加载用户信息的服务
  64. .accessTokenConverter(accessTokenConverter())
  65. .exceptionTranslator(webRespExceptionTranslator)
  66. .tokenEnhancer(enhancerChain);
  67. }
  68. @Override
  69. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  70. security.allowFormAuthenticationForClients();
  71. }
  72. @Bean
  73. public JwtAccessTokenConverter accessTokenConverter() {
  74. JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
  75. jwtAccessTokenConverter.setKeyPair(keyPair());
  76. return jwtAccessTokenConverter;
  77. }
  78. @Bean
  79. public KeyPair keyPair() {
  80. //从classpath下的证书中获取秘钥对
  81. KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("ffzs-jwt.jks"), "ffzs00".toCharArray());
  82. KeyPair keyPair = keyStoreKeyFactory.getKeyPair("ffzs-jwt", "ffzs00".toCharArray());
  83. return keyPair;
  84. }
  85. }

2.3 RSA

  1. 生成密钥库
    使用JDK工具的keytool生成JKS密钥库(Java key Store), 将生成后的文件放到resource目录
    1. keytool -genkey -alias ffzs-jwt -keyalg RSA -keypass ffzs00 -keystore ffzs-jwt.jks -storepass ffzs00
    ``` -genkey 生成密钥

-alias 别名

-keyalg 密钥算法

-keypass 密钥口令

-keystore 生成密钥库的存储路径和名称

-storepass 密钥库口令

  1. 2. 生成公钥文件
  2. <a name="607fd992"></a>
  3. ## 四. 资源服务器
  4. <a name="f80d16f6-1"></a>
  5. ### 1. 依赖
  6. ```xml
  7. <!-- OAuth2资源服务器-->
  8. <dependency>
  9. <groupId>org.springframework.security</groupId>
  10. <artifactId>spring-security-config</artifactId>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.springframework.security</groupId>
  14. <artifactId>spring-security-oauth2-resource-server</artifactId>
  15. </dependency>
  16. <dependency>
  17. <groupId>org.springframework.security</groupId>
  18. <artifactId>spring-security-oauth2-jose</artifactId>
  19. </dependency>
  20. <dependency>
  21. <groupId>com.github.xiaoymin</groupId>
  22. <artifactId>knife4j-spring-boot-starter</artifactId>
  23. </dependency>

2. 资源服务器配置

2.1 资源服务器配置文件(后续jwt公钥验证修改为本地验证)

  1. /**
  2. * 资源服务器配置
  3. * Created by macro on 2020/6/19.
  4. */
  5. @AllArgsConstructor
  6. @Configuration
  7. @EnableWebFluxSecurity
  8. public class ResourceServerConfig {
  9. private final AuthorizationManager authorizationManager;
  10. private final IgnoreUrlsConfig ignoreUrlsConfig;
  11. private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
  12. private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
  13. private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
  14. @Bean
  15. public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
  16. http.oauth2ResourceServer().jwt()
  17. .jwtAuthenticationConverter(jwtAuthenticationConverter());
  18. //自定义处理JWT请求头过期或签名错误的结果
  19. http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);
  20. http.authorizeExchange()
  21. .pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置
  22. .anyExchange().access(authorizationManager)//鉴权管理器配置
  23. .and().exceptionHandling()
  24. .accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权
  25. .authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证
  26. .and().csrf().disable();
  27. return http.build();
  28. }
  29. @Bean
  30. public Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {
  31. JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
  32. jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);
  33. jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);
  34. JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
  35. jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);
  36. return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);
  37. }
  38. }

2.2 鉴权管理器 (逻辑不完善,后续修改)

  1. /**
  2. * 鉴权管理器,用于判断是否有资源的访问权限
  3. * Created by macro on 2020/6/19.
  4. */
  5. @Component
  6. public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
  7. @Autowired
  8. private RedisTemplate<String, Object> redisTemplate;
  9. @Autowired
  10. private IgnoreUrlsConfig ignoreUrlsConfig;
  11. @Override
  12. public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
  13. ServerHttpRequest request = authorizationContext.getExchange().getRequest();
  14. URI uri = request.getURI();
  15. PathMatcher pathMatcher = new AntPathMatcher();
  16. //白名单路径直接放行
  17. List<String> ignoreUrls = ignoreUrlsConfig.getUrls();
  18. for (String ignoreUrl : ignoreUrls) {
  19. if (pathMatcher.match(ignoreUrl, uri.getPath())) {
  20. return Mono.just(new AuthorizationDecision(true));
  21. }
  22. }
  23. //对应跨域的预检请求直接放行
  24. if(request.getMethod()==HttpMethod.OPTIONS){
  25. return Mono.just(new AuthorizationDecision(true));
  26. }
  27. //管理端路径需校验权限
  28. Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY);
  29. Iterator<Object> iterator = resourceRolesMap.keySet().iterator();
  30. List<String> authorities = new ArrayList<>();
  31. while (iterator.hasNext()) {
  32. String pattern = (String) iterator.next();
  33. if (pathMatcher.match(pattern, uri.getPath())) {
  34. authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));
  35. }
  36. }
  37. authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());
  38. //认证通过且角色匹配的用户可访问当前路径
  39. return mono
  40. .filter(Authentication::isAuthenticated)
  41. .flatMapIterable(Authentication::getAuthorities)
  42. .map(GrantedAuthority::getAuthority)
  43. .any(authorities::contains)
  44. .map(AuthorizationDecision::new)
  45. .defaultIfEmpty(new AuthorizationDecision(false));
  46. }
  47. }

2.3 过滤器(逻辑不完善,后续修改)