Spring|Spring Cloud+OAuth2+Spring Security+Redis 实现微服务统一认证授权,附源码
因为目前做了一个基于Spring Cloud的微服务项目,所以了解到了OAuth2,打算整合一下OAuth2来实现统一授权。关于OAuth是一个关于授权的开放网络标准,目前的版本是2.0,这里我就不多做介绍了。下面贴一下我学习过程中参考的资料。
开发环境:Windows10,Intellij Idea2018.2,jdk1.8,redis3.2.9, Spring Boot 2.0.2 Release, Spring Cloud Finchley.RC2 Spring 5.0.6
项目目录
文章图片
eshop —— 父级工程,管理jar包版本
eshop-server —— Eureka服务注册中心
eshop-gateway —— Zuul网关
eshop-auth —— 授权服务
eshop-member —— 会员服务
eshop-email —— 邮件服务(暂未使用)
eshop-common —— 通用类
关于如何构建一个基本的Spring Cloud 微服务这里就不赘述了,不会的可以看一下我的关于Spring Cloud系列的博客。这里给个入口地址:https://blog.csdn.net/wya1993/article/category/7701476
授权服务
首先构建eshop-auth服务,引入相关依赖
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">
eshop-parent
eshop-auth
war
eshop-common
spring-boot-starter-web
spring-cloud-starter-netflix-eureka-client
spring-cloud-starter-oauth2
spring-cloud-starter-security
spring-boot-starter-data-redis
mybatis-spring-boot-starter
spring-boot-starter-actuator
mysql-connector-java
druid
log4j
spring-boot-maven-plugin
接下来,配置Mybatis、redis、eureka,贴一下配置文件
server:
port:1203
spring:
application:
name:eshop-auth
redis:
database:0
host:192.168.0.117
port:6379
password:
jedis:
pool:
max-active:8
max-idle:8
min-idle:0
datasource:
driver-class-name:com.mysql.jdbc.Driver
url:jdbc:mysql://localhost:3306/eshop_member?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username:root
password:root
druid:
initialSize:5#初始化连接大小
minIdle:5#最小连接池数量
maxActive:20#最大连接池数量
maxWait:60000#获取连接时最大等待时间,单位毫秒
timeBetweenEvictionRunsMillis:60000#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
minEvictableIdleTimeMillis:300000#配置一个连接在池中最小生存的时间,单位是毫秒
validationQuery:SELECT1from DUAL#测试连接
testWhileIdle:true#申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性
testOnBorrow:false#获取连接时执行检测,建议关闭,影响性能
testOnReturn:false#归还连接时执行检测,建议关闭,影响性能
poolPreparedStatements:false#是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
maxPoolPreparedStatementPerConnectionSize:20#开启poolPreparedStatements后生效
filters:stat,wall,log4j #配置扩展插件,常用的插件有=>stat:监控统计log4j:日志wall:防御sql注入
connectionProperties:'druid.stat.mergeSql=true;
druid.stat.slowSqlMillis=5000'#通过connectProperties属性来打开mergeSql功能;
慢SQL记录
eureka:
instance:
prefer-ip-address:true
instance-id:${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone:http://localhost:1111/eureka/
mybatis:
type-aliases-package:com.curise.eshop.common.entity
configuration:
map-underscore-to-camel-case:true#开启驼峰命名,l_name->lName
jdbc-type-for-null:NULL
lazy-loading-enabled:true
aggressive-lazy-loading:true
cache-enabled:true#开启二级缓存
call-setters-on-nulls:true#map空列不显示问题
mapper-locations:
-classpath:mybatis/*.xml
AuthApplication添加@EnableDiscoveryClient和@MapperScan注解。
接下来配置认证服务器AuthorizationServerConfig ,并添加@Configuration和@EnableAuthorizationServer注解,其中ClientDetailsServiceConfigurer配置在内存中,当然也可以从数据库读取,以后慢慢完善。
@Configuration
@EnableAuthorizationServer
publicclassAuthorizationServerConfigextendsAuthorizationServerConfigurerAdapter{
@Autowired
privateAuthenticationManager authenticationManager;
@Autowired
privateDataSource dataSource;
@Autowired
privateRedisConnectionFactory redisConnectionFactory;
@Autowired
privateMyUserDetailService userDetailService;
@Bean
publicTokenStoretokenStore(){
returnnewRedisTokenStore(redisConnectionFactory);
}
@Override
publicvoidconfigure(AuthorizationServerSecurityConfigurer security)throwsException{
security
.allowFormAuthenticationForClients()
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
@Override
publicvoidconfigure(ClientDetailsServiceConfigurer clients)throwsException{
// clients.withClientDetails(clientDetails());
clients.inMemory()
.withClient("android")
.scopes("read")
.secret("android")
.authorizedGrantTypes("password","authorization_code","refresh_token")
.and()
.withClient("webapp")
.scopes("read")
.authorizedGrantTypes("implicit")
.and()
.withClient("browser")
.authorizedGrantTypes("refresh_token","password")
.scopes("read");
}
@Bean
publicClientDetailsServiceclientDetails(){
returnnewJdbcClientDetailsService(dataSource);
}
@Bean
publicWebResponseExceptionTranslatorwebResponseExceptionTranslator(){
returnnewMssWebResponseExceptionTranslator();
}
@Override
publicvoidconfigure(AuthorizationServerEndpointsConfigurer endpoints)throwsException{
endpoints.tokenStore(tokenStore())
.userDetailsService(userDetailService)
.authenticationManager(authenticationManager);
endpoints.tokenServices(defaultTokenServices());
//认证异常翻译
// endpoints.exceptionTranslator(webResponseExceptionTranslator());
}
/**
*
注意,自定义TokenServices的时候,需要设置@Primary,否则报错,
* @return
*/
@Primary
@Bean
publicDefaultTokenServicesdefaultTokenServices(){
DefaultTokenServices tokenServices=newDefaultTokenServices();
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);
returntokenServices;
}
}
在上述配置中,认证的token是存到redis里的,如果你这里使用了Spring5.0以上的版本的话,使用默认的RedisTokenStore认证时会报如下异常:
nested exception is java.lang.NoSuchMethodError:org.springframework.data.redis.connection.RedisConnection.set([B[B)V
原因是spring-data-redis 2.0版本中set(String,String)被弃用了,要使用RedisConnection.stringCommands().set(…),所有我自定义一个RedisTokenStore,代码和RedisTokenStore一样,只是把所有conn.set(…)都换成conn..stringCommands().set(…),测试后方法可行。
publicclassRedisTokenStoreimplementsTokenStore{
privatestaticfinalString ACCESS="access:";
privatestaticfinalString AUTH_TO_ACCESS="auth_to_access:";
privatestaticfinalString AUTH="auth:";
privatestaticfinalString REFRESH_AUTH="refresh_auth:";
privatestaticfinalString ACCESS_TO_REFRESH="access_to_refresh:";
privatestaticfinalString REFRESH="refresh:";
privatestaticfinalString REFRESH_TO_ACCESS="refresh_to_access:";
privatestaticfinalString CLIENT_ID_TO_ACCESS="client_id_to_access:";
privatestaticfinalString UNAME_TO_ACCESS="uname_to_access:";
privatefinalRedisConnectionFactory connectionFactory;
privateAuthenticationKeyGenerator authenticationKeyGenerator=newDefaultAuthenticationKeyGenerator();
privateRedisTokenStoreSerializationStrategy serializationStrategy=newJdkSerializationStrategy();
privateString prefix="";
publicRedisTokenStore(RedisConnectionFactory connectionFactory){
this.connectionFactory=connectionFactory;
}
publicvoidsetAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator){
this.authenticationKeyGenerator=authenticationKeyGenerator;
}
publicvoidsetSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy){
this.serializationStrategy=serializationStrategy;
}
publicvoidsetPrefix(String prefix){
this.prefix=prefix;
}
privateRedisConnectiongetConnection(){
returnthis.connectionFactory.getConnection();
}
privatebyte[]serialize(Object object){
returnthis.serializationStrategy.serialize(object);
}
privatebyte[]serializeKey(String object){
returnthis.serialize(this.prefix+object);
}
privateOAuth2AccessTokendeserializeAccessToken(byte[]bytes){
return(OAuth2AccessToken)this.serializationStrategy.deserialize(bytes,OAuth2AccessToken.class);
}
privateOAuth2AuthenticationdeserializeAuthentication(byte[]bytes){
return(OAuth2Authentication)this.serializationStrategy.deserialize(bytes,OAuth2Authentication.class);
}
privateOAuth2RefreshTokendeserializeRefreshToken(byte[]bytes){
return(OAuth2RefreshToken)this.serializationStrategy.deserialize(bytes,OAuth2RefreshToken.class);
}
privatebyte[]serialize(String string){
returnthis.serializationStrategy.serialize(string);
}
privateStringdeserializeString(byte[]bytes){
returnthis.serializationStrategy.deserializeString(bytes);
}
@Override
publicOAuth2AccessTokengetAccessToken(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);
}
}
returnaccessToken;
}
@Override
publicOAuth2AuthenticationreadAuthentication(OAuth2AccessToken token){
returnthis.readAuthentication(token.getValue());
}
@Override
publicOAuth2AuthenticationreadAuthentication(String token){
byte[]bytes=null;
RedisConnection conn=this.getConnection();
try{
bytes=conn.get(this.serializeKey("auth:"+token));
}finally{
conn.close();
}
OAuth2Authentication auth=this.deserializeAuthentication(bytes);
returnauth;
}
@Override
publicOAuth2AuthenticationreadAuthenticationForRefreshToken(OAuth2RefreshToken token){
returnthis.readAuthenticationForRefreshToken(token.getValue());
}
publicOAuth2AuthenticationreadAuthenticationForRefreshToken(String token){
RedisConnection conn=getConnection();
try{
byte[]bytes=conn.get(serializeKey(REFRESH_AUTH+token));
OAuth2Authentication auth=deserializeAuthentication(bytes);
returnauth;
}finally{
conn.close();
}
}
@Override
publicvoidstoreAccessToken(OAuth2AccessToken token,OAuth2Authentication authentication){
byte[]serializedAccessToken=serialize(token);
byte[]serializedAuth=serialize(authentication);
byte[]accessKey=serializeKey(ACCESS+token.getValue());
byte[]authKey=serializeKey(AUTH+token.getValue());
byte[]authToAccessKey=serializeKey(AUTH_TO_ACCESS+authenticationKeyGenerator.extractKey(authentication));
byte[]approvalKey=serializeKey(UNAME_TO_ACCESS+getApprovalKey(authentication));
byte[]clientId=serializeKey(CLIENT_ID_TO_ACCESS+authentication.getOAuth2Request().getClientId());
RedisConnection conn=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,serializedAccessToken);
}
conn.rPush(clientId,serializedAccessToken);
if(token.getExpiration()!=null){
intseconds=token.getExpiresIn();
conn.expire(accessKey,seconds);
conn.expire(authKey,seconds);
conn.expire(authToAccessKey,seconds);
conn.expire(clientId,seconds);
conn.expire(approvalKey,seconds);
}
OAuth2RefreshToken refreshToken=token.getRefreshToken();
if(refreshToken!=null&&refreshToken.getValue()!=null){
byte[]refresh=serialize(token.getRefreshToken().getValue());
byte[]auth=serialize(token.getValue());
byte[]refreshToAccessKey=serializeKey(REFRESH_TO_ACCESS+token.getRefreshToken().getValue());
conn.stringCommands().set(refreshToAccessKey,auth);
byte[]accessToRefreshKey=serializeKey(ACCESS_TO_REFRESH+token.getValue());
conn.stringCommands().set(accessToRefreshKey,refresh);
if(refreshTokeninstanceofExpiringOAuth2RefreshToken){
ExpiringOAuth2RefreshToken expiringRefreshToken=(ExpiringOAuth2RefreshToken)refreshToken;
Date expiration=expiringRefreshToken.getExpiration();
if(expiration!=null){
intseconds=Long.valueOf((expiration.getTime()-System.currentTimeMillis())/1000L)
.intValue();
conn.expire(refreshToAccessKey,seconds);
conn.expire(accessToRefreshKey,seconds);
}
}
}
conn.closePipeline();
}finally{
conn.close();
}
}
privatestaticStringgetApprovalKey(OAuth2Authentication authentication){
String userName=authentication.getUserAuthentication()==null?"":authentication.getUserAuthentication().getName();
returngetApprovalKey(authentication.getOAuth2Request().getClientId(),userName);
}
privatestaticStringgetApprovalKey(String clientId,String userName){
returnclientId+(userName==null?"":":"+userName);
}
@Override
publicvoidremoveAccessToken(OAuth2AccessToken accessToken){
this.removeAccessToken(accessToken.getValue());
}
@Override
publicOAuth2AccessTokenreadAccessToken(String tokenValue){
byte[]key=serializeKey(ACCESS+tokenValue);
byte[]bytes=null;
RedisConnection conn=getConnection();
try{
bytes=conn.get(key);
}finally{
conn.close();
}
OAuth2AccessToken accessToken=deserializeAccessToken(bytes);
returnaccessToken;
}
publicvoidremoveAccessToken(String tokenValue){
byte[]accessKey=serializeKey(ACCESS+tokenValue);
byte[]authKey=serializeKey(AUTH+tokenValue);
byte[]accessToRefreshKey=serializeKey(ACCESS_TO_REFRESH+tokenValue);
RedisConnection conn=getConnection();
try{
conn.openPipeline();
conn.get(accessKey);
conn.get(authKey);
conn.del(accessKey);
conn.del(accessToRefreshKey);
// Don't remove the refresh token - it's up to the caller to do that
conn.del(authKey);
List
推荐阅读
- Activiti(一)SpringBoot2集成Activiti6
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- 2018-07-09|2018-07-09 Spring 的DBCP,c3p0
- 研途14|研途14 2018-03-22
- spring|spring boot项目启动websocket
- Spring|Spring Boot 整合 Activiti6.0.0
- Spring集成|Spring集成 Mina
- springboot使用redis缓存
- Spring|Spring 框架之 AOP 原理剖析已经出炉!!!预定的童鞋可以识别下发二维码去看了