Spring Cloud+OAuth2+Spring Security+Redis 實(shí)現(xiàn)微服務(wù)統(tǒng)一認(rèn)證授權(quán)吹榴,附源碼

因?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)存到表里了赔癌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市澜沟,隨后出現(xiàn)的幾起案子灾票,更是在濱河造成了極大的恐慌,老刑警劉巖茫虽,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刊苍,死亡現(xiàn)場離奇詭異,居然都是意外死亡濒析,警方通過查閱死者的電腦和手機(jī)正什,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來悼枢,“玉大人埠忘,你說我怎么就攤上這事÷鳎” “怎么了莹妒?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绰上。 經(jīng)常有香客問我旨怠,道長,這世上最難降的妖魔是什么蜈块? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任鉴腻,我火速辦了婚禮迷扇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘爽哎。我一直安慰自己蜓席,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布课锌。 她就那樣靜靜地躺著厨内,像睡著了一般。 火紅的嫁衣襯著肌膚如雪渺贤。 梳的紋絲不亂的頭發(fā)上雏胃,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音志鞍,去河邊找鬼瞭亮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛固棚,可吹牛的內(nèi)容都是我干的统翩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼此洲,長吁一口氣:“原來是場噩夢啊……” “哼唆缴!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起黍翎,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤面徽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后匣掸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體趟紊,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年碰酝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了霎匈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡送爸,死狀恐怖铛嘱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情袭厂,我是刑警寧澤墨吓,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站纹磺,受9級特大地震影響帖烘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜橄杨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一秘症、第九天 我趴在偏房一處隱蔽的房頂上張望照卦。 院中可真熱鬧,春花似錦乡摹、人聲如沸役耕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蹄葱。三九已至,卻和暖如春锄列,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惯悠。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工邻邮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人克婶。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓筒严,卻偏偏與公主長得像,于是被迫代替她去往敵國和親情萤。 傳聞我的和親對象是個(gè)殘疾皇子鸭蛙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容