spring cloud security oauth2 + jwt
spring cloud security oauth2 + jwt
什么是OAuth2?
OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在限定时间、限定范围访问指定资源。主要涉及的RFC规范有RFC6749(整体授权框架),RFC6750(令牌使用),RFC6819(威胁模型)这几个,一般我们需要了解的就是RFC6749。获取令牌的方式主要有四种,分别是授权码模式,简单模式,密码模式和客户端模式,如何获取token不在本篇文章的讨论范围,我们这里假定客户端已经通过某种方式获取到了access_token,想了解具体的oauth2授权步骤可以移步阮一峰老师的理解OAuth 2.0,里面有非常详细的说明。
这里要先明确几个OAuth2中的几个重要概念:
resource owner: 拥有被访问资源的用户
user-agent: 一般来说就是浏览器
client: 第三方应用
Authorization server: 认证服务器,用来进行用户认证并颁发token
Resource server:资源服务器,拥有被访问资源的服务器,需要通过token来确定是否有权限访问
明确概念后,就可以看OAuth2的协议握手流程:
(A)用户打开客户端以后,客户端要求用户给予授权。
(B)用户同意给予客户端授权。
(C)客户端使用上一步获得的授权,向认证服务器申请令牌。
(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。
(E)客户端使用令牌,向资源服务器申请获取资源。
(F)资源服务器确认令牌无误,同意向客户端开放资源。
什么是Spring Security?
Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一系列的filter chain来进行拦截过滤,以下是ss中默认的内置过滤器列表,当然你也可以通过custom-filter来自定义扩展filter chain列表:
这里面最核心的就是FILTER_SECURITY_INTERCEPTOR,通过FilterInvocationSecurityMetadataSource来进行资源权限的匹配,AccessDecisionManager来执行访问策略
认证与授权(Authentication and Authorization)
一般意义来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开的。即首先需要确定用户身份,在确定这个用户是否有访问指定资源的权限。认证这块的解决方案很多,主流的有CAS、SAML2、OAUTH2等(不巧这几个都用过-_-),我们常说的单点登录方案(SSO)说的就是这块,授权的话主流的就是spring security和shiro。shiro我没用过,据说是比较轻量级,相比较而言spring security确实架构比较复杂。
JWT介绍
终于来到了著名的JWT部分了,JWT全称为Json Web Token,最近随着微服务架构的流行而越来越火,号称新一代的认证技术。今天我们就来看一下,jwt的本质到底是什么。
我们先来看一下OAuth2的token技术有没有什么痛点,相信从之前的介绍中你也发现了,token技术最大的问题是不携带用户信息,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低的,且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求,这在分布式架构下是很要命的。
JWT就是在这样的背景下诞生的,从本质上来说,jwt就是一种特殊格式的token。普通的oauth2颁发的就是一串随机hash字符串,本身无意义,而jwt格式的token是有特定含义的,分为三部分:
- 头部Header
- 载荷Payload
- 签名Signature
这三部分均用base64进行编码,当中用.进行分隔,一个典型的jwt格式的token类似xxxxx.yyyyy.zzzzz。关于jwt格式的更多具体说明,不是本文讨论的重点,大家可以直接去官网查看官方文档,这里不过多赘述。
相信看到签名大家都很熟悉了,没错,jwt其实并不是什么高深莫测的技术,相反非常简单。认证服务器通过对称或非对称的加密方式利用payload生成signature,并在header中申明签名方式,仅此而已。通过这种本质上极其传统的方式,jwt可以实现分布式的token验证功能,即资源服务器通过事先维护好的对称或者非对称**(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制无疑很对现在分布式架构的胃口。jwt相对于传统的token来说,解决以下两个痛点:
通过验证签名,token的验证可以直接在本地完成,不需要连接认证服务器
在payload中可以定义用户相关信息,这样就轻松实现了token和用户信息的绑定
在上面的那个资源服务器和认证服务器分离的例子中,如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。
授权模式
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。OAuth 2.0定义了四种授权方式。
授权码模式(authorization code)
简化模式(implicit)
密码模式(resource owner password credentials)
客户端模式(client credentials)
本文重点讲解接口对接中常使用的密码模式
项目代码部分
oauth微服务添加jar包:
<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>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
项目最重要的是以下几个类:
TokenConfig Token配置类 代码如下:
package com.hngtsd.zxtk.oauth.config;
import com.hngtsd.zxtk.common.utlis.CommonVo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCrypt;
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;
/*
* @author Mr.wangfeng
* @date 2020/8/5 16:54
* @param token配置类
* @return
*/
@Configuration
public class TokenConfig {
public static final String SIGNING_KEY ="oauth";
/**
* accessTokenConverter
* 使用JWT令牌
*/
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// accessTokenConverter.setSigningKey(SIGNING_KEY); //对称** 资源服务访问需要验证
accessTokenConverter.setSigningKey(CommonVo.SIGNING_KEY); //对称** 资源服务访问需要验证
return accessTokenConverter;
}
/**
* 令牌储存策略
*/
@Bean
public TokenStore tokenStore() {
//JWT储存
return new JwtTokenStore(accessTokenConverter());
}
}
WebSecurityConfig前端配置类 :
package com.hngtsd.zxtk.oauth.config;
import com.hngtsd.zxtk.oauth.config.sms.PhoneAndVerificationCodeAuthenticationProvider;
import com.hngtsd.zxtk.oauth.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.client.RestTemplate;
/*
* @author Mr.wangfeng
* @date 2020/8/6 15:33
* @param 前端配置类
* @return
*/
@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
/**
* authenticationManager
* 认证服务
*
* @return
*/
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
private MyUserDetailsService userDetailsService;
/**
* passwordEncoder
* 密码编码器
*
* @return
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 安全拦截机制
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置电话号码和验证码认证
http.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin();
http;
}
}
AuthorizationServer认证配置类代码:
package com.hngtsd.zxtk.oauth.config;
import com.hngtsd.zxtk.oauth.config.sms.PhoneAndVerificationCodeTokenGranter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.password.PasswordEncoder;
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.ClientDetailsService;
import org.springframework.security.oauth2.provider.CompositeTokenGranter;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.TokenGranter;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
import org.springframework.security.oauth2.provider.code.JdbcAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
import org.springframework.security.oauth2.provider.password.ResourceOwnerPasswordTokenGranter;
import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
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.redis.RedisTokenStore;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/*
* @author Mr.wangfeng
* @date 2020/8/5 16:53
* @param 认证授权类
*/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtAccessTokenConverter accessTokenConverter;
@Autowired
private AuthorizationCodeServices authorizationCodeService;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokenStore tokenStore;
/**
* authorizationCodeService
* 授权码服务
* @return
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource) {
// 基于数据库
return new JdbcAuthorizationCodeServices(dataSource);
}
/**
* clientDetailsService
* 客户端的详情服务
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(DataSource dataSource) {
//客户端数据存储到数据库
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);
((JdbcClientDetailsService)clientDetailsService).setPasswordEncoder(passwordEncoder);
return clientDetailsService;
}
/**
* <p>注意,自定义TokenServices的时候,需要设置@Primary,否则报错,</p>
* 令牌管理服务
* @return
*/
@Primary
@Bean
public AuthorizationServerTokenServices tokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
//客户端的详情服务
tokenServices.setClientDetailsService(clientDetailsService);
// 是否刷新令牌
tokenServices.setSupportRefreshToken(true);
// 存储的方式
tokenServices.setTokenStore(tokenStore);
//令牌增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
tokenServices.setTokenEnhancer(tokenEnhancerChain);
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60 * 60 * 12);
//刷新令牌过期时间,默认24小时
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24);
return tokenServices;
}
/**
* 客户端配置
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService);
}
/**
* 配置令牌访问端点
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager) //密码模式需要
.authorizationCodeServices(authorizationCodeService) //授权码服务
.tokenServices(tokenServices()) //令牌服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()"); //oauth/token_key公开
security .checkTokenAccess("permitAll()"); //oauth/check_token公开 校验token的合法性
security.allowFormAuthenticationForClients(); // 允许表单提交 (申请令牌)
}
}
MyUserDetailsService实现UserDetailsService接口 :
package com.hngtsd.zxtk.oauth.service;
import com.alibaba.fastjson.JSON;
import com.hngtsd.zxtk.common.model.TbPopedom;
import com.hngtsd.zxtk.common.model.TbUser;
import com.hngtsd.zxtk.oauth.mapper.TbUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
TbUserMapper userMapper;
/**
*
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TbUser user = userMapper.selectByUsername(username);
if (user == null) {
return null;
}
if (user.getStatus() == 1) {
return null;
}
List<TbPopedom> tbPopedoms = userMapper.selectTbPopedomByUid(user.getId());
List<String> popedoms = new ArrayList<String>();
if (tbPopedoms !=null && tbPopedoms.size()>0 ){
for (TbPopedom popedom : tbPopedoms) {
if (popedom.getCode() != null) {
popedoms.add(popedom.getCode());
}
}
}
String[] popedomsArray = new String[popedoms.size()];
String[] array = popedoms.toArray(popedomsArray);
//将用户信息转成JSON
String principal = JSON.toJSONString(user);
UserDetails userDetails = User.withUsername(principal).password(user.getPassword()).authorities(array).build();
return userDetails;
}
}
上面认证的数据我都是动态从数据库获取的数据,后面我会将数据库表给大家。
测试
这里面的参数一个都不能少,对应的值你可以自己设置,这里我数据库以及配好了放在配置文件中。
#token存储到redis的过期时间 (设置为一天)
tokenValiditySeconds=43200
clientId=WebApp
clientSecret=secret
cookieDomain=localhost
cookieMaxAge= -1
参数配置好了我们就可以打开postman进行测试了
这里我只使用了password测试
这里我填了很多的权限,所以token比较长,本项目我是使用redis将token进行存储的。
Spring cloud security oauth2 +jwt 使用手机验证码登陆的教程后续再给大家发,项目中已经更新了
项目地址:https://gitee.com/wf109809/security-oauth2-jwt.git
推荐阅读
-
微信授权就是这个原理,Spring Cloud OAuth2 授权码模式
-
Spring Cloud系列-Zuul网关集成JWT身份验证
-
使用Spring Security OAuth2实现单点登录
-
Spring Security 解析(七) —— Spring Security Oauth2 源码解析
-
如何用Spring Security OAuth2 实现登录互踢,面试必学
-
Spring Security OAuth2 SSO
-
基于Spring Security的Oauth2授权实现方法
-
SpringBoot集成Spring security JWT实现接口权限认证
-
spring security oauth2的token续期
-
jwt,spring security ,feign,zuul,eureka 前后端分离 整合 实现 简单 权限管理系统 与 用户认证的实现