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
@EnableWebSecurity
public 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/**"
};
@Override
protected 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
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.2 Spring Oauth2配置 (后续修改,测试使用版本)
@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class Oauth2ServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserServiceImpl userDetailsService;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
@Autowired
private WebRespExceptionTranslator webRespExceptionTranslator;
/**
* redis 链接工厂
* */
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* Oauth2 与redis链接
* */
@Bean
public TokenStore tokenStore(){
return new RedisTokenStore(redisConnectionFactory);
}
/**
* 配置第三方应用
* 四种授权码模式
* 1. code码授权 authorization_code
* 2. 静默授权 implicit
* 3. 密码授权 特别信任的第三方应用 password
* 4. 客户端授权 client_credentials
* */
@Override
public 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);
}
@Override
public 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);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
}
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(keyPair());
return jwtAccessTokenConverter;
}
@Bean
public 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
@EnableWebFluxSecurity
public class ResourceServerConfig {
private final AuthorizationManager authorizationManager;
private final IgnoreUrlsConfig ignoreUrlsConfig;
private final RestfulAccessDeniedHandler restfulAccessDeniedHandler;
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
private final IgnoreUrlsRemoveJwtFilter ignoreUrlsRemoveJwtFilter;
@Bean
public 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();
}
@Bean
public 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.
*/
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private IgnoreUrlsConfig ignoreUrlsConfig;
@Override
public 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));
}
}