朱晔和你聊Spring系列S1E10:强大且复杂的Spring Security(含OAuth2三角色+三模式完整例子)

Spring Security功能多,组件抽象程度高,配置方式多样,导致了Spring Security强大且复杂的特性。Spring Security的学习成本几乎是Spring家族中最高的,Spring Security的精良设计值得我们学习,但是结合实际复杂的业务场景,我们不但需要理解Spring Security的扩展方式还需要去理解一些组件的工作原理和流程(否则怎么去继承并改写需要改写的地方呢?),这又带来了更高的门槛,因此,在决定使用Spring Security搭建整套安全体系(授权、认证、权限、审计)之前还是需要考虑一下将来我们的业务会多复杂,我们徒手写一套安全体系来的划算还是使用Spring Security更好。 短短的一篇文章不可能覆盖Spring Security的方方面面,在最近的工作中会比较多接触OAuth2,因此本文以这个维度来简单阐述一下如果使用Spring Security搭建一套OAuth2授权&SSO架构。 OAuth2简介 OAuth2.0是一套授权体系的开放标准,定义了四大角色: 资源拥有者,也就是用户,由用于授予三方应用权限 客户端,也就是三方应用程序,在访问用户资源之前需要用户授权 资源提供者,或者说资源服务器,提供资源,需要实现Token和ClientID的校验,以及做好相应的权限控制 授权服务器,验证用户身份,为客户端颁发Token,并且维护管理ClientID、Token以及用户 其中后三项都可以是独立的程序,在本文的例子中我们会为这三者建立独立的项目。OAuth2.0标准同时定义了四种授权模式,这里介绍最常用的三种,也是后面会演示的三种(在之后的介绍中令牌=Token,码=Code,可能会混合表达): 不管是哪种模式,通用流程如下: 三方网站(或者说客户端)需要先向授权服务器去申请一套接入的ClientID+ClientSecret 用任意一种模式拿到访问Token(流程见下) 拿着访问Token去资源服务器请求资源 资源服务器根据Token查询到Token对应的权限进行权限控制 授权码模式,最标准最安全的模式,适合和外部交互,流程是: 三方网站客户端转到授权服务器,上送ClientID,授权范围Scope、重定向地址RedirectUri等信息 用户在授权服务器进行登录并且进行授权批准(授权批准这步可以配置为自动完成) 授权完成后重定向回到之前客户端提供的重定向地址,附上授权码 三方网站服务端通过授权码+ClientID+ClientSecret去授权服务器换取Token(Token含访问Token和刷新Token,访问Token过去后用刷新Token去获得新的访问Token) 你可能会问这个模式为什么这么复杂,为什么安全呢?因为我们不会对外暴露ClientSecret,不会对外暴露访问Token,使用授权码换取Token的过程是服务端进行,客户端拿到的只是一次性的授权码 密码凭证模式,适合内部系统之间使用的模式(客户端是自己人,客户端需要拿到用户帐号密码),流程是: 用户提供帐号密码给客户端 客户端凭着用户的帐号密码,以及客户端自己的ClientID+ClientSecret去授权服务器换取Token 客户端模式,适合内部服务端之间使用的模式: 和用户没有关系,不是基于用户的授权 客户端凭着自己的ClientID+ClientSecret去授权服务器换取Token 下面,我们来搭建程序实际体会一下这几种模式。 搭建授权服务器 首先来创建一个父POM,内含三个模块: 4.0.0 me.josephzhu springsecurity101 pom 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.0.6.RELEASE springsecurity101-cloud-oauth2-client springsecurity101-cloud-oauth2-server springsecurity101-cloud-oauth2-userservice UTF-8 UTF-8 1.8 org.projectlombok lombok true org.springframework.cloud spring-cloud-dependencies Finchley.SR2 pom import org.springframework.boot spring-boot-maven-plugin spring-milestones Spring Milestones https://repo.spring.io/libs-milestone false 然后我们创建第一个模块,资源服务器: springsecurity101 me.josephzhu 1.0-SNAPSHOT 4.0.0 springsecurity101-cloud-oauth2-server org.springframework.cloud spring-cloud-starter-oauth2 org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf 这边我们除了使用了Spring Cloud的OAuth2启动器之外还使用数据访问、Web等依赖,因为我们的资源服务器需要使用数据库来保存客户端的信息、用户信息等数据,我们同时也会使用thymeleaf来稍稍美化一下登录页面。 现在我们来创建一个配置文件application.yml: server: port: 8080 spring: application: name: oauth2-server datasource: url: jdbc:mysql://localhost:3306/oauth?useSSL=false username: root password: root driver-class-name: com.mysql.jdbc.Driver 可以看到,我们会使用oauth数据库,授权服务器的端口是8080。 数据库中我们需要初始化一些表: 用户表users:存放用户名密码 授权表authorities:存放用户对应的权限 客户端信息表oauth_client_details:存放客户端的ID、密码、权限、允许访问的资源服务器ID以及允许使用的授权模式等信息 授权码表oauth_code:存放了授权码 授权批准表oauth_approvals:存放了用户授权第三方服务器的批准情况 DDL如下: -- ---------------------------- -- Table structure for authorities -- ---------------------------- DROP TABLE IF EXISTS `authorities`; CREATE TABLE `authorities` ( `username` varchar(50) NOT NULL, `authority` varchar(50) NOT NULL, UNIQUE KEY `ix_auth_username` (`username`,`authority`), CONSTRAINT `fk_authorities_users` FOREIGN KEY (`username`) REFERENCES `users` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for oauth_approvals -- ---------------------------- DROP TABLE IF EXISTS `oauth_approvals`; CREATE TABLE `oauth_approvals` ( `userId` varchar(256) DEFAULT NULL, `clientId` varchar(256) DEFAULT NULL, `partnerKey` varchar(32) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` datetime DEFAULT NULL, `lastModifiedAt` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for oauth_client_details -- ---------------------------- DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(255) NOT NULL, `resource_ids` varchar(255) DEFAULT NULL, `client_secret` varchar(255) DEFAULT NULL, `scope` varchar(255) DEFAULT NULL, `authorized_grant_types` varchar(255) DEFAULT NULL, `web_server_redirect_uri` varchar(255) DEFAULT NULL, `authorities` varchar(255) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additional_information` varchar(4096) DEFAULT NULL, `autoapprove` varchar(255) DEFAULT NULL, PRIMARY KEY (`client_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for users -- ---------------------------- DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `username` varchar(50) NOT NULL, `password` varchar(100) NOT NULL, `enabled` tinyint(1) NOT NULL, PRIMARY KEY (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for oauth_code -- ---------------------------- DROP TABLE IF EXISTS `oauth_code`; CREATE TABLE `oauth_code` ( `code` varchar(255) DEFAULT NULL, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; 在之后演示的时候会看到这些表中的数据。这里可以看到我们并没有在数据库中创建相应的表来存放访问令牌、刷新令牌,这是因为我们之后的实现会把令牌信息使用JWT来传输,不会存放到数据库中。基本上所有的这些表都是可以自己扩展的,只需要继承实现Spring的一些既有类即可,这里不做展开。 下面,我们创建一个最核心的类用于配置授权服务器: package me.josephzhu.springsecurity101.cloud.oauth2.server; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.ClassPathResource; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.crypto.password.NoOpPasswordEncoder; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.approval.JdbcApprovalStore; import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices; import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices; import org.springframework.security.oauth2.provider.token.TokenEnhancer; import org.springframework.security.oauth2.provider.token.TokenEnhancerChain; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import javax.sql.DataSource; import java.util.Arrays; @Configuration @EnableAuthorizationServer public class OAuth2ServerConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired private DataSource dataSource; @Autowired private AuthenticationManager authenticationManager; /** * 代码1 * @param clients * @throws Exception */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } /** * 代码2 * @param security * @throws Exception */ @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.checkTokenAccess("permitAll()") .allowFormAuthenticationForClients().passwordEncoder(NoOpPasswordEncoder.getInstance()); } /** * 代码3 * @param endpoints * @throws Exception */ @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain(); tokenEnhancerChain.setTokenEnhancers( Arrays.asList(tokenEnhancer(), jwtTokenEnhancer())); endpoints.approvalStore(approvalStore()) .authorizationCodeServices(authorizationCodeServices()) .tokenStore(tokenStore()) .tokenEnhancer(tokenEnhancerChain) .authenticationManager(authenticationManager); } @Bean public AuthorizationCodeServices authorizationCodeServices() { return new JdbcAuthorizationCodeServices(dataSource); } @Bean public TokenStore tokenStore() { return new JwtTokenStore(jwtTokenEnhancer()); } @Bean public JdbcApprovalStore approvalStore() { return new JdbcApprovalStore(dataSource); } @Bean public TokenEnhancer tokenEnhancer() { return new CustomTokenEnhancer(); } @Bean protected JwtAccessTokenConverter jwtTokenEnhancer() { KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "mySecretKey".toCharArray()); JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setKeyPair(keyStoreKeyFactory.getKeyPair("jwt")); return converter; } /** * 代码4 */ @Configuration static class MvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("login").setViewName("login"); } } } 分析下这个类: 首先我们可以看到,我们需要通过注解@EnableAuthorizationServer来开启授权服务器 代码片段1中,我们配置了使用数据库来维护客户端信息,当然在各种Demo中我们经常看到的是在内存中维护客户端信息,通过配置直接写死在这里,对于实际的应用我们一般都会用数据库来维护这个信息,甚至还会建立一套工作流来允许客户端自己申请ClientID 代码片段2中,针对授权服务器的安全,我们干了两个事情,首先打开了验证Token的访问权限(以便之后我们演示),然后允许ClientSecret明文方式保存并且可以通过表单提交(而不仅仅是Basic Auth方式提交),之后会演示到这个 代码片段3中,我们干了几个事情: 配置我们的Token存放方式不是内存方式、不是数据库方式、不是Redis方式而是JWT方式,JWT是Json Web Token缩写也就是使用JSON数据格式包装的Token,由.句号把整个JWT分隔为头、数据体、签名三部分,JWT保存Token虽然易于使用但是不是那么安全,一般用于内部,并且需要走HTTPS+配置比较短的失效时间 配置了JWT Token的非对称加密来进行签名 配置了一个自定义的Token增强器,把更多信息放入Token中 配置了使用JDBC数据库方式来保存用户的授权批准记录 代码片段4中,我们配置了登录页面的视图信息(其实可以独立一个配置类更规范) 针对刚才的代码,我们需要补充一些东西到资源目录下,首先需要在资源目录下创建一个templates文件夹然后创建一个login.html登录模板:
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信