开发环境:
  • 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 —— 通用类

授权服务

首先构建eshop-auth服务,引入相关依赖
<?
xml version=
"1.0"
 encoding=
"UTF-8"?>
<
projectxmlns
=
"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>
授权模块
</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
#最小连接池数量
    maxActive: 20
#最大连接池数量
    maxWait: 60000
#获取连接时最大等待时间,单位毫秒
    timeBetweenEvictionRunsMillis: 60000
#配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
    minEvictableIdleTimeMillis: 300000
#配置一个连接在池中最小生存的时间,单位是毫秒
    validationQuery: SELECT 1 from 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
private
 AuthenticationManager authenticationManager;


@Autowired
private
 DataSource dataSource;


@Autowired
private
 RedisConnectionFactory redisConnectionFactory;


@Autowired
private
 MyUserDetailService userDetailService;


@Bean
public TokenStore tokenStore()
{

returnnew
 RedisTokenStore(redisConnectionFactory);

    }


@Override
publicvoidconfigure(AuthorizationServerSecurityConfigurer security)throws Exception
{

        security

                .allowFormAuthenticationForClients()

                .tokenKeyAccess(
"permitAll()"
)

                .checkTokenAccess(
"isAuthenticated()"
);

    }


@Override
publicvoidconfigure(ClientDetailsServiceConfigurer clients)throws Exception
{

// 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
public ClientDetailsService clientDetails()
{

returnnew
 JdbcClientDetailsService(dataSource);

    }


@Bean
public WebResponseExceptionTranslator webResponseExceptionTranslator()
{

returnnew
 MssWebResponseExceptionTranslator();

    }


@Override
publicvoidconfigure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception
{

        endpoints.tokenStore(tokenStore())

                .userDetailsService(userDetailService)

                .authenticationManager(authenticationManager);

        endpoints.tokenServices(defaultTokenServices());

//认证异常翻译
// endpoints.exceptionTranslator(webResponseExceptionTranslator());
    }


/**

     * <p>注意,自定义TokenServices的时候,需要设置
@Primary
,否则报错,</p>

     *
@return
     */

@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;

    }

}
在上述配置中,认证的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
 {


privatestatic
 final String ACCESS =
"access:"
;

privatestatic
 final String AUTH_TO_ACCESS =
"auth_to_access:"
;

privatestatic
 final String AUTH =
"auth:"
;

privatestatic
 final String REFRESH_AUTH =
"refresh_auth:"
;

privatestatic
 final String ACCESS_TO_REFRESH =
"access_to_refresh:"
;

privatestatic
 final String REFRESH =
"refresh:"
;

privatestatic
 final String REFRESH_TO_ACCESS =
"refresh_to_access:"
;

privatestatic
 final String CLIENT_ID_TO_ACCESS =
"client_id_to_access:"
;

privatestatic
 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 =
""
;


publicRedisTokenStore(RedisConnectionFactory connectionFactory)
{

this
.connectionFactory = connectionFactory;

    }


publicvoidsetAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator)
{

this
.authenticationKeyGenerator = authenticationKeyGenerator;

    }


publicvoidsetSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy)
{

this
.serializationStrategy = serializationStrategy;

    }


publicvoidsetPrefix(String prefix)
{

this
.prefix = prefix;

    }


private RedisConnection getConnection()
{

returnthis
.connectionFactory.getConnection();

    }


privatebyte[] serialize(Object object)
{

returnthis
.serializationStrategy.serialize(
object
);

    }


privatebyte[] serializeKey(String object)
{

returnthis
.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);

    }


privatebyte[] serialize(String string)
{

returnthis
.serializationStrategy.serialize(
string
);

    }


private String deserializeString(byte[] bytes)
{

returnthis
.serializationStrategy.deserializeString(bytes);

    }


    @
Override

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;

    }


    @
Override

public
 OAuth2Authentication
readAuthentication
(
OAuth2AccessToken token
)
{

returnthis
.readAuthentication(token.getValue());

    }


    @
Override

public
 OAuth2Authentication
readAuthentication
(
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);

return
 auth;

    }


    @
Override

public
 OAuth2Authentication
readAuthenticationForRefreshToken
(
OAuth2RefreshToken token
)
{

returnthis
.readAuthenticationForRefreshToken(token.getValue());

    }


public OAuth2Authentication readAuthenticationForRefreshToken(String token)
{

        RedisConnection conn = getConnection();

try
 {

byte
[] bytes = conn.
get
(serializeKey(REFRESH_AUTH + token));

            OAuth2Authentication auth = deserializeAuthentication(bytes);

return
 auth;

        }
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
) {

int
 seconds = 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
 (refreshToken instanceof ExpiringOAuth2RefreshToken) {

                    ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;

                    Date expiration = expiringRefreshToken.getExpiration();

if
 (expiration !=
null
) {

int
 seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) /
1000
L)

                                .intValue();

                        conn.expire(refreshToAccessKey, seconds);

                        conn.expire(accessToRefreshKey, seconds);

                    }

                }

            }

            conn.closePipeline();

        }
finally
 {

            conn.close();

        }

    }


privatestatic String getApprovalKey(OAuth2Authentication authentication)
{

        String userName = authentication.getUserAuthentication() ==
null
 ?
""
: authentication.getUserAuthentication().getName();

return
 getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);

    }


privatestatic String getApprovalKey(String clientId, String userName)
{

return
 clientId + (userName ==
null
 ?
""
 :
":"
 + userName);

    }


    @
Override

publicvoidremoveAccessToken
(
OAuth2AccessToken accessToken
)
{

this
.removeAccessToken(accessToken.getValue());

    }


    @
Override

public
 OAuth2AccessToken
readAccessToken
(
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);

return
 accessToken;

    }


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
 (refreshToken instanceof ExpiringOAuth2RefreshToken) {

                ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;

                Date expiration = expiringRefreshToken.getExpiration();

if
 (expiration !=
null
) {

int
 seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) /
1000
L)

                            .intValue();

                    conn.expire(refreshKey, seconds);

                    conn.expire(refreshAuthKey, seconds);

                }

            }

            conn.closePipeline();

        }
finally
 {

            conn.close();

        }

    }


    @
Override

public
 OAuth2RefreshToken
readRefreshToken
(
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);

return
 refreshToken;

    }


    @
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

public
 Collection<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
) {

return
 Collections.<OAuth2AccessToken> emptySet();

        }

        List<OAuth2AccessToken> accessTokens =
new
 ArrayList<OAuth2AccessToken>(byteList.size());

for
 (
byte
[] bytes : byteList) {

            OAuth2AccessToken accessToken = deserializeAccessToken(bytes);

            accessTokens.
add
(accessToken);

        }

return
 Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);

    }


    @
Override

public
 Collection<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
) {

return
 Collections.<OAuth2AccessToken> emptySet();

        }

        List<OAuth2AccessToken> accessTokens =
new
 ArrayList<OAuth2AccessToken>(byteList.size());

for
 (
byte
[] bytes : byteList) {

            OAuth2AccessToken accessToken = deserializeAccessToken(bytes);

            accessTokens.
add
(accessToken);

        }

return
 Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);

    }

}
配置资源服务器
@Configuration
@EnableResourceServer
@Order
(
3
)

public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Override
    public void configure(HttpSecurity http) throws Exception {

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
)

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
    private MyUserDetailService userDetailService;


@Bean
    public PasswordEncoder passwordEncoder() {

//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 的优先级低的。
二者的关系:
  • ResourceServerConfig 用于保护oauth相关的endpoints,同时主要作用于用户的登录(form login,Basic auth)
  • SecurityConfig 用于保护oauth要开放的资源,同时主要作用于client端以及token的认证(Bearer auth)
所以我们让SecurityConfig优先于ResourceServerConfig,且在SecurityConfig 不拦截oauth要开放的资源,在ResourceServerConfig 中配置需要token验证的资源,也就是我们对外提供的接口。所以这里对于所有微服务的接口定义有一个要求,就是全部以/api开头。
如果这里不这样配置的话,在你拿到access_token去请求各个接口时会报 invalid_token的提示。
另外,由于我们自定义认证逻辑,所以需要重写UserDetailService
@Service
(
"userDetailService"
)

publicclassMyUserDetailServiceimplementsUserDetailsService
{


@Autowired
private
 MemberDao memberDao;


@Override
public UserDetails loadUserByUsername(String memberName)throws UsernameNotFoundException
{

        Member member = memberDao.findByMemberName(memberName);

if
 (member ==
null
) {

thrownew
 UsernameNotFoundException(memberName);

        }

        Set<GrantedAuthority> grantedAuthorities =
new
 HashSet<>();

// 可用性 :true:可用 false:不可用
boolean
 enabled =
true
;

// 过期性 :true:没过期 false:过期
boolean
 accountNonExpired =
true
;

// 有效性 :true:凭证有效 false:凭证无效
boolean
 credentialsNonExpired =
true
;

// 锁定性 :true:未锁定 false:已锁定
boolean
 accountNonLocked =
true
;

for
 (Role role : member.getRoles()) {

//角色必须是ROLE_开头,可以在数据库中设置
            GrantedAuthority grantedAuthority =
new
 SimpleGrantedAuthority(role.getRoleName());

            grantedAuthorities.add(grantedAuthority);

//获取权限
for
 (Permission permission : role.getPermissions()) {

                GrantedAuthority authority =
new
 SimpleGrantedAuthority(permission.getUri());

                grantedAuthorities.add(authority);

            }

        }

        User user =
new
 User(member.getMemberName(), member.getPassword(),

                enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);

return
 user;

    }


}
密码验证为了方便我使用了不加密的方式,重写了PasswordEncoder,实际开发还是建议使用BCryptPasswordEncoder。
publicclass
 NoEncryptPasswordEncoder
implements
 PasswordEncoder {


@Override
publicString
 encode(CharSequence charSequence) {

return
 (
String
) charSequence;

    }


@Override
publicboolean
 matches(CharSequence charSequence,
String
 s) {

return
 s.equals((
String
) charSequence);

    }

}
另外,OAuth的密码模式需要AuthenticationManager支持
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {

returnsuper.authenticationManagerBean
();

}
定义一个Controller,提供两个接口,/api/member用来获取当前用户信息,/api/exit用来注销当前用户
@RestController
@RequestMapping
(
"/api"
)

publicclassMemberController
{


@Autowired
private
 MyUserDetailService userDetailService;


@Autowired
private
 ConsumerTokenServices consumerTokenServices;


@GetMapping
(
"/member"
)

public Principal user(Principal member)
{

return
 member;

    }


@DeleteMapping
(value =
"/exit"
)

public Result revokeToken(String access_token)
{

        Result result =
new
 Result();

if
 (consumerTokenServices.revokeToken(access_token)) {

            result.setCode(ResultCode.SUCCESS.getCode());

            result.setMessage(
"注销成功"
);

        }
else
 {

            result.setCode(ResultCode.FAILED.getCode());

            result.setMessage(
"注销失败"
);

        }

return
 result;

    }

}

会员服务配置

引入依赖
<?
xml version=
"1.0"
 encoding=
"UTF-8"?>
<
projectxmlns
=
"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>
会员模块
</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>
配置资源服务器
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {


@Override
    public void configure(HttpSecurity http) throws Exception {

http
.csrf
()
.disable
()

.exceptionHandling
()

.authenticationEntryPoint
((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))

.and
()

.requestMatchers
()
.antMatchers
(
"/api/**"
)

.and
()

.authorizeRequests
()

.antMatchers
(
"/api/**"
)
.authenticated
()

.and
()

.httpBasic
();

    }

}
配置文件配置
sprin
g:
  application:

    name: eshop-member


server:

  por
t:1201

eurek
a:
  instance:

    prefer-ip-addres
s:
 true

    instance-id: ${spring.cloud.client.ip-address}:${server.port}

  clien
t:
    service-ur
l:
      defaultZone: http://localhos
t:1111
/eureka/


security:

  oauth2:

    resource:

      id: eshop-member

      user-info-uri: http://localhos
t:1202
/auth/api/member

      prefer-token-info: false
MemberApplication主类配置
@SpringBootApplication
@EnableDiscoveryClient
@EnableGlobalMethodSecurity
(prePostEnabled = true)

public class MemberApplication {


publicstaticvoidmain
(String[] args) {

SpringApplication.run
(MemberApplication.class,args);

    }

}
提供对外接口
@RestController
@RequestMapping
(
"/api"
)

publicclassMemberController
{


@GetMapping
(
"hello"
)

@PreAuthorize
(
"hasAnyAuthority('hello')"
)

public String hello()
{

return"hello"
;

    }


@GetMapping
(
"current"
)

public Principal user(Principal principal)
{

return
 principal;

    }


@GetMapping
(
"query"
)

@PreAuthorize
(
"hasAnyAuthority('query')"
)

public String query()
{

return"具有query权限"
;

    }

}

配置网关

引入依赖
<?
xml version=
"1.0"
 encoding=
"UTF-8"?>
<
projectxmlns
=
"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>
网关
</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:

  por
t:1202

sprin
g:
  application:

    name: eshop-gateway


#--------------------eureka---------------------

eurek
a:
  instance:

    prefer-ip-addres
s:
 true

    instance-id: ${spring.cloud.client.ip-address}:${server.port}

  clien
t:
    service-ur
l:
      defaultZone: http://localhos
t:1111
/eureka/


#--------------------Zuul-----------------------

zuu
l:
  route
s:
    member:

      path: /member/**

      serviceId: eshop-member

      sensitiveHeader
s:"*"
    auth:

      path: /auth/**

      serviceId: eshop-auth

      sensitiveHeader
s:"*"
  retryable: false

  ignored-service
s:"*"
  ribbon:

    eager-load:

      enabled: true

  hos
t:
    connect-timeout-milli
s:3000
    socket-timeout-milli
s:3000
add
-proxy-header
s:
 true

#---------------------OAuth2---------------------

security:

  oauth2:

    clien
t:
      access-token-uri: http://localhos
t:
${server.port}/auth/oauth/token

      user-authorization-uri: http://localhos
t:
${server.port}/auth/oauth/authorize

      client-id: web

    resource:

      user-info-uri: http://localhos
t:
${server.port}/auth/api/member

      prefer-token-info: false

#----------------------超时配置-------------------

ribbon:

  ReadTimeou
t:3000
  ConnectTimeou
t:3000
  MaxAutoRetrie
s:1
  MaxAutoRetriesNextServer:
2
  eurek
a:
    enabled: true

hystrix:

command
:

    defaul
t:
      execution:

        timeou
t:
          enabled: true

        isolation:

          thread:

            timeoutInMillisecond
s:3500
ZuulApplication主类
@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
@EnableOAuth2Sso
public class ZuulApplication {

publicstaticvoidmain
(String[] args) {

SpringApplication.run
(ZuulApplication.class, args);

    }

}
Spring Security配置
@Configuration
@EnableWebSecurity
@Order
(
99
)

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
    protected void configure(HttpSecurity http) throws Exception {

http.csrf
()
.disable
();

    }

}
接下来分别启动eshop-server、eshop-member、eshop-auth、eshop-gateway。
先发送一个请求测试一下未认证的效果
获取认证
使用access_token请求auth服务下的用户信息接口
使用access_token请求member服务下的用户信息接口
请求member服务的query接口
请求member服务的hello接口,数据库里并没有给用户hello权限
刷新token
注销
后续还会慢慢完善,敬请期待!
关于代码和数据表sql已经上传到GitHub。地址:https://github.com/WYA1993/springcloud_oauth2.0。
注意把数据库和redis替换成自己的地址

获取认证时返回401,如下:

{

"timestamp"
:
"2019-08-13T03:25:27.161+0000"
,

"status"
:
401
,

"error"
:
"Unauthorized"
,

"message"
:
"Unauthorized"
,

"path"
:
"/oauth/token"
}
原因是在发起请求的时候没有添加Basic Auth认证,如下图:
,添加Basic Auth认证后会在headers添加一个认证消息头
添加Basic Auth认证的信息在代码中有体现:

客户端信息和token信息从MySQL数据库中获取

现在客户端信息都是存在内存中的,生产环境肯定不可以这么做,要支持客户端的动态添加或删除,所以我选择把客户端信息存到MySQL中。
首先,创建数据表,数据表的结构官方已经给出,地址在
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
其次,需要修改一下sql脚本,把主键的长度改为128,LONGVARBINARY类型改为blob,调整后的sql脚本:
createtable
 oauth_client_details (

  client_id
VARCHAR
(
128
) PRIMARY
KEY
,

  resource_ids
VARCHAR
(
256
),

  client_secret
VARCHAR
(
256
),

scopeVARCHAR
(
256
),

  authorized_grant_types
VARCHAR
(
256
),

  web_server_redirect_uri
VARCHAR
(
256
),

  authorities
VARCHAR
(
256
),

  access_token_validity
INTEGER
,

  refresh_token_validity
INTEGER
,

  additional_information
VARCHAR
(
4096
),

  autoapprove
VARCHAR
(
256
)

);


createtable
 oauth_client_token (

  token_id
VARCHAR
(
256
),

  token
BLOB
,

  authentication_id
VARCHAR
(
128
) PRIMARY
KEY
,

  user_name
VARCHAR
(
256
),

  client_id
VARCHAR
(
256
)

);


createtable
 oauth_access_token (

  token_id
VARCHAR
(
256
),

  token
BLOB
,

  authentication_id
VARCHAR
(
128
) PRIMARY
KEY
,

  user_name
VARCHAR
(
256
),

  client_id
VARCHAR
(
256
),

authenticationBLOB
,

  refresh_token
VARCHAR
(
256
)

);


createtable
 oauth_refresh_token (

  token_id
VARCHAR
(
256
),

  token
BLOB
,

authenticationBLOB
);


createtable
 oauth_code (

  code
VARCHAR
(
256
),
authenticationBLOB
);


createtable
 oauth_approvals (

 userId
VARCHAR
(
256
),

 clientId
VARCHAR
(
256
),

scopeVARCHAR
(
256
),

statusVARCHAR
(
10
),

 expiresAt
TIMESTAMP
,

 lastModifiedAt
TIMESTAMP
);



-- customized oauth_client_details table
createtable
 ClientDetails (

  appId
VARCHAR
(
128
) PRIMARY
KEY
,

  resourceIds
VARCHAR
(
256
),

  appSecret
VARCHAR
(
256
),

scopeVARCHAR
(
256
),

  grantTypes
VARCHAR
(
256
),

  redirectUrl
VARCHAR
(
256
),

  authorities
VARCHAR
(
256
),

  access_token_validity
INTEGER
,

  refresh_token_validity
INTEGER
,

  additionalInformation
VARCHAR
(
4096
),

  autoApproveScopes
VARCHAR
(
256
)

);
调整后的sql脚步也放到了GitHub中,需要的可以自行下载
然后在eshop_member数据库创建数据表,将客户端信息添加到oauth_client_details表中
如果你的密码不是明文,记得client_secret需要加密后存储。
然后修改代码,配置从数据库读取客户端信息
接下来启动服务测试即可。
获取授权
获取用户信息

刷新token

打开数据表发现token这些信息并没有存到表中,因为tokenStore使用的是redis方式,我们可以替换为从数据库读取。修改配置
重启服务再次测试
查看数据表,发现token数据已经存到表里了。

关于代码和数据表sql已经上传到 GitHub。 
地址:https://github.com/WYA1993/springcloud_oauth2.0。
来自:CSDN,作者:myCat
链接:https://blog.csdn.net/WYA1993/article/details/85050120
 关注公众号:Java后端编程,回复下面关键字 
要Java学习完整路线,回复  路线 
缺Java入门视频,回复 视频 
要Java面试经验,回复  面试 
缺Java项目,回复: 项目 
进Java粉丝群: 加群 
PS:如果觉得我的分享不错,欢迎大家随手点赞、在看。
(完)
加我"微信获取一份 最新Java面试题资料
请备注:666不然不通过~
最近好文
最近面试BAT,整理一份面试资料Java面试BAT通关手册,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:关注公众号并回复 java 领取,更多内容陆续奉上。
明天见(。・ω・。)ノ♡
继续阅读
阅读原文