文章目录
- 前言
- 1.环境基本配置和InMemory模式实现
- 2.五种基本授权模式测试
- 3.资源服务器改造
- 4.JWT令牌
前言
分布式架构为什么需要token?
Session是存储在服务器端的,当浏览器第一次请求Web服务器,服务器会产生一个Session存放在服务器里(可持久化到数据库中),然后通过响应头的方式将SessionID返回给浏览器写入到Cookie中,浏览器下次请求就会将SessiondID以Cookie形式传递给服务器端,服务器端获取SessionID后再去寻找对应的Session。如果找到了则代表用户不是第一次访问,也就记住了用户。
但需要注意的是,若服务器做了负载均衡,用户的下一次请求可能会被定向到其它服务器节点,若那台节点上没有用户的Session信息,就会导致会话验证失败。所以Session默认机制下是不适合分布式部署的。
服务架构简介
OAuth 2.0为用户和应用定义了如下角色:
- 资源拥有者
- 资源服务器
- 客户端应用
- 授权服务器
- 这些角色在下图中表示为:
1.环境基本配置和InMemory模式实现
导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
1.0授权中心
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pEnS9c7R-1666258112257)(C:\Users\zjy\Documents\MD\授权中心(AuthorizationServerConfigurerAdapter).png)]
这里我们先继承该类,重写和内容丰富将在下面的章节完成
package com.zjy.security.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
public AuthorizationServerConfig() {
super();
}
//客户端详情配置,可以写死也可以通过数据库查询
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
// 配置Token访问端点(url)和Token服务(生成Token)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
//Token端点安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
}
属性解释
- ClientDetailsServiceConfigurer:用来配置客户端详情服务(ClientDetailsService),客户端详情信息在 这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
- AuthorizationServerEndpointsConfigurer:用来配置令牌(token)的访问端点和令牌服务(token services)。
- AuthorizationServerSecurityConfigurer:用来配置令牌端点的安全约束.
1.1客户端详情(ClientDetailsService)配置
该配置通过重写授权中心中方法完成
//客户端详情配置,可以写死也可以通过数据库查询
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("Clinet01")
.secret(new BCryptPasswordEncoder().encode("password"))//秘钥
.resourceIds("resource01")
.authorizedGrantTypes() //客户端默认权限类型
.scopes("all") //授权范围
.authorities() //可缺省,看需求
.autoApprove(false) //是否不跳转到授权页面
.redirectUris("/index");//验证回调地址
}
属性解释
-
clientId:(必须的)用来标识客户的Id。
-
secret:(需要值得信任的客户端)客户端安全码,如果有的话。
-
scope:用来限制客户端的访问范围,如果为空(默认)的话,那么客户端拥有全部的访问范围。
-
authorizedGrantTypes:此客户端可以使用的授权类型,默认为空。
authorization_code — 授权码模式(即先登录获取code,再获取token)
password — 密码模式(将用户名,密码传过去,直接获取token)
client_credentials — 客户端模式(无用户,用户向客户端注册,然后客户端以自己的名义向’服务端’获取资源)
implicit — 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash) refresh_token — 刷新access_token
(这里的关键字我在源代码并没有找到,希望能有大佬能指点一下这些关键字(大概率是枚举)的位置)
-
authorities:此客户端可以使用的权限(基于Spring Security authorities)
1.2配置Token访问端点(AuthorizationServerEndpoints)
1.2.1Token管理服务(AuthorizationServerTokenServices)
这个Bean的创建根本上是为了下一步配置Token访问节点的需要。
自己可以创建 AuthorizationServerTokenServices 这个接口的实现,则需要继承 DefaultTokenServices 这个类。而持久化令牌是委托一个 TokenStore 接口来实现。
配置TokenStore
package com.zjy.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
@Configuration
public class TokenConfig {
@Bean
public TokenStore tokenStore(){
return new InMemoryTokenStore();
}
}
配置AuthorizationServerTokenServices
在授权中心创建Token管理服务(AuthorizationServerTokenServices)的Bean
@Resource
private TokenStore tokenStore;
@Resource
private ClientDetailsService clientDetailsService;
@Bean//令牌管理服务
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
services.setClientDetailsService(clientDetailsService);
services.setSupportRefreshToken(true);
services.setTokenStore(tokenStore);
services.setAccessTokenValiditySeconds(3600);
services.setRefreshTokenValiditySeconds(259200);
return services;
}
哪来的ClientDetailsService Bean呢?其实这里@EnableAuthorizationServer注解中会引入配置类ClientDetailsServiceConfiguration,该配置类会创建一个默认的ClientDetailsService!
这里我们如果想要自定义一个ClientDetailsService的Bean,需要在类里加上@Primary把该类当成是主类,否则会导致循环。样例如下:
@Service
@Primary
public class CustomClientDetailService implements ClientDetailsService {
@Resource
private PasswordEncoder passwordEncoder;
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
BaseClientDetails clientDetails = new BaseClientDetails(clientId, "resource_id", "all,select", "authorization_code,client_credentials,refresh_token,password", "aut", "http://www.baidu.com");
clientDetails.setClientSecret(passwordEncoder.encode("secret_" + clientId));
return clientDetails;
}
}
1.2.2配置其他所需类的Bean
注入AuthorizationCodeServices
这里我们选择"authorization_code" 授权码类型模式。同样是在授权中心,配置Bean,当场注入
@Resource
private AuthorizationCodeServices authorizationCodeServices;
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();
}
注入1.2.1节配置的AuthorizationServerTokenServices Bean
@Resource
private AuthorizationServerTokenServices tokenServices;
注入从SpringSecurity搬过来的UserDetailsService
代码如下
package com.zjy.security.service;
import com.zjy.security.mapper.UserMapper;
import com.zjy.security.pojo.MyUser;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Resource
private UserMapper usersMapper;
AuthenticationManager authenticationManager(){
return new OAuth2AuthenticationManager();
}
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
MyUser user=usersMapper.getUserByName(username);
if(user == null) {
throw new UsernameNotFoundException("用户名不存在!");
}
System.out.println(user);
//角色需要前缀“ROLE_”,权限不需要,角色和权限全部写进一个字符串,用逗号分隔
List<GrantedAuthority> auths =
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_vip1,ROLE_vip2");
//**********************************
//如果权限和角色也写进数据库呢?
//查出角色和权限后,遍历进行如下写入:
//auths.add(new SimpleGrantedAuthority(" "));
//***********************************
return new User(user.getUsername(),
new BCryptPasswordEncoder().encode(user.getPassword()),
auths);
}
}
授权中心注入:
@Resource
private MyUserDetailsService userDetailsService;
1.2.3正题:配置AuthorizationServerEndpoints
接下来就是正题:配置AuthorizationServerEndpoints
// 配置Token访问端点(url)和Token服务(生成Token)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(new OAuth2AuthenticationManager())
.authorizationCodeServices(authorizationCodeServices)
.tokenServices(tokenServices)
.userDetailsService(userDetailsService)
.allowedTokenEndpointRequestMethods(HttpMethod.GET)
//.pathMapping("/oauth/authorize","/myAuth/authorize")
;
}
属性解释
四大授权类型(Grant Types)
AuthorizationServerEndpointsConfigurer 通过设定以下属性决定支持的授权类型(Grant Types):
- authenticationManager:认证管理器,当你选择了资源所有者密码(password)授权类型的时候,请设置 这个属性注入一个 AuthenticationManager 对象。
- userDetailsService:如果你设置了这个属性的话,那说明你有一个自己的 UserDetailsService 接口的实现, 或者你可以把这个东西设置到全局域上面去(例如 GlobalAuthenticationManagerConfigurer 这个配置对 象),当你设置了这个之后,那么 “refresh_token” 即刷新令牌授权类型模式的流程中就会包含一个检查,用来确保这个账号仍然有效。
- authorizationCodeServices:这个属性是用来设置授权码服务的(即 AuthorizationCodeServices 的实例对 象),主要用于 “authorization_code” 授权码类型模式。
- implicitGrantService:这个属性用于设置隐式授权模式,用来管理隐式授权模式的状态。
- tokenGranter:当你设置了这个东西(即 TokenGranter 接口实现),那么授权将会交由你来完全掌控,并 且会忽略掉上面的这几个属性,这个属性一般是用作拓展用途的,即标准的四种授权模式已经满足不了你的 需求的时候,才会考虑使用这个。
配置授权端点的URL(Endpoint URLs)
AuthorizationServerEndpointsConfigurer 这个配置对象有一个叫做 pathMapping() 的方法用来配置端点URL链 接,它有两个参数:
- 第一个参数:String 类型的,这个端点URL的默认链接。
- 第二个参数:String 类型的,你要进行替代的URL链接。
以上的参数都将以 “/” 字符为开始的字符串,框架的默认URL链接如下列表,可以作为这个 pathMapping() 方法的 第一个参数:
- /oauth/authorize:授权端点。
- /oauth/token:令牌端点。
- /oauth/confirm_access:用户确认授权提交端点。
- /oauth/error:授权服务错误信息端点。
- /oauth/check_token:用于资源服务访问的令牌解析端点。
- /oauth/token_key:提供公有密匙的端点,如果你使用JWT令牌的话。
需要注意的是授权端点这个URL应该被Spring Security保护起来只供授权用户访问.
1.3Token端点安全约束(AuthorizationServerSecurity)
//Token端点安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")//具体权限写法应该同security
.allowFormAuthenticationForClients();//申请Token
}
1.4SpringSecurity配置
package com.zjy.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.annotation.Resource;
import javax.sql.DataSource;
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/static/index.html").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3")
;
//配置认证
http
.formLogin()
.loginPage("/index") // 配置哪个 url 为登录页面
.loginProcessingUrl("/login") // 设置哪个是登录的 url。
.successForwardUrl("/success") // 登录成功之后跳转到哪个 url
.failureForwardUrl("/fail")
.and()
.logout().deleteCookies().invalidateHttpSession(true)
.and()
.rememberMe().tokenValiditySeconds(60*60)
;
}
}
跑不起来?
尝试加入下述配置
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>3.0.2</version>
</dependency>
至此,授权中心配置完毕。
2.五种基本授权模式测试
2.1授权码模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wuOEetCn-1666258112257)(C:\Users\zjy\Documents\MD\OAuth\授权码模式.png)]
由于配置时我选择的获取令牌请求为get(方便测试),这里我们通过在浏览器直接键入URL发起请求。
获取授权码链接
(我这里redirect_uri=www.baidu.com是为了与授权中心配置的redirectUris一致)
http://localhost:8080/oauth/authorize?client_id=Clinet01&response_type=code&scope=all&redirect_uri=www.baidu.com
转到,跳转到页面:
http://localhost:8080/oauth/www.baidu.com?code=U58iIN
得到授权码:U58iIN
获取令牌
http://localhost:8080/oauth/token?client_id=Clinet01&client_secret=password&grant_type=authorization_code&code=U58iIN&redirect_uri=www.baidu.com
页面显示:
{"access_token":"eb8f91ea-3c19-4a48-8206-7ecbec940978","token_type":"bearer","expires_in":3599,"scope":"all"}
2.2.简化模式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0oVskcVU-1666258112258)(C:\Users\zjy\Documents\MD\OAuth\简化模式.png)]
请求:
http://localhost:8080/oauth/authorize?client_id=Clinet01&response_type=token&scope=all&redirect_uri=www.baidu.com
页面显示:
http://localhost:8080/oauth/www.baidu.com#access_token=e1df8f0b-e430-429b-a9d9-4405bab301f0&token_type=bearer&expires_in=3599
直接获取了令牌
2.3密码模式
http://localhost:8080/oauth/token?client_id=Clinet01&client_secret=password&grant_type=password&username=111&password=111&redirect_uri=www.baidu.com
2.4客户端模式
http://localhost:8080/oauth/token?client_id=Clinet01&client_secret=password&grant_type=client_credentials&redirect_uri=www.baidu.com
2.5资源模式
3.资源服务器改造
基于事先写过的一个常规三层架构provider,将其改造成符合OAuth的资源服务器。实际上并不需要更改其中原有代码,只需要加入两个配置类
依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
token解析服务
package com.zjy.dataprovider8000.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
@Configuration
public class TokenResolveServiceConfig {
@Bean(name="tokenServices")
public RemoteTokenServices tokenServices(){
RemoteTokenServices tokenServices=new RemoteTokenServices();
tokenServices.setClientSecret("password");
tokenServices.setCheckTokenEndpointUrl("http://localhost:9500"+"/oauth/check_token");
tokenServices.setClientId("Clinet01");
return tokenServices;
}
}
配置类
package com.zjy.dataprovider8000.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import javax.annotation.Resource;
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Resource
RemoteTokenServices tokenServices;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources
.resourceId("resource01")//这里要和授权中心配置的资源名相对应
.tokenServices(tokenServices)//验证令牌服务
.stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(
"/swagger-ui.html",
"/swagger-resources/**",
"/v2/api-docs",
"/oauth/check_token").permitAll()
.antMatchers("/**").access("#oauth2.hasScope('all')")
.and()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);//关闭session
}
}
按照oauth2.0协议要求,请求资源需要在Header里携带token,具体如下:
token的参数名称为:Authorization,值为:Bearer [token值]
手动获取token后用postman发起请求,通过,验证成功。
这里如果报401:nobody之类的错误,优先检查自己资源服务器和授权中心写死的验证信息是否有误,笔者这里单词写错导致第一次测试不通过
如果需要更细粒度的权限控制,在授权中心可以给令牌配置不同权限,在资源服务器处用antMatchers()拦截。
4.JWT令牌
什么是JWT?
JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于 在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公 钥/私钥对来签名,防止被篡改。
官网:https://jwt.io/
标准:https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
1)jwt基于json,非常方便解析。
2)可以在令牌中自定义丰富的内容,易扩展。
3)通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
4)资源服务使用JWT可不依赖认证服务即可完成授权。
JWT令牌的缺点:
1)JWT令牌较长,占存储空间比较大。
JWT令牌结构
通过学习JWT令牌结构为自定义jwt令牌打好基础。
JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz
Header
{
"alg": "HS256",
"typ": "JWT"
}
将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。
Payload
第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比 如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。
此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。
最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。
一个例子:
{
"sub": "1234567890",
"name": "456",
"admin": true
}
Signature
第三部分是签名,此部分用于防止jwt内容被篡改。
这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明 签名算法进行签名。
一个例子:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)