目录
一:使用场景和考虑
二:解决方案
1.spring security中用户登录做数据库或者远程调用
2.认证服务器中的-token access存储:
3.客户端的相关的配置
在上一节,我们主要配置授权码和密码模式, 主要还是2个配置config。
一个是资源服务器的配置,包含了写死了的客户端,本地存储token以及访问约束安全配置。我们需要思考的问题:
(1)客户端能否在数据库中配置?
(2)access token存储放在本地内存,如果分布式部署不能共享怎么办?
第二个是spring security的配置,就一个用户授权码登录校验.。我们需要思考:
(1)用户配值又是写死在代理,如果放在数据库中怎样?(实际生产中用户密码等肯定是另一个服务中,这里怎么办?)
我们从简单到难的依次解决:
(1)自定义service实现UserDetailsService(关于一些注意,我写在代码的注释中了)
package springcloud.oauth2.service;
import java.util.ArrayList;
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.factory.PasswordEncoderFactories;
import org.springframework.stereotype.Service;
/**
* 这里是用户登录验证使用
* @代码描述:
* @author monxz
*
* @time 2019年6月21日
*/
@Service
public class UserServiceDetail implements UserDetailsService {
//TODO:这里写死了,实际上就是从数据库中抓取,在实际生产中时候远程调用的,所以我写死了这里
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.err.println("编码123456后:"+PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"));
User user=new User("admin",PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("admin"), true, true, true, true, new ArrayList<>());
return user;
}
}
(2)spring security的配置中就调用这个service来校验用户账户密码
package springcloud.oauth2.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import springcloud.oauth2.service.UserServiceDetail;
/**
*
* @代码描述:spring security的配置
* @author monxz
*
* @time 2019年6月21日
*/
@EnableWebSecurity//开启权限验证
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceDetail details;
/**
* 配置这个bean会在做AuthorizationServerConfigurer配置的时候使用
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
//
// /**
// * 配置用户
// * 使用内存中的用户,实际项目中,一般使用的是数据库保存用户,具体的实现类可以使用JdbcDaoImpl或者JdbcUserDetailsManager
// * @return
// */
// @Bean
// @Override
// protected UserDetailsService userDetailsService() {
// InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// manager.createUser(User.withUsername("admin").password(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("admin")).authorities("USER").build());
// return manager;
// }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(details);
}
}
这里思考一个问题:在上述的方法中我们看到一旦第三方获取access token就一直可用。这样token一直有效,明显不科学。但
是每次都重新请求获取token,有特别消耗资源。如果设置token的失效时间合适,就可以在2个中平衡
(1)自定义token的存储方式(直接复制就可用)
package springcloud.oauth2.config.redis;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.JdkSerializationStrategy;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStoreSerializationStrategy;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
@Slf4j
public class MyRedisTokenStore implements TokenStore {
private static final String ACCESS = "access:";
private static final String AUTH_TO_ACCESS = "auth_to_access:";
private static final String AUTH = "auth:";
private static final String REFRESH_AUTH = "refresh_auth:";
private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
private static final String REFRESH = "refresh:";
private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
private static final String UNAME_TO_ACCESS = "uname_to_access:";
private final RedisConnectionFactory connectionFactory;
private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
private String prefix = "";
public MyRedisTokenStore(RedisConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
this.authenticationKeyGenerator = authenticationKeyGenerator;
}
public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
this.serializationStrategy = serializationStrategy;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
private RedisConnection getConnection() {
return this.connectionFactory.getConnection();
}
private byte[] serialize(Object object) {
return this.serializationStrategy.serialize(object);
}
private byte[] serializeKey(String object) {
return this.serialize(this.prefix + object);
}
private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
return (OAuth2AccessToken)this.serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
}
private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
return (OAuth2Authentication)this.serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
}
private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
return (OAuth2RefreshToken)this.serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
}
private byte[] serialize(String string) {
return this.serializationStrategy.serialize(string);
}
private String deserializeString(byte[] bytes) {
return this.serializationStrategy.deserializeString(bytes);
}
public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
String key = this.authenticationKeyGenerator.extractKey(authentication);
byte[] serializedKey = this.serializeKey("auth_to_access:" + key);
byte[] bytes = null;
RedisConnection conn = this.getConnection();
try {
bytes = conn.get(serializedKey);
} finally {
conn.close();
}
OAuth2AccessToken accessToken = this.deserializeAccessToken(bytes);
if (accessToken != null) {
OAuth2Authentication storedAuthentication = this.readAuthentication(accessToken.getValue());
if (storedAuthentication == null || !key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))) {
this.storeAccessToken(accessToken, authentication);
}
}
return accessToken;
}
public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
return this.readAuthentication(token.getValue());
}
public OAuth2Authentication readAuthentication(String token) {
byte[] bytes = null;
RedisConnection conn = this.getConnection();
try {
bytes = conn.get(this.serializeKey("auth:" + token));
} finally {
conn.close();
}
OAuth2Authentication var4 = this.deserializeAuthentication(bytes);
return var4;
}
public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
return this.readAuthenticationForRefreshToken(token.getValue());
}
public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
RedisConnection conn = this.getConnection();
OAuth2Authentication var5;
try {
byte[] bytes = conn.get(this.serializeKey("refresh_auth:" + token));
OAuth2Authentication auth = this.deserializeAuthentication(bytes);
var5 = auth;
} finally {
conn.close();
}
return var5;
}
//生成的access-token存储在redis中
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
byte[] serializedAccessToken = this.serialize((Object)token);
byte[] serializedAuth = this.serialize((Object)authentication);
byte[] accessKey = this.serializeKey("access:" + token.getValue());
byte[] authKey = this.serializeKey("auth:" + token.getValue());
byte[] authToAccessKey = this.serializeKey("auth_to_access:" + this.authenticationKeyGenerator.extractKey(authentication));
byte[] approvalKey = this.serializeKey("uname_to_access:" + getApprovalKey(authentication));
byte[] clientId = this.serializeKey("client_id_to_access:" + authentication.getOAuth2Request().getClientId());
RedisConnection conn = this.getConnection();
try {
conn.openPipeline();
conn.stringCommands().set(accessKey, serializedAccessToken);
conn.stringCommands().set(authKey, serializedAuth);
conn.stringCommands().set(authToAccessKey, serializedAccessToken);
if (!authentication.isClientOnly()) {
conn.rPush(approvalKey, new byte[][]{serializedAccessToken});
}
conn.rPush(clientId, new byte[][]{serializedAccessToken});
if (token.getExpiration() != null) {
int seconds = token.getExpiresIn();
conn.expire(accessKey, (long)seconds);
conn.expire(authKey, (long)seconds);
conn.expire(authToAccessKey, (long)seconds);
conn.expire(clientId, (long)seconds);
conn.expire(approvalKey, (long)seconds);
}
OAuth2RefreshToken refreshToken = token.getRefreshToken();
if (refreshToken != null && refreshToken.getValue() != null) {
byte[] refresh = this.serialize(token.getRefreshToken().getValue());
byte[] auth = this.serialize(token.getValue());
byte[] refreshToAccessKey = this.serializeKey("refresh_to_access:" + token.getRefreshToken().getValue());
conn.stringCommands().set(refreshToAccessKey, auth);
byte[] accessToRefreshKey = this.serializeKey("access_to_refresh:" + token.getValue());
conn.stringCommands().set(accessToRefreshKey, refresh);
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken)refreshToken;
Date expiration = expiringRefreshToken.getExpiration();
if (expiration != null) {
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue();
conn.expire(refreshToAccessKey, (long)seconds);
conn.expire(accessToRefreshKey, (long)seconds);
}
}
}
conn.closePipeline();
} finally {
conn.close();
}
}
private static String getApprovalKey(OAuth2Authentication authentication) {
String userName = authentication.getUserAuthentication() == null ? "" : authentication.getUserAuthentication().getName();
return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
}
private static String getApprovalKey(String clientId, String userName) {
return clientId + (userName == null ? "" : ":" + userName);
}
public void removeAccessToken(OAuth2AccessToken accessToken) {
this.removeAccessToken(accessToken.getValue());
}
public OAuth2AccessToken readAccessToken(String tokenValue) {
log.info("ACCESS--2");
byte[] key = this.serializeKey("access:" + tokenValue);
byte[] bytes = null;
RedisConnection conn = this.getConnection();
try {
bytes = conn.get(key);
} finally {
conn.close();
}
OAuth2AccessToken var5 = this.deserializeAccessToken(bytes);
return var5;
}
public void removeAccessToken(String tokenValue) {
log.info("ACCESS--3");
byte[] accessKey = this.serializeKey("access:" + tokenValue);
byte[] authKey = this.serializeKey("auth:" + tokenValue);
byte[] accessToRefreshKey = this.serializeKey("access_to_refresh:" + tokenValue);
RedisConnection conn = this.getConnection();
try {
conn.openPipeline();
conn.get(accessKey);
conn.get(authKey);
conn.del(new byte[][]{accessKey});
conn.del(new byte[][]{accessToRefreshKey});
conn.del(new byte[][]{authKey});
List
(2)认证服务器中使用自定义的存储(注意这里如果校验用户密码,因为放在redis中了,必须用上面的userServiceDetail的
用户校验)
//===========================自定义access token保存
@Bean
public TokenStore tokenStore() {
return new MyRedisTokenStore(redisConnectionFactory);
}
@Primary
@Bean
public DefaultTokenServices defaultTokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
//tokenServices.setClientDetailsService(clientDetails());
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60*60*12);
// refresh_token默认30天
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.userDetailsService(userServiceDetail) //如果使用了自定义service,这里必须配置
.authenticationManager(authenticationManager);
endpoints.tokenServices(defaultTokenServices());
}
(1)添加mysql的依赖和链接
(2)认证服务器的客户端配置
// ################# 以下配置jdbc ############
@Autowired
private DataSource dataSource;
@Autowired
private UserServiceDetail userServiceDetail;
//======================1.
@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
4.认证服务器配置完整代码
package springcloud.oauth2.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.security.authentication.AuthenticationManager;
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;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;
import springcloud.oauth2.config.redis.MyRedisTokenStore;
import springcloud.oauth2.service.UserServiceDetail;
import javax.sql.DataSource;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisConnectionFactory redisConnectionFactory;
// ################# 以下配置jdbc ############
@Autowired
private DataSource dataSource;
@Autowired
private UserServiceDetail userServiceDetail;
//======================1.
@Bean // 声明 ClientDetails实现
public ClientDetailsService clientDetailsService() {
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetailsService());
}
//===========================自定义access token保存
@Bean
public TokenStore tokenStore() {
return new MyRedisTokenStore(redisConnectionFactory);
}
@Primary
@Bean
public DefaultTokenServices defaultTokenServices(){
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
//tokenServices.setClientDetailsService(clientDetails());
// token有效期自定义设置,默认12小时
tokenServices.setAccessTokenValiditySeconds(60*60*12);
// refresh_token默认30天
tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
return tokenServices;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(tokenStore())
.userDetailsService(userServiceDetail) //如果使用了自定义service,这里必须配置
.authenticationManager(authenticationManager);
endpoints.tokenServices(defaultTokenServices());
}
//=====token安全约束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}