集成开发环境
·开发工具:Eclipse/Myeclipse/IntelliJ IDEA 任选其一
·运行环境:jdk1.7及以上版本
·数据库:MongoDB
spring-security-oauth2源码
·本地SVN地址:
svn://host /jd_product/fort/java_main/spring-cloud-base/dev/fort-spring-security-oauth2
·github地址:https://github.com/spring-projects/spring-security-oauth.git
构建授权服务
1、使用eclipse新建一个名称为“fort-auth”maven项目,将其pom.xml配置文件修改为如下:
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fort.jidisec</groupId>
<artifactId>fort-auth</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>fort-auth Maven Webapp</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.8.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>fort-spring-security-oauth2</groupId>
<artifactId>fort-spring-security-oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-mongodb -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-mongodb</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.json/json -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160810</version>
</dependency>
</dependencies>
<build>
<finalName>fort-auth</finalName>
</build>
</project>
2、新建程序启动类“Application.java”,如下所示:
@SpringBootApplication
@EnableAuthorizationServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3、新建“AuthServerConfig.java”类继承“AuthorizationServerConfigurerAdapter”类初始化授权服务所需要的信息,如下所示:
@Configuration
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private ClientDetailsService mongoClientDetailsService;
@Autowired
private TokenStore mongoTokenStore;
@Autowired
private UserDetailsService mongoUserDetailsService;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.checkTokenAccess("permitAll()").allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(mongoClientDetailsService);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(mongoTokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(mongoUserDetailsService);
}
}
4、新建“DataSourceConfig”类初始化数据源,如下所示:
@Configuration
public class DataSourceConfig {
@Value("${spring.mongodb.uri}")
private String mongoUri;
@Value("${spring.mongodb.database}")
private String mongoDatabase;
@Bean("mongoDbFactory")
public MongoDbFactory getMongoDbFactory() {
return new SimpleMongoDbFactory(new MongoClient(new MongoClientURI(mongoUri)), mongoDatabase);
}
@Bean("mongoTemplate")
@Resource(name="mongoDbFactory")
public MongoTemplate getMongoTemplate(MongoDbFactory mongoDbFactory) {
return new MongoTemplate(mongoDbFactory);
}
}
5、新建“application.yml”配置文件,内容如下:
server:
port: 8888
spring:
mongodb:
uri: mongodb://root:123@localhost:27017
database: admin
5、定义查询认证信息的接口,代码如下:
@Service("mongoClientDetailsService")
public class MongoClientDetailsService extends MongoSuportTemplate implements ClientDetailsService {
private final String CONLLECTION_NAME = "oauth_client_details";
private PasswordEncoder passwordEncoder = NoOpPasswordEncoder.getInstance();
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
BaseClientDetails client = mongoTemplate.findOne(new Query(Criteria.where("clientId").is(clientId)), BaseClientDetails.class, CONLLECTION_NAME);
if(client != null) {
String secret = client.getClientSecret();
if(!(secret == null || "".equals(secret))) {
client.setClientSecret(passwordEncoder.encode(secret));
}
}
return client;
}
public void addClientDetails(ClientDetails clientDetails) {
mongoTemplate.insert(clientDetails, CONLLECTION_NAME);
}
public void updateClientDetails(ClientDetails clientDetails) {
Update update = new Update();
update.set("resourceIds", clientDetails.getResourceIds());
update.set("clientSecret", clientDetails.getClientSecret());
update.set("authorizedGrantTypes", clientDetails.getAuthorizedGrantTypes());
update.set("registeredRedirectUris", clientDetails.getRegisteredRedirectUri());
update.set("authorities", clientDetails.getAuthorities());
update.set("accessTokenValiditySeconds", clientDetails.getAccessTokenValiditySeconds());
update.set("refreshTokenValiditySeconds", clientDetails.getRefreshTokenValiditySeconds());
update.set("additionalInformation", clientDetails.getAdditionalInformation());
update.set("scope", clientDetails.getScope());
mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientDetails.getClientId())), update, CONLLECTION_NAME);
}
public void updateClientSecret(String clientId, String secret) {
Update update = new Update();
update.set("clientSecret", secret);
mongoTemplate.updateFirst(new Query(Criteria.where("clientId").is(clientId)), update, CONLLECTION_NAME);
}
public void removeClientDetails(String clientId) {
mongoTemplate.remove(new Query(Criteria.where("clientId").is(clientId)), CONLLECTION_NAME);
}
public List<ClientDetails> listClientDetails(){
return mongoTemplate.findAll(ClientDetails.class, CONLLECTION_NAME);
}
}
@Service("mongoTokenStore")
public class MongoTokenStore extends MongoSuportTemplate implements TokenStore {
private final String TOKEN_CONLLECTION_NAME = "oauth_access_token";
private final String TOKEN_REFRESH_CONLLECTION_NAME = "oauth_refresh_token";
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
public AuthenticationKeyGenerator getAuthenticationKeyGenerator() {
return authenticationKeyGenerator;
}
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return readAuthentication(token.getValue());
}
public OAuth2Authentication readAuthentication(String token) {
OAuth2Authentication authentication = null;
OauthAccessToken oauthAccessToken = mongoTemplate.findOne(new Query(Criteria.where("token_id").is(extractTokenKey(token))), OauthAccessToken.class,TOKEN_CONLLECTION_NAME);
if(oauthAccessToken != null) {
authentication = deserializeAuthentication(oauthAccessToken.getAuthentication());
}
return authentication;
}
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
String refreshToken = null;
if (token.getRefreshToken() != null) {
refreshToken = token.getRefreshToken().getValue();
}
if (readAccessToken(token.getValue())!=null) {
removeAccessToken(token.getValue());
}
OauthAccessToken oauthAccessToken = new OauthAccessToken();
oauthAccessToken.setToken_id(extractTokenKey(token.getValue()));
oauthAccessToken.setToken(serializeAccessToken(token));
oauthAccessToken.setAuthentication_id(authenticationKeyGenerator.extractKey(authentication));
oauthAccessToken.setUser_name(authentication.isClientOnly() ? null : authentication.getName());
oauthAccessToken.setClient_id(authentication.getOAuth2Request().getClientId());
oauthAccessToken.setAuthentication(serializeAuthentication(authentication));
oauthAccessToken.setRefresh_token(extractTokenKey(refreshToken));
mongoTemplate.insert(oauthAccessToken, TOKEN_CONLLECTION_NAME);
}
public OAuth2AccessToken readAccessToken(String tokenValue) {
OAuth2AccessToken accessToken = null;
OauthAccessToken oauthAccessToken = mongoTemplate.findOne(new Query(Criteria.where("token_id").is(extractTokenKey(tokenValue))), OauthAccessToken.class,TOKEN_CONLLECTION_NAME);
if(oauthAccessToken != null) {
accessToken = deserializeAccessToken(oauthAccessToken.getToken());
}
return accessToken;
}
public void removeAccessToken(OAuth2AccessToken token) {
removeAccessToken(token.getValue());
}
public void removeAccessToken(String tokenValue) {
mongoTemplate.remove(new Query(Criteria.where("token_id").is(extractTokenKey(tokenValue))), TOKEN_CONLLECTION_NAME);
}
public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
OauthRefreshToken oauthRefreshToken = new OauthRefreshToken();
oauthRefreshToken.setToken_id(extractTokenKey(refreshToken.getValue()));
oauthRefreshToken.setToken(serializeRefreshToken(refreshToken));
oauthRefreshToken.setAuthentication(serializeAuthentication(authentication));
mongoTemplate.insert(oauthRefreshToken, TOKEN_REFRESH_CONLLECTION_NAME);
}
public OAuth2RefreshToken readRefreshToken(String tokenValue) {
OAuth2RefreshToken refreshToken = null;
OauthRefreshToken oauthRefreshToken = mongoTemplate.findOne(new Query(Criteria.where("token_id").is(extractTokenKey(tokenValue))), OauthRefreshToken.class, TOKEN_REFRESH_CONLLECTION_NAME);
if(oauthRefreshToken != null) {
refreshToken = deserializeRefreshToken(oauthRefreshToken.getToken());
}
return refreshToken;
}
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return readAuthenticationForRefreshToken(token.getValue());
}
public OAuth2Authentication readAuthenticationForRefreshToken(String value) {
OAuth2Authentication authentication = null;
OauthRefreshToken oauthRefreshToken = mongoTemplate.findOne(new Query(Criteria.where("token_id").is(extractTokenKey(value))), OauthRefreshToken.class, TOKEN_REFRESH_CONLLECTION_NAME);
if(oauthRefreshToken != null) {
authentication = deserializeAuthentication(oauthRefreshToken.getAuthentication());
}
return authentication;
}
public void removeRefreshToken(OAuth2RefreshToken token) {
removeRefreshToken(token.getValue());
}
public void removeRefreshToken(String token) {
mongoTemplate.remove(new Query(Criteria.where("token_id").is(extractTokenKey(token))), TOKEN_REFRESH_CONLLECTION_NAME);
}
public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
removeAccessTokenUsingRefreshToken(refreshToken.getValue());
}
public void removeAccessTokenUsingRefreshToken(String refreshToken) {
mongoTemplate.remove(new Query(Criteria.where("refresh_token").is(extractTokenKey(refreshToken))), TOKEN_CONLLECTION_NAME);
}
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = authenticationKeyGenerator.extractKey(authentication);
OAuth2AccessToken accessToken = null;
OauthAccessToken oauthAccessToken = mongoTemplate.findOne(new Query(Criteria.where("authentication_id").is(key)), OauthAccessToken.class, TOKEN_CONLLECTION_NAME);
if(oauthAccessToken != null) {
accessToken = deserializeAccessToken(oauthAccessToken.getToken());
}
return accessToken;
}
public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
Query query = new Query();
query.addCriteria(Criteria.where("client_id").is(clientId));
query.addCriteria(Criteria.where("user_name"));
List<OauthAccessToken> tokenList = mongoTemplate.findAllAndRemove(query, OauthAccessToken.class, TOKEN_CONLLECTION_NAME);
Collection<OAuth2AccessToken> collection = new ArrayList<OAuth2AccessToken>();
if(!(tokenList == null || tokenList.isEmpty())) {
for(OauthAccessToken oauthAccessToken : tokenList) {
collection.add(deserializeAccessToken(oauthAccessToken.getToken()));
}
}
return collection;
}
public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
Query query = new Query();
query.addCriteria(Criteria.where("client_id").is(clientId));
List<OauthAccessToken> tokenList = mongoTemplate.findAllAndRemove(query, OauthAccessToken.class, TOKEN_CONLLECTION_NAME);
Collection<OAuth2AccessToken> collection = new ArrayList<OAuth2AccessToken>();
if(!(tokenList == null || tokenList.isEmpty())) {
for(OauthAccessToken oauthAccessToken : tokenList) {
collection.add(deserializeAccessToken(oauthAccessToken.getToken()));
}
}
return collection;
}
protected byte[] serializeAccessToken(OAuth2AccessToken token) {
return SerializationUtils.serialize(token);
}
protected byte[] serializeRefreshToken(OAuth2RefreshToken token) {
return SerializationUtils.serialize(token);
}
protected byte[] serializeAuthentication(OAuth2Authentication authentication) {
return SerializationUtils.serialize(authentication);
}
protected OAuth2AccessToken deserializeAccessToken(byte[] token) {
return SerializationUtils.deserialize(token);
}
protected OAuth2RefreshToken deserializeRefreshToken(byte[] token) {
return SerializationUtils.deserialize(token);
}
protected OAuth2Authentication deserializeAuthentication(byte[] authentication) {
return SerializationUtils.deserialize(authentication);
}
protected String extractTokenKey(String value) {
if (value == null) {
return null;
}
MessageDigest digest;
try {
digest = MessageDigest.getInstance("MD5");
}
catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).");
}
try {
byte[] bytes = digest.digest(value.getBytes("UTF-8"));
return String.format("%032x", new BigInteger(1, bytes));
}
catch (UnsupportedEncodingException e) {
throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).");
}
}
}
@Service("mongoUserDetailsService")
public class MongoUserDetailsService extends MongoSuportTemplate implements UserDetailsService {
private final String USER_CONLLECTION = "user";
@Autowired
private RoleService roleService;
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
FortUserDetails user = mongoTemplate.findOne(new Query(Criteria.where("username").is(username)), FortUserDetails.class, USER_CONLLECTION);
if(user == null) {
throw new RuntimeException("查询用户信息时出现异常");
}
String roleType = user.getRoletype();
if(roleType == null || "".equals(roleType)) {
throw new RuntimeException("用户未分配权限");
}
Role role = roleService.findOneByRoleName(roleType);
if(role == null) {
throw new RuntimeException("用户未分配权限");
}
List<GrantedAuthority> roleList = new ArrayList<GrantedAuthority>();
GrantedAuthority ga = new SimpleGrantedAuthority("ROLE_USER");
roleList.add(ga);
user.setAuthorities(roleList);
return user;
}
}
数据库表结构
由于本示例需要将token以及client_id以及client_secret保存在数据库中,以便重启后还可再次使用,所以采用了MongoDB存储,有关数据结构信息请参考“JdbcTokenStore.java”和“JdbcClientDetailsService.java”
启动授权服务
启动授权服务是很简单的,您只需要运行“Application.java”即可启动授权服务。
测试授权服务
授权模式
oauth2规范中具备了四种授权模式,分别如下:
·授权码模式:authorization code
·简化模式:implicit
·密码模式:resource owner password credentials
·客户端模式:client credentials
注:本示例只演示密码模式和客户端模式,感兴趣的同学自己花时间测试另外两种授权模式。
获取token
密码模式
1、通过接口“/oauth/token”获取token信息,如下所示:
$ curl -d "username=user&password=123&grant_type=password&client_id=client&client_secret=secret&scope=app" http://198.9.9.21:8080/oauth/token
返回:
{"access_token":"8f9c7a5f-b0dd-48a3-a8d0-5969f172c15e","token_type":"bearer","refresh_token":"9fba821d-cbea-40c1-bb48-e173878eb6a1","expires_in":43088,"scope":"app"}
2、通过接口“/oauth/token”刷新token,如下所示:
$ curl -d "grant_type=refresh_token&client_id=client&client_secret=secret&refresh_token=9fba821d-cbea-40c1-bb48-e173878eb6a1" http://198.9.9.21:8080/oauth/token
返回:
{"access_token":"1092a1fe-5426-47bd-8091-bdff79039f30","token_type":"bearer","refresh_token":"9fba821d-cbea-40c1-bb48-e173878eb6a1","expires_in":43199,"scope":"app"}
客户端模式
1、通过接口“/oauth/token”获取token信息,如下所示:
$ curl -d "grant_type=client_credentials&client_id=client&client_secret=secret&scope=app" http://198.9.9.21:8080/oauth/token
返回:
{"access_token":"3e40b12a-ccdf-4c93-981b-8923a2d22358","token_type":"bearer","expires_in":43199,"scope":"app"}
校验token
密码模式
1、通过接口“/oauth/check_token”校验token信息,如下所示:
$ curl -d "token=1092a1fe-5426-47bd-8091-bdff79039f30" http://client:secret@198.9.9.21:8080/oauth/check_token
返回:
{"active":true,"exp":1510678642,"user_name":"user","authorities":["ROLE_USER","ROLE_API","ROLE_RESOURCE"],"client_id":"client","scope":["app"]}
客户端模式
curl -d "token=3e40b12a-ccdf-4c93-981b-8923a2d22358" http://client:secret@198.9.9.21:8080/oauth/check_token
返回:
{"scope":["app"],"active":true,"exp":1510678854,"client_id":"client"}
注:密码模式在校验token时会返回用户权限信息,客户端模式则没有权限信息返回,所以当用户权限信息归授权服务管辖时则采用密码模式,否则采用客户端授权模式。
构建资源服务
1、使用eclipse构建一个名称为“oauth2-resource”的maven项目,将其pom.xml配置文件修改为如下:
<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>oauth2-resource</groupId>
<artifactId>oauth2-resource</artifactId>
<packaging>war</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>oauth2-resource Maven Webapp</name>
<url>http://maven.apache.org</url>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.6.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-security</artifactId>
<version>1.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>fort-spring-security-oauth2</groupId>
<artifactId>fort-spring-security-oauth2</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Log4j -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
<build>
<finalName>oauth2-resource</finalName>
</build>
</project>
2、新建程序启动类“Application.java”,如下所示:
@SpringBootApplication
@EnableResourceServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3、新建“AuthResourceConfig.java”类继承“ResourceServerConfigurerAdapter”类初始化资源服务所需要的信息,如下所示:
@Configuration
public class AuthResourceConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setClientId("client");
tokenService.setClientSecret("secret");
tokenService.setCheckTokenEndpointUrl("http://198.9.9.21:8080/oauth/check_token");
resources.tokenServices(tokenService);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests().antMatchers("/private/data").hasRole("RESOURCE")
.antMatchers("/api/data").permitAll();
}
}
4、新建“ResourceController.java”类构建资源服务所提供的接口,分别为 “/private/data”和“/api/data”,如下所示:
@RestController
public class ResourceController {
@RequestMapping("/private/data")
public String getResource() {
Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
OAuth2AuthenticationDetails oAuth2Authen = (OAuth2AuthenticationDetails) details;
System.out.println(oAuth2Authen.getTokenType());
System.out.println(SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString());
return "Hello! This is private data.";
}
@RequestMapping("/api/data")
public String getApiData() {
Object details = SecurityContextHolder.getContext().getAuthentication().getDetails();
if(details instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails oAuth2Authen = (OAuth2AuthenticationDetails) details;
System.out.println(oAuth2Authen.getTokenType());
System.out.println(SecurityContextHolder.getContext().getAuthentication().getPrincipal().toString());
return "Hello! This is api data.";
}
return "error";
}
}
5、新建“application.yml”配置文件,内容如下:
server:
port: 9001
启动资源服务
我们启动授权服务是通过运行“Application.java”来启动,同理也可以运行资源服务中的“Application.java”来启动资源服务。
资源服务接口权限测试
未授权访问接口
1、访问“/private/data”,如下所示:
$ curl http://198.9.9.21:9001/private/data
返回
{"error":"unauthorized","error_description":"Full authentication is required to access this resource"}
2、访问“/api /data”,如下所示:
$ curl http://198.9.9.21:9001/api/data
返回
error
使用token访问接口
1、访问“/private/data”,如下所示:
·使用密码模式授权token访问
$ curl -d "usernaH "Authorization:Bearer 01a65ff5-cec7-4e2e-8812-69abac4ad566" http://198.9.9.21:9001/private/data
返回
Hello! This is private data.
·使用客户端模式授权token访问
$ curl -H "Authorization:Bearer 3e40b12a-ccdf-4c93-981b-8923a2d22358" http://198.9.9.21:9001/private/data
返回
{"error":"access_denied","error_description":"Access is denied"}
2、访问“/api /data”,如下所示:
$ curl -H "Authorization:Bearer 3e40b12a-ccdf-4c93-981b-8923a2d22358" http://198.9.9.21:9001/api/data
返回
Hello! This is api data.