赞
踩
目录
Spring Cloud Security + OAuth 2.0 + JWT的应用
应用访问安全性基本都是围绕认证(Authentication)和授权(Authorization)两大核心概念。
首先确定用户身份(对用户进行认证),确认身份后再确定用户是否有访问指定资源的权限,即身份认证是验证身份的过程,而授权是验证是否有权访问的过程。
举个例子,坐飞机登记之前需要身份证和机票:身份证是为了证明张三确实是张三,这就是Authentication;机票是为了证明张三确实买了票,可以上飞机,这就是Authorization。
主流认证的解决方案是OAuth 2.0,授权的解决方案有Spring Security和Shiro。在公司里在不同的项目里都有使用过。
OAuth 2.0 的标准是 RFC 6749 文件,是一个开放的、安全的用户认证协议,通过认证用户身份并颁发token(令牌),使得第三方应用可以在限定时间、限定范围使用该令牌访问指定资源。
(1)用户打开客户端以后,客户端要求用户给予授权。
(2)用户同意给予客户端授权。
(3)客户端使用上一步获得的授权,向认证服务器申请令牌。
(4)认证服务器对客户端进行认证以,确认无误后同意发放令牌。
(5)客户端使用令牌,向资源服务器申请获取资源。
(6)资源服务器确认令牌无误,同意向客户端开放资源。
从OAuth 2.0 授权流程可以看出,授权涉及4种角色:
从OAuth 2.0 授权流程可以看出,客户端(第三方应用)需要得到用户的授权(Authorization Grant)才能获得令牌(Access Token)。
OAuth 2.0 定义了4种授权方式:授权码模式(Authorization Code)、简化模式(Implicit)、密码模式(Resource Owner Password Credentials)和客户端模式(Client Credentials)。
注意:不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
凭证式(client credentials),
适用于没有前端的命令行应用,即在命令行下请求令牌。这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
第一步,A 应用在命令行向 B 发出请求。grant_type
参数等于client_credentials
表示采用凭证式,client_id
和client_secret
用来让 B 确认 A 的身份。第二步,B 网站验证通过以后,直接返回令牌。
eg: https://xxx/auth-server/oauth/token?grant-type=client-credentials
授权码模式(Authorization Code)
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
- https://b.com/oauth/authorize?
- response_type=code&
- client_id=CLIENT_ID&
- redirect_uri=CALLBACK_URL&
- scope=read
上面 URL 中,response_type
参数表示要求返回授权码(code
),client_id
参数让 B 知道是谁在请求,redirect_uri
参数是 B 接受或拒绝请求后的跳转网址,scope
参数表示要求的授权范围(这里是只读)。
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri
参数指定的网址。跳转时,会传回一个授权码
第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
- https://b.com/oauth/token?
- client_id=CLIENT_ID&
- client_secret=CLIENT_SECRET&
- grant_type=authorization_code&
- code=AUTHORIZATION_CODE&
- redirect_uri=CALLBACK_URL
上面 URL 中,client_id
参数和client_secret
参数用来让 B 确认 A 的身份(client_secret
参数是保密的,因此只能在后端发请求),grant_type
参数的值是AUTHORIZATION_CODE
,表示采用的授权方式是授权码,code
参数是上一步拿到的授权码,redirect_uri
参数是令牌颁发后的回调网址。
第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri
指定的网址,发送一段 JSON 数据。
- {
- "access_token": "ACCESS_TOKEN",
- "token_type": "bearer",
- "expires_in": 2592000,
- "refresh_token": "REFRESH_TOKEN",
- "scope": "read",
- "uid": 100101,
- "info": {...}
- }
上面 JSON 数据中,access_token
字段就是令牌,A 网站在后端拿到了。
其他模式:
简化模式(Implicit)、密码模式(Resource Owner Password Credentials)很少用,此处忽略。
JWT是一个开放标准RFC7519,其代表的是一种紧凑的、URL安全的、能够在网络应用间传输的声明。JWT 由 header.payload.signature 三段信息构成,“.” 链接成字符串。可使用在线校验工具(https://jwt.io)将 token 编/解码。
header 头部描述了该JWT的最基本信息,如类型、签名算法等:
(1)令牌类型 typ:JWT
(2)加密算法 alg:JWT签名默认的算法为HMAC SHA256,即HS256。
payload 载荷(是JWT主体)存放了令牌有效信息:
标准中注册的声明(Registered Claims)是对令牌中的一些标准属性信息进行声明,标准规定是建议,不强制。
常用的属性信息有:
signature:
将头部和载荷使用Base 64编码后,通过所使用的加密方法进行签名,签名后的结果放在这部分内容中。例如:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息在传递过程中有没有被更改,并且对于使用私钥签名的Token还可以验证JWT的发送方是否为它所称的发送方。secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和验证的,所以secret是服务器端的私钥,在任何场景都不应该泄露出去。
OAuth 2.0最大的痛点是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,以验证Token的有效性、获取Token对应的用户信息。在分布式架构下,如果有大量相关请求,处理效率是很低的,并且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求。JWT就是为解决这些问题而诞生的。普通的OAuth 2.0颁发的是一串随机hash字符串,本身无意义,而JWT格式的Token是有特定含义的。
JWT相对于传统的Token来说,解决了两个痛点:
认证时,当用户用他们的凭证成功登录以后,一个JSON Web Token就会被返回。此后,Token就是用户凭证了,用户想要访问受保护的路由或者资源,用户代理(通常是浏览器)都应该带上JWT,通常用Bearer schema放在Authorization header中,例如:
‘Authorization': 'Bearer ’ + token
Spring Security 除了用户认证,访问授权,还提供了加解密,CSRF保护、CORS、方法级安全访问、单点登录等核心功能。
spring-cloud-starter-oauth2 依赖整合了 spring-cloud-starter-security、spring-security-oauth2、spring-security-jwt 这3个依赖。
- <properties>
- <java.version>1.8</java.version>
- <spring-cloud.version>Edgware.SR3</spring-cloud.version>
- </properties>
-
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-starter-oauth2</artifactId>
- </dependency>
- </dependencies>
-
- <dependencyManagement>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-dependencies</artifactId>
- <version>${spring-cloud.version}</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencyManagement>
配置认证服务器:@EnableAuthorizationServer 并扩展AuthorizationServerConfigurerAdapter类
AuthorizationServerConfigurerAdapter 提供了以下3个重载的configure方法:
- @Override
-
- public void configure(AuthorizationServerSecurityConfigurer security) throws Exception{
- // 用于配置令牌端点(Token Endpoint)的安全约束
- }
-
- @Override
-
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
- // 用来配置及初始化客户端详情服务(ClientDetailService)
- }
-
- @Override
-
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- // 用于配置授权以及Token的访问端点和Token服务,以及Token的存储方式(eg:inMemory)
- }
Spring Security OAuth 2.0 公开两个端点用于检查 token 和获取 token:/oauth/check_token,/oauth/token_key,这些端点默认受保护 denyAll()。
配置资源服务器:@EnableResourceServer 并扩展 ResourceServerConfigurerAdapter 类
- @Override
- protected void configure(HttpSecurity http) throws Exception{
-
- }
-
- @Override
- protected void configure(AuthenticationManagerBuilder auth) throws Exception {
-
- }
-
默认情况下,Token只会有用户名等基本信息,实际应用中需要把关于用户的更多信息返回给客户端,通过自定义Token增强器来丰富Token内容。
如何自定义Token增强器?
Spring Security 自带 TokenEnhancer 接口:
- public interface TokenEnhancer {
-
- /**
- * 增强令牌
- *
- * @param accessToken 当前具有scope及过期时间的访问令牌
- * @param authentication 当前认证用户信息
- * @return 添加了额外信息的新令牌
- */
- OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication);
-
- }
实现TokenEnhancer接口;
- public class JwtTokenEnhancer implements TokenEnhancer {
-
- @Override
- public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
- Map<String, Object> additionalInformation = new HashMap<>();
- // 1. 获取认证信息
- // 客户端
- String clientId = authentication.getOAuth2Request().getClientId();// 客户端ID
- Set<String> resourceIds = authentication.getOAuth2Request().getResourceIds(); // 资源集合
- // 用户
- Authentication userAuthentication = authentication.getUserAuthentication();
- Object principal = userAuthentication.getPrincipal();
- if (principal instanceof User){
- User user= (User) principal;
- additionalInformation.put("userName", user.getUsername());
- }
- // 2.设置到accessToken中
- additionalInformation.put("resourceIds", resourceIds);
- additionalInformation.put("clientId", clientId);
- additionalInformation.put("deptId", "0001");
- ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
- return accessToken;
- }
- }
在AuthorizationServer配置类中声明TokenEnhancer Bean对象,然后在端点配置类中添加令牌增强器。
- // 端点配置
- @Override
- public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
- // 配置端点允许的请求方式
- endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
- // 配置认证管理器
- endpoints.authenticationManager(authenticationManager);
- // 自定义异常翻译器,用于处理OAuth2Exception
- endpoints.exceptionTranslator(myWebResponseExceptionTranslator);
- // 重新组装令牌颁发者,加入自定义授权模式
- endpoints.tokenGranter(getTokenGranter(endpoints));
- /* // 添加JWT令牌
- // JWT令牌转换器
- endpoints.accessTokenConverter(jwtAccessTokenConverter);
- // JWT 存储令牌*/
- endpoints.tokenStore(redisTokenStore);
- // 刷新令牌模式添加 userDetailsService
- endpoints.userDetailsService(userDetailsService);
- // 添加令牌增强器
- endpoints.tokenEnhancer(tokenEnhancer());
- }
- @Bean
- public TokenEnhancer tokenEnhancer() {
- return new MyTokenEnhancer();
- }
WebSecurityConfigurerAdapter,可以配置 BCryptPasswordEncoder 哈希来保存用户密码。
- @Configuration
- @EnableWebSecurity
- public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http
- .authorizeRequests()
- .antMatchers("/", "/home").permitAll()
- .anyRequest().authenticated()
- .and()
- .formLogin()
- .loginPage("/login")
- .permitAll()
- .and()
- .logout()
- .permitAll();
- }
-
- @Autowired
- public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
- auth
- .inMemoryAuthentication()
- .withUser("admin").password("admin").roles("USER");
- }
- }
除了 inMemoryAuthentication,还可以采用 ldapAuthentication,jdbcAuthentication等。
<待整理>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。