问题
用户先通过密码模式获取令牌(前端携带用户名和密码,在网关添加客户端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
代码
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);
}
@Bean
public AuthorizationCodeServices codeServices() {
return new InMemoryAuthorizationCodeServices();
}
}
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;
}
}