文章目录
参考
Spring Boot OAuth2.0应用 - 这个应该是个标准的流程,可以学习下
问题
用户先通过密码模式获取令牌(前端携带用户名和密码,在网关添加客户端id和客户端密码参数,认证服务通过密码模式发放令牌),此后使用该令牌访问服务。
现在,需要该用户授权给第三方客户端访问这个用户的资源。按标准的情况来说,应该是不能允许用户通过密码模式获取token登录的(通过密码模式获取的token代表的是这个用户完全信任这个客户端而把密码交给这个客户端从而获取到访问令牌),但是有的系统已经这样做了,现在需要在这个的基础上添加授权码模式。
Security OAuth2它本身就自带了授权码模式的实现,但是它需要先跳到1个授权页面,然后点击授权通过后,再跳到第三方客户端的重定向url并携带code,然后code换取token。但是这个默认的方式不是前后端分离的方式,是符合模板引擎的方式。现在就把一些必要的步骤摘出来(不作严格校验),使得通过密码模式登录的用户能够通过授权码模式授权给指定客户端,模仿它原来的方法来应用在前后端分离的方式中。
测试
通过密码模式获取访问令牌
http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=password&username=admin&password=admin
获取code
http://localhost:53020/uaa/oauth/code?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com
code换取token
http://localhost:53020/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=authorization_code&code=RSBpm2&redirect_uri=http://www.baidu.com
使用token访问资源服务
http://localhost:53021/resource/salary/query
分析
- 用户已经通过密码模式返回的token,那么携带此token的请求就可以被解析为OAuth2Authentication认证对象,而OAuth2Authentication = OAuth2Request + Authentication(其中OAuth2Request就代表授权信息,而Authentication就代表授权的用户信息),那么谁负责去解析就是需要解决的问题?因此就添加1条资源服务器过滤器链,来管理除了授权请求(/oauth/token、/oauth/token_key、/oauth/check_token)以外的其它请求
- code授权码本身代表了指定的用户对指定的客户端进行了某种授权,所以code授权码会关联1个OAuth2Authentication对象。即生成此授权码时需要1个OAuth2Authentication对象。当客户端携带此授权码到认证服务时,认证服务需要校验客户端身份,并且能够根据该授权码加载出OAuth2Authentication对象,再然后根据授权码模式颁发令牌。
- 其实,此时就2条securityFilterChain的过滤器链,1条是授权服务过滤器链(仅处理/oauth/token、/oauth/token_key、/oauth/check_token这些请求),另1条是资源服务过滤器链(处理授权服务过滤器链以外的其它任何请求)。此时的认证服务,既作为授权服务,也作为资源服务,其中作为授权服务通过security定义的各个XXXEndpoint端点处理授权服务相关请求,作为资源服务保护自定义获取授权码的处理器方法(也就是必须经过授权之后,才能访问自定义获取授权码的接口)
- 当通过自定义获取授权码接口,获得授权码之后,只能由指定的客户端携带它的客户端id和它的客户端密码,以证明客户端自己的身份(此code码只能由该指定客户端使用), 并且携带此code来换取访问令牌。当客户端携带此换取到的访问令牌访问时,资源服务依然需要能够解析此访问令牌得到OAuth2Authentication对象以获得是哪个用户对哪个客户端进行了怎样的授权这些信息。
- 前面说的都是使用密码模式获取访问令牌之后再使用授权码模式,但这种应该不算标准的方式,但是实现的过程就比较简单。如果要按照正常的情况来的话,也就是按照security默认实现来的话,应该是用户先通过用户名和密码先通过@EnableWebSecurity的那条过滤器链获得Authentication用户身份(就是security普通的用户名和密码登录,注意将它与密码模式获取tokne登录区别开),然后发起oauth/authorize授权请求,该授权请求仍然会经过@EnableWebSecurity的那条过滤器链,然后授权请求交给AuthorizationEndpoint处理,在此端点的处理器方法中能够获取到Authenticcation当前用户身份,并且是对哪个clientId客户端授权,因此此时就是跟上面说的密码模式使用授权码模式是一样的处理方式了。上面说的密码模式登录的用户再来使用授权码模式就是参考这个端点类中的实现)
- 可以自定义AuthorizationCodeServices授权码服务接口的实现,比如说把授权码存入redis,并设置过期时间来实现code授权码的过期
- 其它校验之类的,比如用户可以选择的授权选项,对哪些资源授权,可以自定义其它接口实现
代码
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo-spring-security-oauth2</artifactId>
<groupId>com.zzhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>distributed-security-uaa</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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>
<dependency>
<groupId>javax.interceptor</groupId>
<artifactId>javax.interceptor-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot-version}</version>
<configuration>
<mainClass>com.zzhua.UaaServerApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.yml
server:
port: 53020
servlet:
context‐path: /uaa
spring:
application:
name: uaa‐service
main:
allow‐bean‐definition‐overriding: true
mvc:
throw‐exception‐if‐no‐handler‐found: true
resources:
add‐mappings: false
management:
endpoints:
web:
exposure:
include: refresh,health,info,env
UaaServerApplication
@SpringBootApplication
public class UaaServerApplication {
public static void main(String[] args) {
SpringApplication.run(UaaServerApplication.class, args);
}
}
MyAuthorizationConfig
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter {
/*************************令牌端点的安全约束开始************************/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()") // oauth/token_key公开
.checkTokenAccess("permitAll()") // oauth/check_token公开
.allowFormAuthenticationForClients(); // 允许表单认证,申请令牌
}
/*************************令牌端点的安全约束结束************************/
/*************************配置客户端信息开始************************/
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//内存配置的方式配置用户信息
clients.inMemory()//内存方式
.withClient("c1") //client_id
.secret(passwordEncoder.encode("secret")) //客户端秘钥
.resourceIds("salary", "auth") //客户端拥有的资源列表
.authorizedGrantTypes("authorization_code", //该client允许的授权类型
"password",
"client_credentials",
"implicit",
"refresh_token")
.scopes("all") //允许的授权范围
.autoApprove(false) //跳转到授权页面
.redirectUris("http://www.baidu.com"); //回调地址
}
/*************************配置客户端信息结束************************/
/*************************配置令牌服务开始************************/
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
/*
注意到 {@link ClientDetailsServiceConfiguration#clientDetailsService()}
{@link AuthorizationServerSecurityConfiguration#configure(ClientDetailsServiceConfigurer)}
*/
@Autowired
private ClientDetailsService clientDetailsService;
/*@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore(); // 使用基于内存的普通令牌
}*/
@Bean
public JwtAccessTokenConverter accessTokenConvert(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();// [使用JWT令牌]
converter.setSigningKey("uaa");
return converter;
}
@Bean
public TokenStore tokenStore(){
return new JwtTokenStore(accessTokenConvert()); // [使用JWT令牌]
}
@Bean
public AuthorizationServerTokenServices tokenService() {
DefaultTokenServices service = new DefaultTokenServices();
service.setClientDetailsService(clientDetailsService); // 客户端详情服务
service.setSupportRefreshToken(true); // 允许令牌自动刷新
service.setTokenStore(tokenStore()); // 令牌存储策略-内存
service.setTokenEnhancer(accessTokenConvert()); // [使用JWT令牌]
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
return service;
}
//设置授权码模式的授权码如何存取,暂时用内存方式。
@Bean
public AuthorizationCodeServices authorizationCodeServices(){
return new InMemoryAuthorizationCodeServices();//JdbcAuthorizationCodeServices
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
// .pathMapping("/oauth/confirm_access","/customer/confirm_access")//定制授权同意页面
.authenticationManager(authenticationManager) //认证管理器
.userDetailsService(userDetailsService) //密码模式的用户信息管理
.authorizationCodeServices(authorizationCodeServices()) //授权码服务
.tokenServices(tokenService()) //令牌管理服务
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
/*************************配置令牌服务结束************************/
}
MyResourceServerConfig
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private TokenStore tokenStore;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.resourceId("auth") //资源ID
.tokenStore(tokenStore) // [使用JWT令牌],就不需要调用远程服务了,用本地验证方式就可以了。
.stateless(true); //无状态模式
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated();
http.requestMatcher(r-> true);
}
}
AuthCodeController
@RestController
public class AuthCodeController extends AuthorizationServerConfigurerAdapter {
private AuthorizationServerEndpointsConfigurer endpoints;
@PostMapping("oauth/code")
public String oauthCode(@RequestParam Map<String, String> parameters, Principal principal) {
AuthorizationRequest authorizationRequest = endpoints.getOAuth2RequestFactory().createAuthorizationRequest(parameters);
if (!(principal instanceof OAuth2Authentication)) {
throw new RuntimeException("ERR...");
}
OAuth2Authentication oauth2AuthUser = (OAuth2Authentication) principal;
Authentication userAuthentication = oauth2AuthUser.getUserAuthentication();
OAuth2Request storedOAuth2Request = endpoints.getOAuth2RequestFactory().createOAuth2Request(authorizationRequest);
OAuth2Authentication combinedAuth = new OAuth2Authentication(storedOAuth2Request, userAuthentication);
String code = endpoints.getAuthorizationCodeServices().createAuthorizationCode(combinedAuth);
// NOTE: 当前获取授权码已经完成, 后续的流程: 服务端需要指定客户端携带客户端id和客户端密码,证明客户端自己的身份, 并且携带此code来获取令牌 因为code代表用户对指定客户端的授权
return code;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
this.endpoints = endpoints;
}
}