OAuth2
什么是OAuth2?
OAuth 2.0(Open Authorization 2.0)是一种开放标准的授权协议,用于在不透露用户凭据(例如用户名和密码)的情况下,让用户授权第三方应用程序访问其受保护的资源。
简单来说,OAuth 2.0 提供了一种安全的机制,允许用户使用其已有的认证凭据(例如社交媒体账户)来登录和授权其他应用程序访问其个人信息。
OAuth 2.0 的核心角色包括:
- 用户(resource owner):拥有受保护资源的实体,是授权的主体。
- 客户端(client):代表用户请求访问受保护资源的应用程序,例如一个第三方应用或者服务。
- 授权服务器(authorization server):负责验证用户身份并授予客户端访问令牌的服务器。
- 资源服务器(resource server):存储用户的受保护资源,并负责根据访问令牌提供授权的访问。
OAuth2认证过程
首先先来快速认识一下Oauth2的认证流程图
流程图内容解释
Clinet(客户端)、ResourceOwner(资源拥有者)
Authorization Server(授权服务器),ResourceServer(资源服务器)
AuthorizationRequest(授权请求)、AuthorizationGrant(授权许可)
AccessToken(访问令牌)
Portected Resource(受保护的资源)
Client(客户端)本身不用来存储资源,需要通过请求资源服务器的资源来获取资源。
而要获取到资源服务器的资源,需要经过ABCDEF这些过程。总结来说有4个流程
第一步:向资源拥有者发起授权请求,通俗来讲,就类似你微信授权登录某小程序,小程序就是客户端,而你微信就资源拥有者,用户的意思。
第二步:资源拥有者返回客户端一个授权许可,此时等同你已经同意授权了。
第三步:客户端拿到授权许可后会向授权服务器申请访问令牌,然后授权服务器会认证你这个用户或者客户端并颁发令牌
其中:认证用户是通过验证用户提供的凭据,包括匹配用户名密码或者授权码等。 认证客户端是通过提供的客户端标识和客户端密钥等。
注意:因为OAuth2的授权模式有授权码、密码、简化、客户端模式等,选择哪个模式对应认证哪个方面。
第四步:客户端拿到访问令牌就可以向资源服务器请求受保护的资源了。这些受保护的资源类似你登录成功后展示给你看的商品图片,数据等都是。
JWT
OAuth2令牌可以分为透明令牌(Transparent Token)和不透明令牌(Opaque Token)。
不透明令牌:指令牌本身并不包含令牌的具体信息,仅包含一个令牌标识(Token Identifier)
透明令牌:指令牌本身包含了令牌的所有信息,例如访问令牌的有效期、权限范围等
JWT分为三部分,分别是头部、载荷、签名。
- 头部(Header): JWT的头部通常由两部分组成,令牌的类型(即"typ"字段,一般都是JWT)和所使用的签名算法(即"alg"字段)。例如: { "alg": "HS256", "typ": "JWT" }。
- 载荷(Payload):载荷是JWT的第二部分,它包含了一些称为声明(Claims)的数据。有三种类型的声明:注册声明(Registered claims)、公共声明(Public claims)和私有声明(Private claims)。注册声明是预定义的声明类型,如iss(签发者)、exp(过期时间)、sub(主题)等。公共声明是自定义的声明,但不建议重复使用。私有声明定义了用户自定义的声明,用于在通信的各方之间共享信息。
- 签名(Signature):签名是JWT的第三部分,它由前两部分(头部和载荷)及一个密钥进行签名生成。签名的目的是为了验证消息的完整性和真实性。签名通常使用 HMAC(基于哈希的消息认证码)或 RSA(非对称加密算法)等算法进行生成。签名通过对头部、载荷和密钥进行加密生成一个哈希值,并将其作为签名部分的内容。
JWT令牌的优点
1、jwt基于json,非常方便解析。
2、可以在令牌中自定义丰富的内容,易扩展。
3、通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4、资源服务使用JWT可不依赖认证服务即可完成授权。
JWT令牌的缺点
1、JWT令牌较长,占存储空间比较大。
下面通过一个构建认证服务让了解更加深刻。
配置类
TokenEnhanceConfig JWT声明内容加强配置
WebSecurityConfig 安全配置
AuthorizationServerConfig OAuth2授权服务器配置
注意事项
不是所有配置类都可以作为OAuth2.0认证中心的配置类, 需要满足以下两点:
1、继承AuthorizationServerConfigurerAdapter
2、 标注 @EnableAuthorizationServer 注解
AuthorizationServerConfigurerAdapter需要实现的三个方法如下
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
//配置令牌端点安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {
}
//用来配置客户端详情服务
@Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer authorizationServerEndpointsConfigurer) throws Exception {
//配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
}
}
TokenEnhanceConfig
它是用来配置JWT声明信息达到增强令牌的一个配置类。
@Configuration
@RequiredArgsConstructor
public class TokenEnhanceConfig {
private final RedisTemplate redisTemplate;
@Bean
// 根据authentication对象中的principal属性的类型,将不同的用户信息存放到additionalInfo对象中,
// 并将additionalInfo设置为accessToken的附加信息
public TokenEnhancer tokenEnhancer() {
return (accessToken, authentication) -> {
// 实现TokenEnhancer接口的匿名内部类,增强访问令牌的信息
Object principal = authentication.getUserAuthentication().getPrincipal();
Map<String, Object> additionalInfo = MapUtil.newHashMap();
if (principal instanceof SysUserDetails) {
SysUserDetails sysUserDetails = (SysUserDetails) principal;
additionalInfo.put("userId", sysUserDetails.getUserId());
additionalInfo.put("username", sysUserDetails.getUsername());
additionalInfo.put("deptId", sysUserDetails.getDeptId());
additionalInfo.put("dataScope",sysUserDetails.getDataScope());
/**
* 系统用户按钮权限标识数据量多存放至redis
*
* key:AUTH:USER_PERMS:2
* value:['sys:user:add',...]
*/
//这样可以方便地将用户的权限信息存储在分布式缓存Redis中,以供后续的权限验证等操作使用。
redisTemplate.opsForValue().set("AUTH:USER_PERMS:" + sysUserDetails.getUserId(), sysUserDetails.getPerms());
} else if (principal instanceof MemberUserDetails) {
MemberUserDetails memberUserDetails = (MemberUserDetails) principal;
additionalInfo.put("memberId", memberUserDetails.getMemberId());
additionalInfo.put("username", memberUserDetails.getUsername());
}
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
};
}
}
增强访问令牌的信息
TokenEnhanceConfig里实现了tokenEnhancer的匿名内部类,目的是增强访问令牌的信息。将授权主体(指用户)的相关信息存放到addtionalInfo中,然后设置到accessToken(访问令牌)的附加信息中。
可能你会问,配置了声明信息如何达到增强的效果呢?
答:通过配置了上述代码的声明,令牌声明里多了用户的权限信息,
服务端就可以利用这些信息进行身份验证、授权等操作。这样可以减少数据库的压力,简化跨域通信的流程,简化系统的实现等。
WebSecurityConfig
安全配置
@ConfigurationProperties(prefix = "security")
@Configuration
@EnableWebSecurity
@Slf4j
@RequiredArgsConstructor
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserDetailsService sysUserDetailsService;
private final UserDetailsService memberUserDetailsService;
private final WxMaService wxMaService;
private final MemberFeignClient memberFeignClient;
private final StringRedisTemplate redisTemplate;
@Setter
private List<String> ignoreUrls;
@Override
//配置安全过滤器
protected void configure(HttpSecurity http) throws Exception {
if (CollectionUtil.isEmpty(ignoreUrls)) {
//将默认的白名单路径添加到ignoreUrls列表
ignoreUrls = Arrays.asList("/webjars/**", "/doc.html", "/swagger-resources/**", "/v2/api-docs");
}
log.info("whitelist path:{}", JSONUtil.toJsonStr(ignoreUrls));
http
.authorizeRequests()
//允许ignoreUrls中的所有URL进行无需身份验证的访问
.antMatchers(Convert.toStrArray(ignoreUrls)).permitAll()
//其他所有请求则需要进行身份验证
.anyRequest().authenticated()
.and()
//禁用了跨站请求伪造(CSRF)保护
.csrf().disable();
}
/**
* 认证管理对象
*
* @return
* @throws Exception
*/
@Bean
// 这个方法暴露了AuthenticationManager bean,用于处理身份验证和授权过程。
//AuthenticationManager在密码授权模式下会用到,这里提前注入,如果你用的不是密码模式,可以不注入.
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
// 这个方法通过指定认证提供者来配置AuthenticationManagerBuilder。
// 它注册了微信、用户名密码和短信验证码认证提供者。
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(wechatAuthenticationProvider()).
authenticationProvider(daoAuthenticationProvider()).
authenticationProvider(smsCodeAuthenticationProvider());
}
/**
* 手机验证码认证授权提供者
*
* @return
*/
@Bean
public SmsCodeAuthenticationProvider smsCodeAuthenticationProvider() {
SmsCodeAuthenticationProvider provider = new SmsCodeAuthenticationProvider();
provider.setUserDetailsService(memberUserDetailsService);
provider.setRedisTemplate(redisTemplate);
return provider;
}
/**
* 微信认证授权提供者
*
* @return
*/
@Bean
public WechatAuthenticationProvider wechatAuthenticationProvider() {
WechatAuthenticationProvider provider = new WechatAuthenticationProvider();
provider.setUserDetailsService(memberUserDetailsService);
provider.setWxMaService(wxMaService);
provider.setMemberFeignClient(memberFeignClient);
return provider;
}
/**
* 用户名密码认证授权提供者
*
* @return
*/
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(sysUserDetailsService);
provider.setPasswordEncoder(passwordEncoder());
provider.setHideUserNotFoundExceptions(false); // 是否隐藏用户不存在异常,默认:true-隐藏;false-抛出异常;
return provider;
}
/**
* 密码编码器
* <p>
* 委托方式,根据密码的前缀选择对应的encoder,例如:{bcypt}前缀->标识BCYPT算法加密;{noop}->标识不使用任何加密即明文的方式
* 密码判读 DaoAuthenticationProvider#additionalAuthenticationChecks
*/
@Bean
// 这个方法配置了用户名密码认证所使用的密码编码器。
// 它使用Spring Security的DelegatingPasswordEncoder创建一个密码编码器,根据密码的前缀委派给不同的编码器
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
这个类整合的内容有
1、加密方式
PasswordEncoderFactories.createDelegatingPasswordEncoder()返回的是一个委托密码编码器,
默认使用的是 BCryptPasswordEncoder 加密方式来加密和验证密码。
2、配置用户,
它注册了微信、用户名密码和短信验证码认证提供者。
3、注入认证管理器
因为上述代码使用的是密码授权模式,在这里提前注入,如果用的不是密码模式,可以不注入。
4、配置安全拦截策略
允许ignoreUrls中的所有URL进行无需身份验证的访问,禁用了跨站请求伪造(CSRF)保护等。
AuthorizationServerConfig
它是 OAuth2授权服务器配置
@Configuration
@EnableAuthorizationServer//启用授权服务器功能,将当前应用程序作为一个OAuth2授权服务器
@RequiredArgsConstructor
//这个类用于配置和初始化授权服务器的各种参数和组件
//AuthorizationServerConfig用 @EnableAuthorizationServer 注解标识并继承AuthorizationServerConfigurerAdapter来配置OAuth2.0 授权服务器。
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final AuthenticationManager authenticationManager;//用于进行身份验证
private final SysUserDetailsServiceImpl sysUserDetailsService;
private final MemberUserDetailsServiceImpl memberUserDetailsService;
private final StringRedisTemplate stringRedisTemplate;
private final DataSource dataSource;
private final TokenEnhancer tokenEnhancer;//注入TokenEnhancer对象,用于增强令牌的内容。
/**
* OAuth2客户端
*/
@Override
//用来配置客户端详情服务,基于 JDBC 的客户端详情服务配置为当前的 Spring Security OAuth2 客户端配置
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(jdbcClientDetailsService());
}
/**
* 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*/
@Override
//用来配置令牌(token)的访问端点和令牌服务(token services)
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
// Token增强
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
//tokenEnhancer里有附加信息为用户的信息
tokenEnhancers.add(tokenEnhancer);
tokenEnhancers.add(jwtAccessTokenConverter());
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
//token存储模式设定 默认为InMemoryTokenStore模式存储到内存中
endpoints.tokenStore(jwtTokenStore());
// 获取原有默认授权模式(授权码模式、密码模式、客户端模式、简化模式)的授权者
//,endpoints.getTokenGranter() 方法返回一个 TokenGranter 数组,通过使用 Arrays.asList(...) 方法将其转换为一个 List
List<TokenGranter> granterList = new ArrayList<>(Arrays.asList(endpoints.getTokenGranter()));
// 添加验证码授权模式授权者
granterList.add(new CaptchaTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory(), authenticationManager, stringRedisTemplate
));
// 添加手机短信验证码授权模式的授权者
granterList.add(new SmsCodeTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory(), authenticationManager
));
// 添加微信授权模式的授权者
granterList.add(new WechatTokenGranter(endpoints.getTokenServices(), endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory(), authenticationManager
));
// 用于组合多个 TokenGranter 实例,以实现多种授权模式的支持。
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(granterList);
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter())
.tokenEnhancer(tokenEnhancerChain)
.tokenGranter(compositeTokenGranter)
.tokenServices(tokenServices(endpoints))
;
}
/**
* jwt token存储模式
*/
@Bean
public JwtTokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
public DefaultTokenServices tokenServices(AuthorizationServerEndpointsConfigurer endpoints) {
// 用于组合多个令牌增强器
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
// 用于存储令牌增强器。
List<TokenEnhancer> tokenEnhancers = new ArrayList<>();
tokenEnhancers.add(tokenEnhancer);
tokenEnhancers.add(jwtAccessTokenConverter());
//将 tokenEnhancers 设置为 tokenEnhancerChain 的令牌增强器列表。
tokenEnhancerChain.setTokenEnhancers(tokenEnhancers);
//用于管理令牌相关的业务逻辑
DefaultTokenServices tokenServices = new DefaultTokenServices();
// 设置 endpoints 中的令牌存储方式为 tokenServices 的令牌存储方式。即上面的JwtTokenStore这个方法
tokenServices.setTokenStore(endpoints.getTokenStore());
// 设置 tokenServices 支持刷新令牌。
tokenServices.setSupportRefreshToken(true);
// 设置 tokenServices 使用的客户端详情服务。
tokenServices.setClientDetailsService(jdbcClientDetailsService());
//设置 tokenServices 使用的令牌增强器。
tokenServices.setTokenEnhancer(tokenEnhancerChain);
// 多用户体系下,刷新token再次认证客户端ID和 UserDetailService 的映射Map
//用于存储客户端ID和对应的用户详情服务
Map<String, UserDetailsService> clientUserDetailsServiceMap = new HashMap<>();
clientUserDetailsServiceMap.put(SecurityConstants.ADMIN_CLIENT_ID, sysUserDetailsService); // 系统管理客户端
clientUserDetailsServiceMap.put(SecurityConstants.APP_CLIENT_ID, memberUserDetailsService); // Android、IOS、H5 移动客户端
clientUserDetailsServiceMap.put(SecurityConstants.WEAPP_CLIENT_ID, memberUserDetailsService); // 微信小程序客户端
// 刷新token模式下,重写预认证提供者替换其AuthenticationManager,可自定义根据客户端ID和认证方式区分用户体系获取认证用户信息
PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();//用于进行预认证。
//设置 provider 的预认证用户详情服务为 PreAuthenticatedUserDetailsService,并传入 clientUserDetailsServiceMap
provider.setPreAuthenticatedUserDetailsService(new PreAuthenticatedUserDetailsService<>(clientUserDetailsServiceMap));
// 设置 tokenServices 使用的身份验证管理器为 ProviderManager,并传入包含 provider 的集合。
tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));
/**
* refresh_token有两种使用方式:重复使用(true)、非重复使用(false),默认为true
* 1 重复使用:access_token过期刷新时, refresh_token过期时间未改变,仍以初次生成的时间为准
* 2 非重复使用:access_token过期刷新时, refresh_token过期时间延续,在refresh_token有效期内刷新便永不失效达到无需再次登录的目的
*/
//设置tokenServices 的刷新令牌是否可重复使用。
tokenServices.setReuseRefreshToken(true);
return tokenServices;
}
/**
* 使用非对称加密算法对token签名
*/
//认证服务和资源服务使用相同的密钥,这叫对称加密,对称加密效率高,如果一旦密钥泄露可以伪造jwt令牌。
// JWT还可以使用非对称加密,认证服务自己保留私钥,将公钥下发给受信任的客户端、资源服务,公钥和私钥是配对的,
// 成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密非对称加密更安全一些。
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyPair());
return converter;
}
/**
* 密钥库中获取密钥对(公钥+私钥)
*/
@Bean
public KeyPair keyPair() {
//通过传递一个ClassPathResource对象和密码字符串,该工厂负责加载密钥库文件并准备密钥对。
// 这里的keystore.jks是密钥库的文件名,它位于类路径下。
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "123456".toCharArray());
// 通过getKeyPair方法获取密钥对:
// 调用KeyStoreKeyFactory对象的getKeyPair方法,使用密钥库密码和密钥的别名(这里是jwt)来获取密钥对。
// 密钥别名用于标识密钥对,而密钥库密码用于保护密钥库的访问。
KeyPair keyPair = factory.getKeyPair("jwt", "123456".toCharArray());
return keyPair;
}
/**
* 自定义认证异常响应数据
*/
@Bean
//用于在用户尚未进行身份认证或者认证失败时处理请求。
public AuthenticationEntryPoint authenticationEntryPoint() {
return (request, response, e) -> {
response.setStatus(HttpStatus.HTTP_OK);
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Cache-Control", "no-cache");
Result result = Result.failed(ResultCode.CLIENT_AUTHENTICATION_FAILED);
response.getWriter().print(JSONUtil.toJsonStr(result));
response.getWriter().flush();
};
}
/**
* 可自定义实现
*
* @return
*/
@Bean
public JdbcClientDetailsService jdbcClientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
}
这个类整合了
1、 令牌相关的配置
JwtAccessTokenConverter令牌增强,用于JWT令牌和OAuth身份进行转换。
JwtTokenStore存储模式(这里使用的是JWTTokenStore存储模式,此外还有RedisTokenStore和JdbcTokenStore), JwtTokenStore 使用 JWT(JSON Web Token)作为存储模式,它将令牌作为简单的字符串进行存储, 每当用户登录并成功认证后,系统会生成一个 JWT Token 返回给客户端。这个 JWT Token 包含一些关键信息,如用户身份标识和访问权限等。
这里使用的是非对称加密,通过Keypair获取密钥对。
非对称加密指:通过密钥对获取公钥和私钥,认证服务自己保留私钥,将公钥下发给受信任的客户端、资源服务,公钥和私钥是配对的, 成对的公钥和私钥才可以正常加密和解密,非对称加密效率低但相比对称加密非对称加密更安全一些。
2、 令牌管理服务的配置
使用的是DefaultTokenServices这个实现类,通过上述代码的注释可以了解清楚, 至于final修饰的那部分内容,可以去源码查看。
3、 令牌访问端点的配置
给端点设置认证管理器,JWT令牌和OAuth身份转换,令牌增强器,令牌授权者,令牌管理服务。
4、 客户端配置
基于 JDBC 的客户端详情服务配置为当前的 Spring Security OAuth2 客户端配置。
此外还设置了自定义认证异常响应数据。