使用spring could security实现OAuth2来控制服务中api的安全
使用spring could oauth
OAuth2 在服务提供者上可分为两类: 授权认证服务:AuthenticationServer 资源获取服务:ResourceServer 两者也可以在同一个服务上,但就微服务而言,应该在不同的服务中,本项目api-server为资源获取服务security-server为授权认证服务
授权认证服务的作用,可参考上一节:
1. 获取第三方应用发送的授权码(code)以及第三方应用标识2. 根据授权码及标识进行校验3. 校验通过,发送令牌(Access Token) 简单用上一节的例子,第一步就是点击微信第三方登陆的url,请求参数带有client_id(第三方用户的id(可理解为账号))client_secret(第三方应用和授权服务器之间的安全凭证(可理解为密码)。除此还会带有redirect_uri中的回调链接,微信服务会生成相关用户凭证,并在其回调链接上附带code 第二步中,授权服务器(微信),首先会校验第三方服务器(比如简书平台)的真实可靠信接着会根据授权码(code)进行校验客户是否已认证 第三步,通过第二步授权码code认证通过后,生成token,通过回调地址返回(MD5类型,uuid类型,jwt类型等)
令牌的生成和管理
创建AccessToken,并保存,以备后续请求访问都可以认证成功并获取到资源 AccessToken还有一个潜在功能,就是使用jwt生成token时候,可以用来加载一些信息,把一些相关权限等包含在AccessToken中 创建方法:1. 可实现AuthorizationServerTokenServices 接口提供了对AccessToken的相关操作创建、刷新、获取2. spring就默认为我们提供了一个默认的DefaultTokenServices,提供一些基础的操作token 保存方法,创建AccessToken完之后,除了发放给第三方,肯定还得保存起来:1. inMemoryTokenStore:这个是OAuth2默认采用的实现方式。在单服务上可以体现出很好特效(即并发量不大,并且它在失败的时候不会进行备份),大多项目都可以采用此方法。毕竟存在内存,而不是磁盘中,调试简易。2. JdbcTokenStore:这个是基于JDBC的实现,令牌(Access Token)会保存到数据库。这个方式,可以在多个服务之间实现令牌共享。3. JwtTokenStore:jwt全称 JSON Web Token。这个实现方式不用管如何进行存储(内存或磁盘),因为它可以把相关信息数据编码存放在令牌里。JwtTokenStore 不会保存任何数据,但是它在转换令牌值以及授权信息方面与 DefaultTokenServices 所扮演的角色是一样的。但有两个缺点: 撤销一个已经授权的令牌会很困难,因此只适用于处理一个生命周期较短的以及撤销刷新令牌。 令牌占用空间大,如果加入太多用户凭证信息,会存在传输冗余
端点接入
授权认证是使用AuthorizationEndpoint这个端点来进行控制,一般使用AuthorizationServerEndpointsConfigurer 来进行配置。
1. 端点(endpoints)的相关属性配置: authenticationManager:认证管理器。若我们上面的Grant Type设置为password,则需设置一个AuthenticationManager对象 userDetailsService:若是我们实现了UserDetailsService,来管理用户信息,那么得设我们的userDetailsService对象 authorizationCodeServices:授权码服务。若我们上面的Grant Type设置为authorization_code,那么得设一个AuthorizationCodeServices对象 tokenStore:这个就是我们上面说到,把我们想要是实现的Access Token类型设置 accessTokenConverter:Access Token的编码器。也就是JwtAccessTokenConverter tokenEnhancer:token的拓展。当使用jwt时候,可以实现TokenEnhancer来进行jwt对包含信息的拓展 tokenGranter:当默认的Grant Type已经不够我们业务逻辑,实现TokenGranter 接口,授权将会由我们控制,并且忽略Grant Type的几个属性。 2. 端点(endpoints)的授权url: 要授权认证,肯定得由url请求,才可以传输。因此OAuth2提供了配置授权端点的URL。 AuthorizationServerEndpointsConfigurer ,还是这个配置对象进行配置,其中由一个pathMapping()方法进行配置授权端点URL路径 默认实现 /oauth/authorize:授权端点 /oauth/token:令牌端点 /oauth/confirm_access:用户确认授权提交端点 /oauth/error:授权服务错误信息端点 /oauth/check_token:用于资源服务访问的令牌解析端点 /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
使用Oauth2的授权码模式实战
首先创建一个安全服务spring security,用于控制身份验证和授权。
增加pom依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency>
在启动类上启用@EnableAuthorizationServer表示启用授权服务器,可参照如下配置:
//启用资源服务器@SpringBootApplication@RestController@EnableAuthorizationServer@ComponentScan("com.xzg.security.service")public class SecurityApp {public static void main(String[] args) { SpringApplication.run(SecurityApp.class, args); } }
配置web security服务
@Configurationpublic class WebSecurityConfigurer extends WebSecurityConfigurerAdapter { @Autowired private BaseUserDetailService baseUserDetailService; //Spring Security 4.x -> 5.x 會無法直接注入AuthenticationManager,下面解決 @Bean(name = BeanIds.AUTHENTICATION_MANAGER) @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 用户验证 * @param auth */ @Override public void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(daoAuthenticationProvider()); auth.userDetailsService(baseUserDetailService) .passwordEncoder(passwordEncoder()); } /** * @param http * WebSecurityConfigurerAdapter和ResourceServerConfigurerAdapter二者是分工协作的 * @throws Exception * WebSecurityConfigurerAdapter不拦截oauth要开放的资源 */ @Override public void configure(HttpSecurity http) throws Exception { http // 配置登陆页/login并允许访问 .formLogin().permitAll() // 登出页 .and().logout().logoutUrl("/logout").logoutSuccessUrl("/") // 其余所有请求全部需要鉴权认证 .and().authorizeRequests().anyRequest().authenticated() // 由于使用的是JWT,我们这里不需要csrf .and().csrf().disable(); } @Bean public DaoAuthenticationProvider daoAuthenticationProvider(){ DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); // 设置userDetailsService provider.setUserDetailsService(baseUserDetailService); // 禁止隐藏用户未找到异常 provider.setHideUserNotFoundExceptions(false); // 使用BCrypt(BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥)进行密码的hash provider.setPasswordEncoder(passwordEncoder()); return provider; } @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
配置oauth2授权服务器
/** * @author xzg * 授权认证服务:AuthenticationServer */@Configurationpublic class OAuth2Config extends AuthorizationServerConfigurerAdapter { @Autowired private BaseUserDetailService userDetailsService; @Autowired private AuthenticationManager authenticationManager; /** * @param endpointsConfigurer * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。 * @throws Exception * 配置令牌 管理 (jwtAccessTokenConverter) */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.authenticationManager(authenticationManager) // 配置JwtAccessToken转换器 .accessTokenConverter(jwtAccessTokenConverter()) // refresh_token需要userDetailsService //走password的就是用AuthorizationServerEndpointsConfigurer中配置的userDetailsService来进行认证 .reuseRefreshTokens(false) .userDetailsService(userDetailsService); //.tokenStore(getJdbcTokenStore()); } /** * @param clientDetailsServiceConfigurer * ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService) * 客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。 * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception { // Using hardcoded inmemory mechanism because it is just an example clientDetailsServiceConfigurer .inMemory()//使用方法代替in-memory、JdbcClientDetailsService、jwt// client_id: 用来标识客户的Id。第三方用户的id(可理解为账号) .withClient("client")// client_secret:第三方应用和授权服务器之间的安全凭证(可理解为密码) //(需要值得信任的客户端)客户端安全码 .secret(BCryptUtil.encodePassword("password")) .accessTokenValiditySeconds(7200) .authorizedGrantTypes("authorization_code", "refresh_token", "client_credentials", "implicit", "password")// .scopes("app"); .authorities("ROLE_USER") .scopes("apiAccess"); } /** * @param security * AuthorizationServerSecurityConfigurer:用来配置令牌端点(Token Endpoint)的安全约束 * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security // 开启/oauth/token_key验证端口无权限访问 .tokenKeyAccess("permitAll()") // 开启/oauth/check_token验证端口认证权限访问 .checkTokenAccess("isAuthenticated()") .passwordEncoder(new BCryptPasswordEncoder())// 请求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter .allowFormAuthenticationForClients(); } /** * 使用非对称加密算法来对Token进行签名 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter() { final JwtAccessTokenConverter converter = new JwtAccessToken(); // 导入证书 KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "password".toCharArray()); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("selfsigned")); return converter; } }
为了测试springsecurity方便,去掉spring默认的用户密码替换硬编码(用户密码client和password)。可根据业务调整
@Servicepublic class BaseUserDetailService implements UserDetailsService { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.info("获取登陆信息:" + username); // 调用FeignClient查询用户略... //注意:实际生产应该从数据库中获取,用户名,密码(为BCryptPasswordEncoder hash后的密码) if (!"client".equals(username)) { logger.error("找不到该用户,用户名:" +username); throw new UsernameNotFoundException("找不到该用户,用户名:" + username); } //密码硬编码 String password = BCryptUtil.encodePassword("password"); // 获取用户权限列表 List<GrantedAuthority> authorities = CreatHardRole.createAuthorities().get(); // 返回带有用户权限信息的User org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(username, password, true, true, true, true, authorities); return new BaseUserDetail(new BaseUser(username, password), user); } }
其他服务以及jwt token参考源码
配置文件
info: component: Security Server# password 为生成证书的密码server: port: 9001 ssl: key-store: classpath:keystore.jks key-store-password: password key-password: password# contextPath表示上下文;路径 contextPath: /auth# 暂时使用硬编码security: user: password: password logging: level: org.springframework.security: DEBUG
除此之外,在security-server服务启用https,加密传输的方式,配置如下:
1) 创建证书嵌入到项目中
keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -ext san=dns:localhost -storepass password -validity 365 -keysize 2048
执行过程如下图:
制作开发证书
将证书放入项目,并配置如图:
图片.png
注意:使用-ext来定义主题设备名称(san). 可以使用浏览器或者Openssl下载证书,
启动security-serve测试
在浏览器中
在浏览器中请求:https://localhost:9001/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com
如果未登陆则spring security会让登陆,这里使用程序中的硬编码
图片.png
登陆后,选择授权authorization,如下图
authorization
authorization服务会转发到baidu的url并附code,如下图
图片.png
获取code发送去获取token。 下面命令中'Authorization: Basic Y2xpZW50OmNsaWVudHNlY3JldA==' 基于插件。可使用post
curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' -i https://localhost:9001/oauth/token --data 'grant_type=authorization_code&client_id=client&redirect_uri=http://www.baidu.com&code=GXz7W9'
响应token结果如下图
图片.png
至此获取token后,就可以使用token去请求资源服务器的api了
下一节中,资源服务器和zuul边缘服务器一起构成整个微服务的api网关的权限控制
作者:勃列日涅夫
链接:https://www.jianshu.com/p/6e11e6ab0a85