因?yàn)槟壳白隽艘粋€(gè)基于Spring Cloud的微服務(wù)項(xiàng)目暖哨,所以了解到了OAuth2肌厨,打算整合一下OAuth2來實(shí)現(xiàn)統(tǒng)一授權(quán)培慌。關(guān)于OAuth是一個(gè)關(guān)于授權(quán)的開放網(wǎng)絡(luò)標(biāo)準(zhǔn),目前的版本是2.0柑爸,這里我就不多做介紹了吵护。下面貼一下我學(xué)習(xí)過程中參考的資料。
開發(fā)環(huán)境:Windows10表鳍,? Intellij Idea2018.2馅而,? ?jdk1.8,? redis3.2.9进胯, Spring Boot 2.0.2 Release, Spring Cloud Finchley.RC2 Spring 5.0.6
項(xiàng)目目錄
eshop —— 父級工程原押,管理jar包版本
eshop-server —— Eureka服務(wù)注冊中心
eshop-gateway —— Zuul網(wǎng)關(guān)
eshop-auth —— 授權(quán)服務(wù)
eshop-member —— 會(huì)員服務(wù)
eshop-email —— 郵件服務(wù)(暫未使用)
eshop-common —— 通用類
關(guān)于如何構(gòu)建一個(gè)基本的Spring Cloud 微服務(wù)這里就不贅述了胁镐,不會(huì)的可以看一下我的關(guān)于Spring Cloud系列的博客。這里給個(gè)入口地址:https://blog.csdn.net/wya1993/article/category/7701476
授權(quán)服務(wù)
首先構(gòu)建eshop-auth服務(wù),引入相關(guān)依賴
<?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>eshop-parent</artifactId>
<groupId>com.curise.eshop</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eshop-auth</artifactId>
<packaging>war</packaging>
<description>授權(quán)模塊</description>
<dependencies>
<dependency>
<groupId>com.curise.eshop</groupId>
<artifactId>eshop-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
接下來盯漂,配置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#最小連接池?cái)?shù)量
maxActive:20#最大連接池?cái)?shù)量
maxWait:60000#獲取連接時(shí)最大等待時(shí)間帖渠,單位毫秒
timeBetweenEvictionRunsMillis:60000#配置間隔多久才進(jìn)行一次檢測,檢測需要關(guān)閉的空閑連接竭宰,單位是毫秒
minEvictableIdleTimeMillis:300000#配置一個(gè)連接在池中最小生存的時(shí)間空郊,單位是毫秒
validationQuery:SELECT1from DUAL? #測試連接
testWhileIdle:true#申請連接的時(shí)候檢測,建議配置為true切揭,不影響性能狞甚,并且保證安全性
testOnBorrow:false#獲取連接時(shí)執(zhí)行檢測,建議關(guān)閉廓旬,影響性能
testOnReturn:false#歸還連接時(shí)執(zhí)行檢測哼审,建議關(guān)閉,影響性能
poolPreparedStatements:false#是否開啟PSCache孕豹,PSCache對支持游標(biāo)的數(shù)據(jù)庫性能提升巨大涩盾,oracle建議開啟,mysql下建議關(guān)閉
maxPoolPreparedStatementPerConnectionSize:20#開啟poolPreparedStatements后生效
filters:stat,wall,log4j #配置擴(kuò)展插件励背,常用的插件有=>stat:監(jiān)控統(tǒng)計(jì)? 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注解春霍。
接下來配置認(rèn)證服務(wù)器AuthorizationServerConfig?,并添加@Configuration和@EnableAuthorizationServer注解椅野,其中ClientDetailsServiceConfigurer配置在內(nèi)存中终畅,當(dāng)然也可以從數(shù)據(jù)庫讀取,以后慢慢完善竟闪。
@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());
//認(rèn)證異常翻譯
// endpoints.exceptionTranslator(webResponseExceptionTranslator());
}
/**
*
注意离福,自定義TokenServices的時(shí)候,需要設(shè)置@Primary炼蛤,否則報(bào)錯(cuò)妖爷,
* @return
*/
@Primary
@Bean
publicDefaultTokenServicesdefaultTokenServices(){
DefaultTokenServices tokenServices=newDefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
tokenServices.setSupportRefreshToken(true);
//tokenServices.setClientDetailsService(clientDetails());
// token有效期自定義設(shè)置,默認(rèn)12小時(shí)
tokenServices.setAccessTokenValiditySeconds(60*60*12);
// refresh_token默認(rèn)30天
tokenServices.setRefreshTokenValiditySeconds(60*60*24*7);
returntokenServices;
}
}
在上述配置中理朋,認(rèn)證的token是存到redis里的絮识,如果你這里使用了Spring5.0以上的版本的話,使用默認(rèn)的RedisTokenStore認(rèn)證時(shí)會(huì)報(bào)如下異常:
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(…)次舌,所有我自定義一個(gè)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<Object>results=conn.closePipeline();
byte[]access=(byte[])results.get(0);
byte[]auth=(byte[])results.get(1);
OAuth2Authentication authentication=deserializeAuthentication(auth);
if(authentication!=null){
String key=authenticationKeyGenerator.extractKey(authentication);
byte[]authToAccessKey=serializeKey(AUTH_TO_ACCESS+key);
byte[]unameKey=serializeKey(UNAME_TO_ACCESS+getApprovalKey(authentication));
byte[]clientId=serializeKey(CLIENT_ID_TO_ACCESS+authentication.getOAuth2Request().getClientId());
conn.openPipeline();
conn.del(authToAccessKey);
conn.lRem(unameKey,1,access);
conn.lRem(clientId,1,access);
conn.del(serialize(ACCESS+key));
conn.closePipeline();
}
}finally{
conn.close();
}
}
@Override
publicvoidstoreRefreshToken(OAuth2RefreshToken refreshToken,OAuth2Authentication authentication){
byte[]refreshKey=serializeKey(REFRESH+refreshToken.getValue());
byte[]refreshAuthKey=serializeKey(REFRESH_AUTH+refreshToken.getValue());
byte[]serializedRefreshToken=serialize(refreshToken);
RedisConnection conn=getConnection();
try{
conn.openPipeline();
conn.stringCommands().set(refreshKey,serializedRefreshToken);
conn.stringCommands().set(refreshAuthKey,serialize(authentication));
if(refreshTokeninstanceofExpiringOAuth2RefreshToken){
ExpiringOAuth2RefreshToken expiringRefreshToken=(ExpiringOAuth2RefreshToken)refreshToken;
Date expiration=expiringRefreshToken.getExpiration();
if(expiration!=null){
intseconds=Long.valueOf((expiration.getTime()-System.currentTimeMillis())/1000L)
.intValue();
conn.expire(refreshKey,seconds);
conn.expire(refreshAuthKey,seconds);
}
}
conn.closePipeline();
}finally{
conn.close();
}
}
@Override
publicOAuth2RefreshTokenreadRefreshToken(String tokenValue){
byte[]key=serializeKey(REFRESH+tokenValue);
byte[]bytes=null;
RedisConnection conn=getConnection();
try{
bytes=conn.get(key);
}finally{
conn.close();
}
OAuth2RefreshToken refreshToken=deserializeRefreshToken(bytes);
returnrefreshToken;
}
@Override
publicvoidremoveRefreshToken(OAuth2RefreshToken refreshToken){
this.removeRefreshToken(refreshToken.getValue());
}
publicvoidremoveRefreshToken(String tokenValue){
byte[]refreshKey=serializeKey(REFRESH+tokenValue);
byte[]refreshAuthKey=serializeKey(REFRESH_AUTH+tokenValue);
byte[]refresh2AccessKey=serializeKey(REFRESH_TO_ACCESS+tokenValue);
byte[]access2RefreshKey=serializeKey(ACCESS_TO_REFRESH+tokenValue);
RedisConnection conn=getConnection();
try{
conn.openPipeline();
conn.del(refreshKey);
conn.del(refreshAuthKey);
conn.del(refresh2AccessKey);
conn.del(access2RefreshKey);
conn.closePipeline();
}finally{
conn.close();
}
}
@Override
publicvoidremoveAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken){
this.removeAccessTokenUsingRefreshToken(refreshToken.getValue());
}
privatevoidremoveAccessTokenUsingRefreshToken(String refreshToken){
byte[]key=serializeKey(REFRESH_TO_ACCESS+refreshToken);
List<Object>results=null;
RedisConnection conn=getConnection();
try{
conn.openPipeline();
conn.get(key);
conn.del(key);
results=conn.closePipeline();
}finally{
conn.close();
}
if(results==null){
return;
}
byte[]bytes=(byte[])results.get(0);
String accessToken=deserializeString(bytes);
if(accessToken!=null){
removeAccessToken(accessToken);
}
}
@Override
publicCollection<OAuth2AccessToken>findTokensByClientIdAndUserName(String clientId,String userName){
byte[]approvalKey=serializeKey(UNAME_TO_ACCESS+getApprovalKey(clientId,userName));
List<byte[]>byteList=null;
RedisConnection conn=getConnection();
try{
byteList=conn.lRange(approvalKey,0,-1);
}finally{
conn.close();
}
if(byteList==null||byteList.size()==0){
returnCollections.<OAuth2AccessToken>emptySet();
}
List<OAuth2AccessToken>accessTokens=newArrayList<OAuth2AccessToken>(byteList.size());
for(byte[]bytes:byteList){
OAuth2AccessToken accessToken=deserializeAccessToken(bytes);
accessTokens.add(accessToken);
}
returnCollections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);
}
@Override
publicCollection<OAuth2AccessToken>findTokensByClientId(String clientId){
byte[]key=serializeKey(CLIENT_ID_TO_ACCESS+clientId);
List<byte[]>byteList=null;
RedisConnection conn=getConnection();
try{
byteList=conn.lRange(key,0,-1);
}finally{
conn.close();
}
if(byteList==null||byteList.size()==0){
returnCollections.<OAuth2AccessToken>emptySet();
}
List<OAuth2AccessToken>accessTokens=newArrayList<OAuth2AccessToken>(byteList.size());
for(byte[]bytes:byteList){
OAuth2AccessToken accessToken=deserializeAccessToken(bytes);
accessTokens.add(accessToken);
}
returnCollections.<OAuth2AccessToken>unmodifiableCollection(accessTokens);
}
}
配置資源服務(wù)器
@Configuration
@EnableResourceServer
@Order(3)
publicclassResourceServerConfigextendsResourceServerConfigurerAdapter{
@Override
publicvoidconfigure(HttpSecurity http)throwsException{
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request,response,authException)->response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.httpBasic();
}
}
配置Spring Security
@Configuration
@EnableWebSecurity
@Order(2)
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{
@Autowired
privateMyUserDetailService userDetailService;
@Bean
publicPasswordEncoderpasswordEncoder(){
//return new BCryptPasswordEncoder();
returnnewNoEncryptPasswordEncoder();
}
@Override
protectedvoidconfigure(HttpSecurity http)throwsException{
http.requestMatchers().antMatchers("/oauth/**")
.and()
.authorizeRequests()
.antMatchers("/oauth/**").authenticated()
.and()
.csrf().disable();
}
@Override
protectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{
auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
}
/**
* 不定義沒有password grant_type
*
* @return
* @throws Exception
*/
@Override
@Bean
publicAuthenticationManagerauthenticationManagerBean()throwsException{
returnsuper.authenticationManagerBean();
}
}
可以看到ResourceServerConfig 是比SecurityConfig 的優(yōu)先級低的。
二者的關(guān)系:
ResourceServerConfig 用于保護(hù)oauth相關(guān)的endpoints逐沙,同時(shí)主要作用于用戶的登錄(form login,Basic auth)
SecurityConfig 用于保護(hù)oauth要開放的資源哲思,同時(shí)主要作用于client端以及token的認(rèn)證(Bearer auth)
所以我們讓SecurityConfig優(yōu)先于ResourceServerConfig,且在SecurityConfig 不攔截oauth要開放的資源吩案,在ResourceServerConfig 中配置需要token驗(yàn)證的資源棚赔,也就是我們對外提供的接口。所以這里對于所有微服務(wù)的接口定義有一個(gè)要求徘郭,就是全部以/api開頭靠益。
如果這里不這樣配置的話,在你拿到access_token去請求各個(gè)接口時(shí)會(huì)報(bào)invalid_token的提示崎岂。
另外捆毫,由于我們自定義認(rèn)證邏輯,所以需要重寫UserDetailService
@Service("userDetailService")
publicclassMyUserDetailServiceimplementsUserDetailsService{
@Autowired
privateMemberDao memberDao;
@Override
publicUserDetailsloadUserByUsername(String memberName)throwsUsernameNotFoundException{
Member member=memberDao.findByMemberName(memberName);
if(member==null){
thrownewUsernameNotFoundException(memberName);
}
Set<GrantedAuthority>grantedAuthorities=newHashSet<>();
// 可用性 :true:可用 false:不可用
booleanenabled=true;
// 過期性 :true:沒過期 false:過期
booleanaccountNonExpired=true;
// 有效性 :true:憑證有效 false:憑證無效
booleancredentialsNonExpired=true;
// 鎖定性 :true:未鎖定 false:已鎖定
booleanaccountNonLocked=true;
for(Role role:member.getRoles()){
//角色必須是ROLE_開頭冲甘,可以在數(shù)據(jù)庫中設(shè)置
GrantedAuthority grantedAuthority=newSimpleGrantedAuthority(role.getRoleName());
grantedAuthorities.add(grantedAuthority);
//獲取權(quán)限
for(Permission permission:role.getPermissions()){
GrantedAuthority authority=newSimpleGrantedAuthority(permission.getUri());
grantedAuthorities.add(authority);
}
}
User user=newUser(member.getMemberName(),member.getPassword(),
enabled,accountNonExpired,credentialsNonExpired,accountNonLocked,grantedAuthorities);
returnuser;
}
}
密碼驗(yàn)證為了方便我使用了不加密的方式绩卤,重寫了PasswordEncoder,實(shí)際開發(fā)還是建議使用BCryptPasswordEncoder江醇。
publicclassNoEncryptPasswordEncoderimplementsPasswordEncoder{
@Override
publicStringencode(CharSequence charSequence){
return(String)charSequence;
}
@Override
publicbooleanmatches(CharSequence charSequence,String s){
returns.equals((String)charSequence);
}
}
另外濒憋,OAuth的密碼模式需要AuthenticationManager支持
@Override
@Bean
publicAuthenticationManagerauthenticationManagerBean()throwsException{
returnsuper.authenticationManagerBean();
}
定義一個(gè)Controller,提供兩個(gè)接口陶夜,/api/member用來獲取當(dāng)前用戶信息凛驮,/api/exit用來注銷當(dāng)前用戶
@RestController
@RequestMapping("/api")
publicclassMemberController{
@Autowired
privateMyUserDetailService userDetailService;
@Autowired
privateConsumerTokenServices consumerTokenServices;
@GetMapping("/member")
publicPrincipaluser(Principal member){
returnmember;
}
@DeleteMapping(value="/exit")
publicResultrevokeToken(String access_token){
Result result=newResult();
if(consumerTokenServices.revokeToken(access_token)){
result.setCode(ResultCode.SUCCESS.getCode());
result.setMessage("注銷成功");
}else{
result.setCode(ResultCode.FAILED.getCode());
result.setMessage("注銷失敗");
}
returnresult;
}
}
會(huì)員服務(wù)配置
引入依賴
<?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>eshop-parent</artifactId>
<groupId>com.curise.eshop</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eshop-member</artifactId>
<packaging>war</packaging>
<description>會(huì)員模塊</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</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>
</plugin>
</plugins>
</build>
</project>
配置資源服務(wù)器
@Configuration
@EnableResourceServer
publicclassResourceServerConfigextendsResourceServerConfigurerAdapter{
@Override
publicvoidconfigure(HttpSecurity http)throwsException{
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint((request,response,authException)->response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
.and()
.requestMatchers().antMatchers("/api/**")
.and()
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.httpBasic();
}
}
配置文件配置
spring:
application:
name:eshop-member
server:
port:1201
eureka:
instance:
prefer-ip-address:true
instance-id:${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone:http://localhost:1111/eureka/
security:
oauth2:
resource:
id:eshop-member
user-info-uri:http://localhost:1202/auth/api/member
prefer-token-info:false
MemberApplication主類配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableGlobalMethodSecurity(prePostEnabled=true)
publicclassMemberApplication{
publicstaticvoidmain(String[]args){
SpringApplication.run(MemberApplication.class,args);
}
}
提供對外接口
@RestController
@RequestMapping("/api")
publicclassMemberController{
@GetMapping("hello")
@PreAuthorize("hasAnyAuthority('hello')")
publicStringhello(){
return"hello";
}
@GetMapping("current")
publicPrincipaluser(Principal principal){
returnprincipal;
}
@GetMapping("query")
@PreAuthorize("hasAnyAuthority('query')")
publicStringquery(){
return"具有query權(quán)限";
}
}
配置網(wǎng)關(guān)
引入依賴
<?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>eshop-parent</artifactId>
<groupId>com.curise.eshop</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>eshop-gateway</artifactId>
<description>網(wǎng)關(guān)</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
server:
port:1202
spring:
application:
name:eshop-gateway
#--------------------eureka---------------------
eureka:
instance:
prefer-ip-address:true
instance-id:${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone:http://localhost:1111/eureka/
#--------------------Zuul-----------------------
zuul:
routes:
member:
path:/member/**
serviceId: eshop-member
sensitiveHeaders: "*"
auth:
path: /auth/**
serviceId: eshop-auth
sensitiveHeaders: "*"
retryable: false
ignored-services: "*"
ribbon:
eager-load:
enabled: true
host:
connect-timeout-millis: 3000
socket-timeout-millis: 3000
add-proxy-headers: true
#---------------------OAuth2---------------------
security:
oauth2:
client:
access-token-uri: http://localhost:${server.port}/auth/oauth/token
user-authorization-uri: http://localhost:${server.port}/auth/oauth/authorize
client-id: web
resource:
user-info-uri:? http://localhost:${server.port}/auth/api/member
prefer-token-info: false
#----------------------超時(shí)配置-------------------
ribbon:
ReadTimeout: 3000
ConnectTimeout: 3000
MaxAutoRetries: 1
MaxAutoRetriesNextServer: 2
eureka:
enabled: true
hystrix:
command:
default:
execution:
timeout:
enabled: true
isolation:
thread:
timeoutInMilliseconds: 3500
ZuulApplication主類
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableOAuth2Sso
publicclassZuulApplication{
publicstaticvoidmain(String[]args){
SpringApplication.run(ZuulApplication.class,args);
}
}
Spring Security配置
@Configuration
@EnableWebSecurity
@Order(99)
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{
@Override
protectedvoidconfigure(HttpSecurity http)throwsException{
http.csrf().disable();
}
}
接下來分別啟動(dòng)eshop-server、eshop-member条辟、eshop-auth黔夭、eshop-gateway。
先發(fā)送一個(gè)請求測試一下未認(rèn)證的效果
獲取認(rèn)證
使用access_token請求auth服務(wù)下的用戶信息接口
使用access_token請求member服務(wù)下的用戶信息接口
請求member服務(wù)的query接口
請求member服務(wù)的hello接口羽嫡,數(shù)據(jù)庫里并沒有給用戶hello權(quán)限
刷新token
注銷
后續(xù)還會(huì)慢慢完善本姥,敬請期待!杭棵!
關(guān)于代碼和數(shù)據(jù)表sql已經(jīng)上傳到GitHub婚惫。地址:
https://github.com/WYA1993/springcloud_oauth2.0。
注意把數(shù)據(jù)庫和redis替換成自己的地址
統(tǒng)一回復(fù)一下魂爪,有很多人反映獲取認(rèn)證時(shí)返回401先舷,如下:
{
"timestamp":"2019-08-13T03:25:27.161+0000",
"status":401,
"error":"Unauthorized",
"message":"Unauthorized",
"path":"/oauth/token"
}
原因是在發(fā)起請求的時(shí)候沒有添加Basic Auth認(rèn)證,如下圖:
滓侍,添加Basic Auth認(rèn)證后會(huì)在headers添加一個(gè)認(rèn)證消息頭
添加Basic Auth認(rèn)證的信息在代碼中有體現(xiàn):
客戶端信息和token信息從MySQL數(shù)據(jù)庫中獲取
現(xiàn)在客戶端信息都是存在內(nèi)存中的蒋川,生產(chǎn)環(huán)境肯定不可以這么做,要支持客戶端的動(dòng)態(tài)添加或刪除撩笆,所以我選擇把客戶端信息存到MySQL中捺球。
首先街图,創(chuàng)建數(shù)據(jù)表,數(shù)據(jù)表的結(jié)構(gòu)官方已經(jīng)給出懒构,地址在
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
222
其次,需要修改一下sql腳本耘擂,把主鍵的長度改為128胆剧,LONGVARBINARY類型改為blob,調(diào)整后的sql腳本:
create table oauth_client_details(
client_idVARCHAR(128)PRIMARY KEY,
resource_idsVARCHAR(256),
client_secretVARCHAR(256),
scopeVARCHAR(256),
authorized_grant_typesVARCHAR(256),
web_server_redirect_uriVARCHAR(256),
authoritiesVARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_informationVARCHAR(4096),
autoapproveVARCHAR(256)
);
create table oauth_client_token(
token_idVARCHAR(256),
token BLOB,
authentication_idVARCHAR(128)PRIMARY KEY,
user_nameVARCHAR(256),
client_idVARCHAR(256)
);
create table oauth_access_token(
token_idVARCHAR(256),
token BLOB,
authentication_idVARCHAR(128)PRIMARY KEY,
user_nameVARCHAR(256),
client_idVARCHAR(256),
authentication BLOB,
refresh_tokenVARCHAR(256)
);
create table oauth_refresh_token(
token_idVARCHAR(256),
token BLOB,
authentication BLOB
);
create table oauth_code(
codeVARCHAR(256),authentication BLOB
);
create table oauth_approvals(
userIdVARCHAR(256),
clientIdVARCHAR(256),
scopeVARCHAR(256),
statusVARCHAR(10),
expiresAt TIMESTAMP,
lastModifiedAt TIMESTAMP
);
--customized oauth_client_details table
create table ClientDetails(
appIdVARCHAR(128)PRIMARY KEY,
resourceIdsVARCHAR(256),
appSecretVARCHAR(256),
scopeVARCHAR(256),
grantTypesVARCHAR(256),
redirectUrlVARCHAR(256),
authoritiesVARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformationVARCHAR(4096),
autoApproveScopesVARCHAR(256)
);
調(diào)整后的sql腳步也放到了GitHub中醉冤,需要的可以自行下載
然后在eshop_member數(shù)據(jù)庫創(chuàng)建數(shù)據(jù)表秩霍,將客戶端信息添加到oauth_client_details表中
如果你的密碼不是明文,記得client_secret需要加密后存儲(chǔ)蚁阳。
然后修改代碼铃绒,配置從數(shù)據(jù)庫讀取客戶端信息
接下來啟動(dòng)服務(wù)測試即可。
獲取授權(quán)
獲取用戶信息
刷新token
打開數(shù)據(jù)表發(fā)現(xiàn)token這些信息并沒有存到表中螺捐,因?yàn)閠okenStore使用的是redis方式颠悬,我們可以替換為從數(shù)據(jù)庫讀取。修改配置
重啟服務(wù)再次測試
查看數(shù)據(jù)表定血,發(fā)現(xiàn)token數(shù)據(jù)已經(jīng)存到表里了赔癌。