spring cloud 微服务下统一认证授权
一. 前置知识
1. 认证授权解决方案
理想的解决方案
我们理想的解决方案应该是这样的,认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。
微服务下的三种访问场景
- 外部访问通过gateway,需要鉴权,用户登录后可访问的资源
- 外部访问通过gateway,不需要鉴权,比如验证码、静态文件等资源,需要将url加入到spring security的白名单中
- 内部服务通过Feign访问,不需要鉴权,可单独进行配置,只允许内部互相调用,不允许通过gateway调用
- 应用架构
- micro-auth 统一认证服务器,负责对用户登录请求的认证工作
spring security + spring Oauth2 - micro-gateway 资源管理服务器,负责对请求的统一鉴权、转发、管理工作
spring security + spring Oauth2 - micro-upms 等 普通服务,不整合 安全模块
- micro-auth 统一认证服务器,负责对用户登录请求的认证工作
- 微服务间鉴权
微服务间鉴权,feign调用时,增加请求头标识,标识为feign请求,服务拦截器统一拦截feign请求
请求经过网关时,请求feign标识请求头,防止请求伪造 - 网关-微服务,用户信息传递
网关解析token,获取用户信息,将用户信息放置请求头
微服务拦截器拦截请求头,获取用户信息,放置ThreadLocal线程池内
二. OAuth2 和 JWT相关知识
1. OAuth2
1.1 什么是Oauth2?
OAUth2就是一套广泛流行的认证授权协议,大白话说呢OAuth2这套协议中有两个核心的角色,认证服务器和资源服务器。
2. JWT
2.1 什么是 jwt?
JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。
2.2 jwt 组成 头部、负载、签名
- header+payload+signature
- 头部:主要是描述签名算法
- 负载:主要描述是加密对象的信息,如用户的id等,也可以加些规范里面的东西,如iss签发者,exp 过期时间,sub 面向的用户
- 签名:主要是把前面两部分进行加密,防止别人拿到token进行base解密后篡改token
三. 认证服务器
1. 依赖
<dependency><groupId>org.springframework.security.oauth.boot</groupId><artifactId>spring-security-oauth2-autoconfigure</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency>
2. 认证服务器配置 (初版,后续更改) (详细使用spring security系列文章)
2.1 Spring security配置
@Configuration@EnableWebSecuritypublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {/*** 认证中心默认忽略验证地址*/private static final String[] SECURITY_ENDPOINTS = {"/auth/**","/oauth/token","/login/*","/actuator/**","/v2/api-docs","/doc.html","/webjars/**","**/favicon.ico","/swagger-resources/**"};@Overrideprotected void configure(HttpSecurity http) throws Exception {ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry config= http.requestMatchers().anyRequest().and().formLogin().and()// .apply(smsCodeAuthenticationSecurityConfig)// .and()// .apply(socialAuthenticationSecurityConfig)// .and().authorizeRequests();List<String> list = new ArrayList<>();Collections.addAll(list, SECURITY_ENDPOINTS);list.forEach(url -> {config.antMatchers(url).permitAll();});config//任何请求.anyRequest()//都需要身份认证.authenticated()//csrf跨站请求.and().csrf().disable();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}}
2.2 Spring Oauth2配置 (后续修改,测试使用版本)
@AllArgsConstructor@Configuration@EnableAuthorizationServerpublic class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate UserServiceImpl userDetailsService;@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtTokenEnhancer jwtTokenEnhancer;@Autowiredprivate WebRespExceptionTranslator webRespExceptionTranslator;/*** redis 链接工厂* */@Autowiredprivate RedisConnectionFactory redisConnectionFactory;/*** Oauth2 与redis链接* */@Beanpublic TokenStore tokenStore(){return new RedisTokenStore(redisConnectionFactory);}/*** 配置第三方应用* 四种授权码模式* 1. code码授权 authorization_code* 2. 静默授权 implicit* 3. 密码授权 特别信任的第三方应用 password* 4. 客户端授权 client_credentials* */@Overridepublic void configure(ClientDetailsServiceConfigurer clients) throws Exception {clients.inMemory().withClient("admin-app") // 第三方应用的客户端id.secret(passwordEncoder.encode("123456")) //配置第三方应用的密码.scopes("all") // 配置第三方应用的业务作用域.authorizedGrantTypes("password", "refresh_token") //四种授权模式.accessTokenValiditySeconds(3600*24).refreshTokenValiditySeconds(3600*24*7)// 授权后跳转的地址//.redirectUris("http://www.baidu.com").and().withClient("portal-app").secret("123456").scopes("all").authorizedGrantTypes("password", "refresh_token").accessTokenValiditySeconds(3600*24).refreshTokenValiditySeconds(3600*24*7);}@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {TokenEnhancerChain enhancerChain = new TokenEnhancerChain();List<TokenEnhancer> delegates = new ArrayList<>();delegates.add(jwtTokenEnhancer);delegates.add(accessTokenConverter());enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager).userDetailsService(userDetailsService) //配置加载用户信息的服务.accessTokenConverter(accessTokenConverter()).exceptionTranslator(webRespExceptionTranslator).tokenEnhancer(enhancerChain);}@Overridepublic void configure(AuthorizationServerSecurityConfigurer security) throws Exception {security.allowFormAuthenticationForClients();}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();jwtAccessTokenConverter.setKeyPair(keyPair());return jwtAccessTokenConverter;}@Beanpublic KeyPair keyPair() {//从classpath下的证书中获取秘钥对KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("ffzs-jwt.jks"), "ffzs00".toCharArray());KeyPair keyPair = keyStoreKeyFactory.getKeyPair("ffzs-jwt", "ffzs00".toCharArray());return keyPair;}}
2.3 RSA
- 生成密钥库
使用JDK工具的keytool生成JKS密钥库(Java key Store), 将生成后的文件放到resource目录
``` -genkey 生成密钥keytool -genkey -alias ffzs-jwt -keyalg RSA -keypass ffzs00 -keystore ffzs-jwt.jks -storepass ffzs00
-alias 别名
-keyalg 密钥算法
-keypass 密钥口令
-keystore 生成密钥库的存储路径和名称
-storepass 密钥库口令
2. 生成公钥文件<a name="607fd992"></a>## 四. 资源服务器<a name="f80d16f6-1"></a>### 1. 依赖```xml<!-- OAuth2资源服务器--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-resource-server</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-jose</artifactId></dependency><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId></dependency>
2. 资源服务器配置
2.1 资源服务器配置文件(后续jwt公钥验证修改为本地验证)
/*** 资源服务器配置* Created by macro on 2020/6/19.*/@AllArgsConstructor@Configuration@EnableWebFluxSecuritypublic class ResourceServerConfig {private final AuthorizationManager authorizationManager;private final IgnoreUrlsConfig ignoreUrlsConfig;private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;@Beanpublic SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());//自定义处理JWT请求头过期或签名错误的结果http.oauth2ResourceServer().authenticationEntryPoint(restAuthenticationEntryPoint);http.authorizeExchange().pathMatchers(ArrayUtil.toArray(ignoreUrlsConfig.getUrls(),String.class)).permitAll()//白名单配置.anyExchange().access(authorizationManager)//鉴权管理器配置.and().exceptionHandling().accessDeniedHandler(restfulAccessDeniedHandler)//处理未授权.authenticationEntryPoint(restAuthenticationEntryPoint)//处理未认证.and().csrf().disable();return http.build();}@Beanpublic Converter<Jwt, ? extends Mono<? extends AbstractAuthenticationToken>> jwtAuthenticationConverter() {JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();jwtGrantedAuthoritiesConverter.setAuthorityPrefix(AuthConstant.AUTHORITY_PREFIX);jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(AuthConstant.AUTHORITY_CLAIM_NAME);JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);return new ReactiveJwtAuthenticationConverterAdapter(jwtAuthenticationConverter);}}
2.2 鉴权管理器 (逻辑不完善,后续修改)
/*** 鉴权管理器,用于判断是否有资源的访问权限* Created by macro on 2020/6/19.*/@Componentpublic class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate IgnoreUrlsConfig ignoreUrlsConfig;@Overridepublic Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {ServerHttpRequest request = authorizationContext.getExchange().getRequest();URI uri = request.getURI();PathMatcher pathMatcher = new AntPathMatcher();//白名单路径直接放行List<String> ignoreUrls = ignoreUrlsConfig.getUrls();for (String ignoreUrl : ignoreUrls) {if (pathMatcher.match(ignoreUrl, uri.getPath())) {return Mono.just(new AuthorizationDecision(true));}}//对应跨域的预检请求直接放行if(request.getMethod()==HttpMethod.OPTIONS){return Mono.just(new AuthorizationDecision(true));}//管理端路径需校验权限Map<Object, Object> resourceRolesMap = redisTemplate.opsForHash().entries(AuthConstant.RESOURCE_ROLES_MAP_KEY);Iterator<Object> iterator = resourceRolesMap.keySet().iterator();List<String> authorities = new ArrayList<>();while (iterator.hasNext()) {String pattern = (String) iterator.next();if (pathMatcher.match(pattern, uri.getPath())) {authorities.addAll(Convert.toList(String.class, resourceRolesMap.get(pattern)));}}authorities = authorities.stream().map(i -> i = AuthConstant.AUTHORITY_PREFIX + i).collect(Collectors.toList());//认证通过且角色匹配的用户可访问当前路径return mono.filter(Authentication::isAuthenticated).flatMapIterable(Authentication::getAuthorities).map(GrantedAuthority::getAuthority).any(authorities::contains).map(AuthorizationDecision::new).defaultIfEmpty(new AuthorizationDecision(false));}}
