-
OAuth2介绍
OAuth(开放授权)是⼀个开放协议/标准,允许⽤户授权第三⽅应⽤访问他们存储 在另外的服务提供者上的信息,⽽不需要将⽤户名和密码提供给第三⽅应⽤或分享 他们数据的所有内容。允许⽤户授权第三⽅应⽤访问他们存储在另外的服务提供者上的信息,⽽不需要将 ⽤户名和密码提供给第三⽅应⽤或分享他们数据的所有内容
-
Auth2的颁发Token授权⽅式
授权码(authorization-code)
密码式(password)提供⽤户名+密码换取token令牌
隐藏式(implicit)
客户端凭证(client credentials)
授权码模式使⽤到了回调地址,是最复杂的授权⽅式,微博、微信、QQ等第三⽅登 录就是这种模式。我们重点讲解接⼝对接中常使⽤的password密码模式(提供⽤户 名+密码换取token)。 -
Spring Cloud OAuth2介绍
Spring Cloud OAuth2 是 Spring Cloud 体系对OAuth2协议的实现,可以⽤来做多 个微服务的统⼀认证(验证身份合法性)授权(验证权限)。通过向OAuth2服务 (统⼀认证授权服务)发送某个类型的grant_type进⾏集中认证和授权,从⽽获得 access_token(访问令牌),⽽这个token是受其他微服务信任的。
注意:使⽤OAuth2解决问题的本质是,引⼊了⼀个认证授权层,认证授权层连接了 资源的拥有者,在授权层⾥⾯,资源的拥有者可以给第三⽅应⽤授权去访问我们的 某些受保护资源。
-
搭建认证服务器(Authorization Server)
1)pom<!--导⼊Eureka Client依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--导⼊spring cloud oauth2依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <exclusions> <exclusion> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.11.RELEASE</version> </dependency> <!--导⼊security对oauth2的支持--> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.11.RELEASE</version> </dependency>
2) yml
server: port: 9999 Spring: application: name: cloud-oauth-server eureka: client: serviceUrl: # eureka server的路径 defaultZone: http://CloudEurekaServerA:8761/eureka,http://CloudEurekaServerB:8762/eureka instance: #服务实例中显示ip,而不是显示主机名(兼容老的eureka版本) prefer-ip-address: true instance-id: ${ spring.cloud.client.ip-address}:${ spring.application.name}:${ server.port}:@project.version@
3)启动类
@SpringBootApplication @EnableDiscoveryClient public class OauthServerApplication9999 { public static void main(String[] args) { SpringApplication.run(OauthServerApplication9999.class, args); } }
4)config认证服务器安全配置类
/** * @author Lossdate * * 当前类为Oauth2 server的配置类 * (需要继承特定的父类 AuthorizationServerConfigurerAdapter) * * EnableAuthorizationServer : 开启认证服务器功能 */ @Configuration @EnableAuthorizationServer public class OauthServerConfig extends AuthorizationServerConfigurerAdapter { private final String sign_key = "lossdate123"; private final AuthenticationManager authenticationManager; @Autowired public OauthServerConfig(AuthenticationManager authenticationManager) { this.authenticationManager = authenticationManager; } /** * 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等) * 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置 */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); // 相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问该接口 security // 允许客户端表单认证 .allowFormAuthenticationForClients() // 开启端口/oauth/token_key的访问权限(允许) .tokenKeyAccess("permitAll()") // 开启端口/oauth/check_token的访问权限(允许) .checkTokenAccess("permitAll()"); } /** * 客户端详情配置, * 比如client_id,secret * 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进行登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网 * 颁发client_id等必要参数,表明客户端是谁 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { super.configure(clients); // 从内存中加载客户端详情 clients //客户端信息存储在什么地方,可以在内存中,可以在数据库里 .inMemory() //添加一个client配置,指定其client_id .withClient("client_lossdate") //指定客户端的密码/安全码 .secret("abcxyz") //指定客户端所能访问资源id清单,此处的资源id是需要在具体的资源服务器上也配置一样 .resourceIds("autodeliver") //认证类型/令牌颁发模式,可以配置多个在这里,但是不一定都用,具体使用哪种方式颁发token,需要客户端调用的时候传递参数指定 .authorizedGrantTypes("password", "refresh_token") //客户端的权限范围,此处配置为all全部即可 .scopes("all"); } /** * 认证服务器是玩转token的,那么这里配置token令牌管理相关 * (token此时就是一个字符串,当下的token需要在服务器端存储,那么存储在哪里呢?都是在这里配置) */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { super.configure(endpoints); endpoints //指定token的存储方法 .tokenStore(tokenStore()) //token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等 .tokenServices(authorizationServerTokenServices()) // 指定认证管理器,随后注入一个到当前类使用即可 .authenticationManager(authenticationManager) //指定认证管理器,随后注入一个到当前类使用即可 .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); } /** * 该方法用于创建tokenStore对象(令牌存储对象) * token以什么形式存储 */ public TokenStore tokenStore() { // 使用jwt令牌 return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 返回jwt令牌转换器(帮助生成jwt令牌的) * 在这里,可以把签名密钥传递进去给转换器对象 */ private JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); // 签名密钥 jwtAccessTokenConverter.setSigningKey(sign_key); // 验证时使用的密钥,和签名密钥保持一致 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); return jwtAccessTokenConverter; } /** * 该方法用户获取一个token服务对象(该对象描述了token有效期等信息) */ public AuthorizationServerTokenServices authorizationServerTokenServices() { // 使用默认实现 DefaultTokenServices defaultTokenServices = new DefaultTokenServices(); // 是否开启令牌刷新 defaultTokenServices.setSupportRefreshToken(true); defaultTokenServices.setTokenStore(tokenStore()); // 针对jwt令牌的添加 defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter()); // 设置令牌有效时间(一般设置为2个小时),这里设置20s方便测试; access_token就是我们请求资源需要携带的令牌 defaultTokenServices.setAccessTokenValiditySeconds(20); // 设置刷新令牌的有效时间 3天 defaultTokenServices.setRefreshTokenValiditySeconds(259200); return defaultTokenServices; } }
关于三个configure⽅法
-
configure(ClientDetailsServiceConfigurer clients)
⽤来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这 ⾥进⾏初始化,你能够把客户端详情信息写死在这⾥或者是通过数据库来存 储调取详情信息
-
configure(AuthorizationServerEndpointsConfigurer endpoints)
⽤来配置令牌(token)的访问端点和令牌服务(token services)
-
configure(AuthorizationServerSecurityConfigurer oauthServer)
⽤来配置令牌端点的安全约束.
关于 TokenStore
-
InMemoryTokenStore
默认采⽤,它可以完美的⼯作在单服务器上(即访问并发量 压⼒不⼤ 的情况下,并且它在失败的时候不会进⾏备份),⼤多数的项⽬都可以 使⽤这个版本的实现来进⾏ 尝试,你可以在开发的时候使⽤它来进⾏ 管理,因为不会被保存到磁盘中,所以更易于调试。
-
JdbcTokenStore
这是⼀个基于JDBC的实现版本,令牌会被保存进关系型数据库。使⽤ 这个版本的实现时, 你可以在不同的服务器之间共享令牌信息,使⽤ 这个版本的时候请注意把"spring-jdbc"这个依赖加⼊到你的 classpath 当中。
-
JwtTokenStore
这个版本的全称是 JSON Web Token(JWT),它可以 把令牌相关的数据进⾏编码(因此对于后端服务来说,它不需要进⾏存 储,这将是⼀个重⼤优势),缺点就是这个令牌占⽤的空间会⽐较⼤, 如果你加⼊了⽐较多⽤户凭证信息,JwtTokenStore 不会保存任何数据。
5)认证服务器安全配置类
/** * @author Lossdate * * 该配置类,主要处理用户名和密码的校验等事宜 */ @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 注册一个认证管理器对象到容器 */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 密码编码对象(密码不进行加密处理) */ public PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } /** * 处理用户名和密码验证事宜 * 1)客户端传递username和password参数到认证服务器 * 2)一般来说,username和password会存储在数据库中的用户表中 * 3)根据用户表中数据,验证当前传递过来的用户信息的合法性 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 在这个方法中就可以去关联数据库了,当前我们先把用户信息配置在内存中 // 实例化一个用户对象(相当于数据表中的一条用户记录) UserDetails user = new User("admin", "123456", new ArrayList<>()); auth.inMemoryAuthentication() .withUser(user).passwordEncoder(passwordEncoder()); } }
6)测试
获取token:http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=admin&password=123456&client_id=client_lossdate- endpoint:/oauth/token
- 获取token携带的参数
client_id:客户端id
client_secret:客户单密码
grant_type:指定使⽤哪种颁发类型,password
username:⽤户名
password:密码
校验token:localhost:9999/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXV0b2RlbGl2ZXIiXSwiZXhwIjoxNjE1MDQyNzQzLCJ1c2VyX25hbWUiOiJhZG1pbiIsImp0aSI6ImI4MTJkY2MxLWQ0YTctNDMyMy1iOWRhLTY0MDJhNDhhMDgxYSIsImNsaWVudF9pZCI6ImNsaWVudF9sb3NzZGF0ZSIsInNjb3BlIjpbImFsbCJdfQ.MsD0xp8aY5oeI4YWhB-j9fNq_jBnG_BBQnHHKxr78ak
刷新token:localhost:9999/oauth/token?grant_type=refresh_token&client_id=client_lossdate&client_secret=abcxyz&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXV0b2RlbGl2ZXIiXSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiYjgxMmRjYzEtZDRhNy00MzIzLWI5ZGEtNjQwMmE0OGEwODFhIiwiZXhwIjoxNjE1MzAxOTIzLCJqdGkiOiJiZWFmZGQyZi1jOGNkLTRiODUtYjc4Mi0xZWZiZTNiZTI3MjYiLCJjbGllbnRfaWQiOiJjbGllbnRfbG9zc2RhdGUifQ.uMFwq4taobrPAh1pThEvw8L47jwdDX8rDUmzeIJ9Lkg
-
资源服务器(希望访问被认证的微服务)Resource Server配置
1)在需要认证的服务的pom里新增<!--导⼊spring cloud oauth2依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> <exclusions> <exclusion> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.11.RELEASE</version> </dependency> <!--导⼊security对oauth2的支持--> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.11.RELEASE</version> </dependency>
2)config类
/** * @author Lossdate * * 开启资源服务器功能 * EnableResourceServer : 开启web访问安全 * EnableWebSecurity : 开启web访问安全 */ @Configuration @EnableResourceServer @EnableWebSecurity public class ResourceServerConfig extends ResourceServerConfigurerAdapter { // jwt签名密钥 private final String sign_key = "lossdate123"; /** * 该方法用于定义资源服务器向远程认证服务器发起请求,进行token校验等事宜 */ @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { // jwt令牌改造 resources .resourceId("autodeliver") .tokenStore(tokenStore()) // 无状态设置 .stateless(true); } /** * 该方法用于创建tokenStore对象(令牌存储对象) * token以什么形式存储 */ public TokenStore tokenStore() { // 使用jwt令牌 return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 返回jwt令牌转换器(帮助我们生成jwt令牌的) * 在这里,我们可以把签名密钥传递进去给转换器对象 */ private JwtAccessTokenConverter jwtAccessTokenConverter() { JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter(); // 签名密钥 jwtAccessTokenConverter.setSigningKey(sign_key); // 验证时使用的密钥,和签名密钥保持一致 jwtAccessTokenConverter.setVerifier(new MacSigner(sign_key)); return jwtAccessTokenConverter; } /** * 场景:一个服务中可能有很多资源(API接口) * 某一些API接口,需要先认证,才能访问 * 某一些API接口,压根就不需要认证,本来就是对外开放的接口 * 我们就需要对不同特点的接口区分对待(在当前configure方法中完成),设置是否需要经过认证 */ @Override public void configure(HttpSecurity http) throws Exception { http // 设置session的创建策略(根据需要创建即可) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests() // autoDeliver为前缀的请求需要认证 .antMatchers("/autoDeliver/**").authenticated() // demo为前缀的请求需要认证 .antMatchers("/demo/**").authenticated() // 其他请求不认证 .anyRequest().permitAll(); } }
3)测试
无token: http://localhost:8096/autoDeliver/checkState/1545132
有token: http://localhost:8096/autoDeliver/checkState/1545132?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYXV0b2RlbGl2ZXIiXSwiZXhwIjoxNjE1MDQzOTI5LCJ1c2VyX25hbWUiOiJhZG1pbiIsImp0aSI6ImI0Nzg3YmI0LTk0N2YtNGJmMi1iNjI3LTUzMTkwNDRlODBkZiIsImNsaWVudF9pZCI6ImNsaWVudF9sb3NzZGF0ZSIsInNjb3BlIjpbImFsbCJdfQ.Phw4RgEATdHY6I34ZXrJLyXjZyLiWFwvXXQ8OLLR9pg
-
从数据库加载Oauth2客户端信息
1)创建数据表并初始化数据(表名及字段保持固定)
数据库名:oauth2
表:oauth_client_detailsSET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for oauth_client_details -- ---------------------------- DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(48) NOT NULL, `resource_ids` varchar(256) DEFAULT NULL, `client_secret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `authorized_grant_types` varchar(256) DEFAULT NULL, `web_server_redirect_uri` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(256) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Records of oauth_client_details -- ---------------------------- BEGIN; INSERT INTO `oauth_client_details` VALUES ('client_lossdate123', 'autodeliver,resume', 'abcxyz', 'all', 'password,refresh_token', NULL, NULL, 7200, 259200, NULL, NULL); COMMIT; SET FOREIGN_KEY_CHECKS = 1;
表:user
SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES (1, 'root', '123456'); SET FOREIGN_KEY_CHECKS = 1;
2)认证服务器cloud-oauth-server-9999修改
-
pom添加mysql依赖
<!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- 事务控制 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency>
-
yml添加数据库配置
Spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/oauth2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=UTC username: root password: 123456 druid: initialSize: 10 minIdle: 10 maxActive: 30 maxWait: 50000
-
config认证服务器安全配置类修改 -> OauthServerConfig
-
service + dao
/** * @author Lossdate */ @Service public class JdbcUserDetailService implements UserDetailsService { private final UserRepository userRepository; public JdbcUserDetailService(UserRepository userRepository) { this.userRepository = userRepository; } /** * 根据username查询出该用户的所有信息,封装成UserDetails类型的对象返回,至于密码,框架会自动匹配 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), new ArrayList<>()); } }
/** * @author Lossdate */ public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); }
-
认证服务器安全配置类修改 -> SecurityConfig
-
验证
client_id : client_lossdate123
username : root
password : 123456
http://localhost:9999/oauth/token?client_secret=abcxyz&grant_type=password&username=root&password=123456&client_id=client_lossdate123
-