当前位置:   article > 正文

SpringCloud+SpringBoot+OAuth2+Spring Security+Redis实现的微服务统一认证授权

security+oauth2+redis+springcloud

因为目前做了一个基于 Spring Cloud 的微服务项目,所以了解到了 OAuth2,打算整合一下 OAuth2 来实现统一授权。关于 OAuth 是一个关于授权的开放网络标准,目前的版本是 2.0,这里我就不多做介绍了。

开发环境:Windows10,  Intellij Idea2018.2,   jdk1.8,  redis3.2.9, Spring Boot 2.0.2 Release, Spring Cloud Finchley.RC2 Spring 5.0.6

项目目录

55317c4424c5176eb2064c6aa826a852.png

eshop                                      —— 父级工程,管理 jar 包版本

eshop-server                 —— Eureka 服务注册中心

eshop-gateway              —— Zuul 网关

eshop-auth                    —— 授权服务

eshop-member              —— 会员服务

eshop-email                   —— 邮件服务(暂未使用)

eshop-common              —— 通用类

关于如何构建一个基本的 Spring Cloud 微服务这里就不赘述了,不会的可以看一下我的关于 Spring Cloud 系列的博客。这里给个入口地址:https://blog.csdn.net/wya1993/article/category/7701476

授权服务

首先构建 eshop-auth 服务,引入相关依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>eshop-parent</artifactId>
  7. <groupId>com.curise.eshop</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>eshop-auth</artifactId>
  12. <packaging>war</packaging>
  13. <description>授权模块</description>
  14. <dependencies>
  15. <dependency>
  16. <groupId>com.curise.eshop</groupId>
  17. <artifactId>eshop-common</artifactId>
  18. <version>1.0-SNAPSHOT</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.springframework.boot</groupId>
  22. <artifactId>spring-boot-starter-web</artifactId>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.springframework.cloud</groupId>
  26. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.springframework.cloud</groupId>
  30. <artifactId>spring-cloud-starter-oauth2</artifactId>
  31. </dependency>
  32. <dependency>
  33. <groupId>org.springframework.cloud</groupId>
  34. <artifactId>spring-cloud-starter-security</artifactId>
  35. </dependency>
  36. <dependency>
  37. <groupId>org.springframework.boot</groupId>
  38. <artifactId>spring-boot-starter-data-redis</artifactId>
  39. </dependency>
  40. <dependency>
  41. <groupId>org.mybatis.spring.boot</groupId>
  42. <artifactId>mybatis-spring-boot-starter</artifactId>
  43. </dependency>
  44. <dependency>
  45. <groupId>org.springframework.boot</groupId>
  46. <artifactId>spring-boot-starter-actuator</artifactId>
  47. </dependency>
  48. <dependency>
  49. <groupId>mysql</groupId>
  50. <artifactId>mysql-connector-java</artifactId>
  51. </dependency>
  52. <dependency>
  53. <groupId>com.alibaba</groupId>
  54. <artifactId>druid</artifactId>
  55. </dependency>
  56. <dependency>
  57. <groupId>log4j</groupId>
  58. <artifactId>log4j</artifactId>
  59. </dependency>
  60. </dependencies>
  61. <build>
  62. <plugins>
  63. <plugin>
  64. <groupId>org.springframework.boot</groupId>
  65. <artifactId>spring-boot-maven-plugin</artifactId>
  66. </plugin>
  67. </plugins>
  68. </build>
  69. </project>

接下来,配置 Mybatis、redis、eureka,贴一下配置文件

  1. server:
  2. port: 1203
  3. spring:
  4. application:
  5. name: eshop-auth
  6. redis:
  7. database: 0
  8. host: 192.168.0.117
  9. port: 6379
  10. password:
  11. jedis:
  12. pool:
  13. max-active: 8
  14. max-idle: 8
  15. min-idle: 0
  16. datasource:
  17. driver-class-name: com.mysql.jdbc.Driver
  18. url: jdbc:mysql://localhost:3306/eshop_member?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
  19. username: root
  20. password: root
  21. druid:
  22. initialSize: 5 #初始化连接大小
  23. minIdle: 5 #最小连接池数量
  24. maxActive: 20 #最大连接池数量
  25. maxWait: 60000 #获取连接时最大等待时间,单位毫秒
  26. timeBetweenEvictionRunsMillis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
  27. minEvictableIdleTimeMillis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
  28. validationQuery: SELECT 1 from DUAL #测试连接
  29. testWhileIdle: true #申请连接的时候检测,建议配置为true,不影响性能,并且保证安全性
  30. testOnBorrow: false #获取连接时执行检测,建议关闭,影响性能
  31. testOnReturn: false #归还连接时执行检测,建议关闭,影响性能
  32. poolPreparedStatements: false #是否开启PSCache,PSCache对支持游标的数据库性能提升巨大,oracle建议开启,mysql下建议关闭
  33. maxPoolPreparedStatementPerConnectionSize: 20 #开启poolPreparedStatements后生效
  34. filters: stat,wall,log4j #配置扩展插件,常用的插件有=>stat:监控统计 log4j:日志 wall:防御sql注入
  35. connectionProperties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' #通过connectProperties属性来打开mergeSql功能;慢SQL记录
  36. eureka:
  37. instance:
  38. prefer-ip-address: true
  39. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  40. client:
  41. service-url:
  42. defaultZone: http://localhost:1111/eureka/
  43. mybatis:
  44. type-aliases-package: com.curise.eshop.common.entity
  45. configuration:
  46. map-underscore-to-camel-case: true #开启驼峰命名,l_name -> lName
  47. jdbc-type-for-null: NULL
  48. lazy-loading-enabled: true
  49. aggressive-lazy-loading: true
  50. cache-enabled: true #开启二级缓存
  51. call-setters-on-nulls: true #map空列不显示问题
  52. mapper-locations:
  53. - classpath:mybatis/*.xml

AuthApplication 添加 @EnableDiscoveryClient 和 @MapperScan 注解。

搜索公纵号:MarkerHub,关注回复[ vue ]获取前后端入门教程!

接下来配置认证服务器 AuthorizationServerConfig ,并添加 @Configuration 和 @EnableAuthorizationServer 注解,其中 ClientDetailsServiceConfigurer 配置在内存中,当然也可以从数据库读取,以后慢慢完善。

  1. @Configuration
  2. @EnableAuthorizationServer
  3. public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  4. @Autowired
  5. private AuthenticationManager authenticationManager;
  6. @Autowired
  7. private DataSource dataSource;
  8. @Autowired
  9. private RedisConnectionFactory redisConnectionFactory;
  10. @Autowired
  11. private MyUserDetailService userDetailService;
  12. @Bean
  13. public TokenStore tokenStore() {
  14. return new RedisTokenStore(redisConnectionFactory);
  15. }
  16. @Override
  17. public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
  18. security
  19. .allowFormAuthenticationForClients()
  20. .tokenKeyAccess("permitAll()")
  21. .checkTokenAccess("isAuthenticated()");
  22. }
  23. @Override
  24. public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
  25. // clients.withClientDetails(clientDetails());
  26. clients.inMemory()
  27. .withClient("android")
  28. .scopes("read")
  29. .secret("android")
  30. .authorizedGrantTypes("password", "authorization_code", "refresh_token")
  31. .and()
  32. .withClient("webapp")
  33. .scopes("read")
  34. .authorizedGrantTypes("implicit")
  35. .and()
  36. .withClient("browser")
  37. .authorizedGrantTypes("refresh_token", "password")
  38. .scopes("read");
  39. }
  40. @Bean
  41. public ClientDetailsService clientDetails() {
  42. return new JdbcClientDetailsService(dataSource);
  43. }
  44. @Bean
  45. public WebResponseExceptionTranslator webResponseExceptionTranslator(){
  46. return new MssWebResponseExceptionTranslator();
  47. }
  48. @Override
  49. public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
  50. endpoints.tokenStore(tokenStore())
  51. .userDetailsService(userDetailService)
  52. .authenticationManager(authenticationManager);
  53. endpoints.tokenServices(defaultTokenServices());
  54. //认证异常翻译
  55. // endpoints.exceptionTranslator(webResponseExceptionTranslator());
  56. }
  57. /**
  58. * <p>注意,自定义TokenServices的时候,需要设置@Primary,否则报错,</p>
  59. * @return
  60. */
  61. @Primary
  62. @Bean
  63. public DefaultTokenServices defaultTokenServices(){
  64. DefaultTokenServices tokenServices = new DefaultTokenServices();
  65. tokenServices.setTokenStore(tokenStore());
  66. tokenServices.setSupportRefreshToken(true);
  67. //tokenServices.setClientDetailsService(clientDetails());
  68. // token有效期自定义设置,默认12小时
  69. tokenServices.setAccessTokenValiditySeconds(60*60*12);
  70. // refresh_token默认30天
  71. tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);
  72. return tokenServices;
  73. }
  74. }

在上述配置中,认证的 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(…),测试后方法可行。

  1. public class RedisTokenStore implements TokenStore {
  2. private static final String ACCESS = "access:";
  3. private static final String AUTH_TO_ACCESS = "auth_to_access:";
  4. private static final String AUTH = "auth:";
  5. private static final String REFRESH_AUTH = "refresh_auth:";
  6. private static final String ACCESS_TO_REFRESH = "access_to_refresh:";
  7. private static final String REFRESH = "refresh:";
  8. private static final String REFRESH_TO_ACCESS = "refresh_to_access:";
  9. private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:";
  10. private static final String UNAME_TO_ACCESS = "uname_to_access:";
  11. private final RedisConnectionFactory connectionFactory;
  12. private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator();
  13. private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy();
  14. private String prefix = "";
  15. public RedisTokenStore(RedisConnectionFactory connectionFactory) {
  16. this.connectionFactory = connectionFactory;
  17. }
  18. public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) {
  19. this.authenticationKeyGenerator = authenticationKeyGenerator;
  20. }
  21. public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) {
  22. this.serializationStrategy = serializationStrategy;
  23. }
  24. public void setPrefix(String prefix) {
  25. this.prefix = prefix;
  26. }
  27. private RedisConnection getConnection() {
  28. return this.connectionFactory.getConnection();
  29. }
  30. private byte[] serialize(Object object) {
  31. return this.serializationStrategy.serialize(object);
  32. }
  33. private byte[] serializeKey(String object) {
  34. return this.serialize(this.prefix + object);
  35. }
  36. private OAuth2AccessToken deserializeAccessToken(byte[] bytes) {
  37. return (OAuth2AccessToken)this.serializationStrategy.deserialize(bytes, OAuth2AccessToken.class);
  38. }
  39. private OAuth2Authentication deserializeAuthentication(byte[] bytes) {
  40. return (OAuth2Authentication)this.serializationStrategy.deserialize(bytes, OAuth2Authentication.class);
  41. }
  42. private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) {
  43. return (OAuth2RefreshToken)this.serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class);
  44. }
  45. private byte[] serialize(String string) {
  46. return this.serializationStrategy.serialize(string);
  47. }
  48. private String deserializeString(byte[] bytes) {
  49. return this.serializationStrategy.deserializeString(bytes);
  50. }
  51. @Override
  52. public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
  53. String key = this.authenticationKeyGenerator.extractKey(authentication);
  54. byte[] serializedKey = this.serializeKey(AUTH_TO_ACCESS + key);
  55. byte[] bytes = null;
  56. RedisConnection conn = this.getConnection();
  57. try {
  58. bytes = conn.get(serializedKey);
  59. } finally {
  60. conn.close();
  61. }
  62. OAuth2AccessToken accessToken = this.deserializeAccessToken(bytes);
  63. if (accessToken != null) {
  64. OAuth2Authentication storedAuthentication = this.readAuthentication(accessToken.getValue());
  65. if (storedAuthentication == null || !key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))) {
  66. this.storeAccessToken(accessToken, authentication);
  67. }
  68. }
  69. return accessToken;
  70. }
  71. @Override
  72. public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
  73. return this.readAuthentication(token.getValue());
  74. }
  75. @Override
  76. public OAuth2Authentication readAuthentication(String token) {
  77. byte[] bytes = null;
  78. RedisConnection conn = this.getConnection();
  79. try {
  80. bytes = conn.get(this.serializeKey("auth:" + token));
  81. } finally {
  82. conn.close();
  83. }
  84. OAuth2Authentication auth = this.deserializeAuthentication(bytes);
  85. return auth;
  86. }
  87. @Override
  88. public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) {
  89. return this.readAuthenticationForRefreshToken(token.getValue());
  90. }
  91. public OAuth2Authentication readAuthenticationForRefreshToken(String token) {
  92. RedisConnection conn = getConnection();
  93. try {
  94. byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token));
  95. OAuth2Authentication auth = deserializeAuthentication(bytes);
  96. return auth;
  97. } finally {
  98. conn.close();
  99. }
  100. }
  101. @Override
  102. public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
  103. byte[] serializedAccessToken = serialize(token);
  104. byte[] serializedAuth = serialize(authentication);
  105. byte[] accessKey = serializeKey(ACCESS + token.getValue());
  106. byte[] authKey = serializeKey(AUTH + token.getValue());
  107. byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication));
  108. byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
  109. byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
  110. RedisConnection conn = getConnection();
  111. try {
  112. conn.openPipeline();
  113. conn.stringCommands().set(accessKey, serializedAccessToken);
  114. conn.stringCommands().set(authKey, serializedAuth);
  115. conn.stringCommands().set(authToAccessKey, serializedAccessToken);
  116. if (!authentication.isClientOnly()) {
  117. conn.rPush(approvalKey, serializedAccessToken);
  118. }
  119. conn.rPush(clientId, serializedAccessToken);
  120. if (token.getExpiration() != null) {
  121. int seconds = token.getExpiresIn();
  122. conn.expire(accessKey, seconds);
  123. conn.expire(authKey, seconds);
  124. conn.expire(authToAccessKey, seconds);
  125. conn.expire(clientId, seconds);
  126. conn.expire(approvalKey, seconds);
  127. }
  128. OAuth2RefreshToken refreshToken = token.getRefreshToken();
  129. if (refreshToken != null && refreshToken.getValue() != null) {
  130. byte[] refresh = serialize(token.getRefreshToken().getValue());
  131. byte[] auth = serialize(token.getValue());
  132. byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue());
  133. conn.stringCommands().set(refreshToAccessKey, auth);
  134. byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue());
  135. conn.stringCommands().set(accessToRefreshKey, refresh);
  136. if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
  137. ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
  138. Date expiration = expiringRefreshToken.getExpiration();
  139. if (expiration != null) {
  140. int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
  141. .intValue();
  142. conn.expire(refreshToAccessKey, seconds);
  143. conn.expire(accessToRefreshKey, seconds);
  144. }
  145. }
  146. }
  147. conn.closePipeline();
  148. } finally {
  149. conn.close();
  150. }
  151. }
  152. private static String getApprovalKey(OAuth2Authentication authentication) {
  153. String userName = authentication.getUserAuthentication() == null ? "": authentication.getUserAuthentication().getName();
  154. return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName);
  155. }
  156. private static String getApprovalKey(String clientId, String userName) {
  157. return clientId + (userName == null ? "" : ":" + userName);
  158. }
  159. @Override
  160. public void removeAccessToken(OAuth2AccessToken accessToken) {
  161. this.removeAccessToken(accessToken.getValue());
  162. }
  163. @Override
  164. public OAuth2AccessToken readAccessToken(String tokenValue) {
  165. byte[] key = serializeKey(ACCESS + tokenValue);
  166. byte[] bytes = null;
  167. RedisConnection conn = getConnection();
  168. try {
  169. bytes = conn.get(key);
  170. } finally {
  171. conn.close();
  172. }
  173. OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
  174. return accessToken;
  175. }
  176. public void removeAccessToken(String tokenValue) {
  177. byte[] accessKey = serializeKey(ACCESS + tokenValue);
  178. byte[] authKey = serializeKey(AUTH + tokenValue);
  179. byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
  180. RedisConnection conn = getConnection();
  181. try {
  182. conn.openPipeline();
  183. conn.get(accessKey);
  184. conn.get(authKey);
  185. conn.del(accessKey);
  186. conn.del(accessToRefreshKey);
  187. // Don't remove the refresh token - it's up to the caller to do that
  188. conn.del(authKey);
  189. List<Object> results = conn.closePipeline();
  190. byte[] access = (byte[]) results.get(0);
  191. byte[] auth = (byte[]) results.get(1);
  192. OAuth2Authentication authentication = deserializeAuthentication(auth);
  193. if (authentication != null) {
  194. String key = authenticationKeyGenerator.extractKey(authentication);
  195. byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key);
  196. byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication));
  197. byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId());
  198. conn.openPipeline();
  199. conn.del(authToAccessKey);
  200. conn.lRem(unameKey, 1, access);
  201. conn.lRem(clientId, 1, access);
  202. conn.del(serialize(ACCESS + key));
  203. conn.closePipeline();
  204. }
  205. } finally {
  206. conn.close();
  207. }
  208. }
  209. @Override
  210. public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) {
  211. byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue());
  212. byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue());
  213. byte[] serializedRefreshToken = serialize(refreshToken);
  214. RedisConnection conn = getConnection();
  215. try {
  216. conn.openPipeline();
  217. conn.stringCommands().set(refreshKey, serializedRefreshToken);
  218. conn.stringCommands().set(refreshAuthKey, serialize(authentication));
  219. if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
  220. ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken;
  221. Date expiration = expiringRefreshToken.getExpiration();
  222. if (expiration != null) {
  223. int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L)
  224. .intValue();
  225. conn.expire(refreshKey, seconds);
  226. conn.expire(refreshAuthKey, seconds);
  227. }
  228. }
  229. conn.closePipeline();
  230. } finally {
  231. conn.close();
  232. }
  233. }
  234. @Override
  235. public OAuth2RefreshToken readRefreshToken(String tokenValue) {
  236. byte[] key = serializeKey(REFRESH + tokenValue);
  237. byte[] bytes = null;
  238. RedisConnection conn = getConnection();
  239. try {
  240. bytes = conn.get(key);
  241. } finally {
  242. conn.close();
  243. }
  244. OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes);
  245. return refreshToken;
  246. }
  247. @Override
  248. public void removeRefreshToken(OAuth2RefreshToken refreshToken) {
  249. this.removeRefreshToken(refreshToken.getValue());
  250. }
  251. public void removeRefreshToken(String tokenValue) {
  252. byte[] refreshKey = serializeKey(REFRESH + tokenValue);
  253. byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue);
  254. byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue);
  255. byte[] access2RefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue);
  256. RedisConnection conn = getConnection();
  257. try {
  258. conn.openPipeline();
  259. conn.del(refreshKey);
  260. conn.del(refreshAuthKey);
  261. conn.del(refresh2AccessKey);
  262. conn.del(access2RefreshKey);
  263. conn.closePipeline();
  264. } finally {
  265. conn.close();
  266. }
  267. }
  268. @Override
  269. public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
  270. this.removeAccessTokenUsingRefreshToken(refreshToken.getValue());
  271. }
  272. private void removeAccessTokenUsingRefreshToken(String refreshToken) {
  273. byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken);
  274. List<Object> results = null;
  275. RedisConnection conn = getConnection();
  276. try {
  277. conn.openPipeline();
  278. conn.get(key);
  279. conn.del(key);
  280. results = conn.closePipeline();
  281. } finally {
  282. conn.close();
  283. }
  284. if (results == null) {
  285. return;
  286. }
  287. byte[] bytes = (byte[]) results.get(0);
  288. String accessToken = deserializeString(bytes);
  289. if (accessToken != null) {
  290. removeAccessToken(accessToken);
  291. }
  292. }
  293. @Override
  294. public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) {
  295. byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName));
  296. List<byte[]> byteList = null;
  297. RedisConnection conn = getConnection();
  298. try {
  299. byteList = conn.lRange(approvalKey, 0, -1);
  300. } finally {
  301. conn.close();
  302. }
  303. if (byteList == null || byteList.size() == 0) {
  304. return Collections.<OAuth2AccessToken> emptySet();
  305. }
  306. List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
  307. for (byte[] bytes : byteList) {
  308. OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
  309. accessTokens.add(accessToken);
  310. }
  311. return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
  312. }
  313. @Override
  314. public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) {
  315. byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId);
  316. List<byte[]> byteList = null;
  317. RedisConnection conn = getConnection();
  318. try {
  319. byteList = conn.lRange(key, 0, -1);
  320. } finally {
  321. conn.close();
  322. }
  323. if (byteList == null || byteList.size() == 0) {
  324. return Collections.<OAuth2AccessToken> emptySet();
  325. }
  326. List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size());
  327. for (byte[] bytes : byteList) {
  328. OAuth2AccessToken accessToken = deserializeAccessToken(bytes);
  329. accessTokens.add(accessToken);
  330. }
  331. return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens);
  332. }
  333. }

配置资源服务器

  1. @Configuration
  2. @EnableResourceServer
  3. @Order(3)
  4. public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  5. @Override
  6. public void configure(HttpSecurity http) throws Exception {
  7. http
  8. .csrf().disable()
  9. .exceptionHandling()
  10. .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
  11. .and()
  12. .requestMatchers().antMatchers("/api/**")
  13. .and()
  14. .authorizeRequests()
  15. .antMatchers("/api/**").authenticated()
  16. .and()
  17. .httpBasic();
  18. }
  19. }

配置 Spring Security

  1. @Configuration
  2. @EnableWebSecurity
  3. @Order(2)
  4. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  5. @Autowired
  6. private MyUserDetailService userDetailService;
  7. @Bean
  8. public PasswordEncoder passwordEncoder() {
  9. //return new BCryptPasswordEncoder();
  10. return new NoEncryptPasswordEncoder();
  11. }
  12. @Override
  13. protected void configure(HttpSecurity http) throws Exception {
  14. http.requestMatchers().antMatchers("/oauth/**")
  15. .and()
  16. .authorizeRequests()
  17. .antMatchers("/oauth/**").authenticated()
  18. .and()
  19. .csrf().disable();
  20. }
  21. @Override
  22. protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  23. auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder());
  24. }
  25. /**
  26. * 不定义没有password grant_type
  27. *
  28. * @return
  29. * @throws Exception
  30. */
  31. @Override
  32. @Bean
  33. public AuthenticationManager authenticationManagerBean() throws Exception {
  34. return super.authenticationManagerBean();
  35. }
  36. }

可以看到 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

  1. @Service("userDetailService")
  2. public class MyUserDetailService implements UserDetailsService {
  3. @Autowired
  4. private MemberDao memberDao;
  5. @Override
  6. public UserDetails loadUserByUsername(String memberName) throws UsernameNotFoundException {
  7. Member member = memberDao.findByMemberName(memberName);
  8. if (member == null) {
  9. throw new UsernameNotFoundException(memberName);
  10. }
  11. Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
  12. // 可用性 :true:可用 false:不可用
  13. boolean enabled = true;
  14. // 过期性 :true:没过期 false:过期
  15. boolean accountNonExpired = true;
  16. // 有效性 :true:凭证有效 false:凭证无效
  17. boolean credentialsNonExpired = true;
  18. // 锁定性 :true:未锁定 false:已锁定
  19. boolean accountNonLocked = true;
  20. for (Role role : member.getRoles()) {
  21. //角色必须是ROLE_开头,可以在数据库中设置
  22. GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName());
  23. grantedAuthorities.add(grantedAuthority);
  24. //获取权限
  25. for (Permission permission : role.getPermissions()) {
  26. GrantedAuthority authority = new SimpleGrantedAuthority(permission.getUri());
  27. grantedAuthorities.add(authority);
  28. }
  29. }
  30. User user = new User(member.getMemberName(), member.getPassword(),
  31. enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
  32. return user;
  33. }
  34. }

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

  1. public class NoEncryptPasswordEncoder implements PasswordEncoder {
  2. @Override
  3. public String encode(CharSequence charSequence) {
  4. return (String) charSequence;
  5. }
  6. @Override
  7. public boolean matches(CharSequence charSequence, String s) {
  8. return s.equals((String) charSequence);
  9. }
  10. }

另外,OAuth 的密码模式需要 AuthenticationManager 支持

  1. @Override
  2. @Bean
  3. public AuthenticationManager authenticationManagerBean() throws Exception {
  4. return super.authenticationManagerBean();
  5. }

定义一个 Controller,提供两个接口,/api/member 用来获取当前用户信息,/api/exit 用来注销当前用户

  1. @RestController
  2. @RequestMapping("/api")
  3. public class MemberController {
  4. @Autowired
  5. private MyUserDetailService userDetailService;
  6. @Autowired
  7. private ConsumerTokenServices consumerTokenServices;
  8. @GetMapping("/member")
  9. public Principal user(Principal member) {
  10. return member;
  11. }
  12. @DeleteMapping(value = "/exit")
  13. public Result revokeToken(String access_token) {
  14. Result result = new Result();
  15. if (consumerTokenServices.revokeToken(access_token)) {
  16. result.setCode(ResultCode.SUCCESS.getCode());
  17. result.setMessage("注销成功");
  18. } else {
  19. result.setCode(ResultCode.FAILED.getCode());
  20. result.setMessage("注销失败");
  21. }
  22. return result;
  23. }
  24. }

会员服务配置

引入依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>eshop-parent</artifactId>
  7. <groupId>com.curise.eshop</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>eshop-member</artifactId>
  12. <packaging>war</packaging>
  13. <description>会员模块</description>
  14. <dependencies>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-web</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.boot</groupId>
  21. <artifactId>spring-boot-starter-test</artifactId>
  22. <scope>test</scope>
  23. </dependency>
  24. <dependency>
  25. <groupId>org.springframework.cloud</groupId>
  26. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.springframework.cloud</groupId>
  30. <artifactId>spring-cloud-starter-oauth2</artifactId>
  31. </dependency>
  32. <dependency>
  33. <groupId>org.springframework.cloud</groupId>
  34. <artifactId>spring-cloud-starter-security</artifactId>
  35. </dependency>
  36. <dependency>
  37. <groupId>com.alibaba</groupId>
  38. <artifactId>fastjson</artifactId>
  39. </dependency>
  40. </dependencies>
  41. <build>
  42. <plugins>
  43. <plugin>
  44. <groupId>org.springframework.boot</groupId>
  45. <artifactId>spring-boot-maven-plugin</artifactId>
  46. </plugin>
  47. </plugins>
  48. </build>
  49. </project>

配置资源服务器

  1. @Configuration
  2. @EnableResourceServer
  3. public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
  4. @Override
  5. public void configure(HttpSecurity http) throws Exception {
  6. http
  7. .csrf().disable()
  8. .exceptionHandling()
  9. .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED))
  10. .and()
  11. .requestMatchers().antMatchers("/api/**")
  12. .and()
  13. .authorizeRequests()
  14. .antMatchers("/api/**").authenticated()
  15. .and()
  16. .httpBasic();
  17. }
  18. }

配置文件配置

  1. spring:
  2. application:
  3. name: eshop-member
  4. server:
  5. port: 1201
  6. eureka:
  7. instance:
  8. prefer-ip-address: true
  9. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  10. client:
  11. service-url:
  12. defaultZone: http://localhost:1111/eureka/
  13. security:
  14. oauth2:
  15. resource:
  16. id: eshop-member
  17. user-info-uri: http://localhost:1202/auth/api/member
  18. prefer-token-info: false

MemberApplication 主类配置

搜索公纵号:MarkerHub,关注回复[ vue ]获取前后端入门教程!

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. @EnableGlobalMethodSecurity(prePostEnabled = true)
  4. public class MemberApplication {
  5. public static void main(String[] args) {
  6. SpringApplication.run(MemberApplication.class,args);
  7. }
  8. }

提供对外接口

  1. @RestController
  2. @RequestMapping("/api")
  3. public class MemberController {
  4. @GetMapping("hello")
  5. @PreAuthorize("hasAnyAuthority('hello')")
  6. public String hello(){
  7. return "hello";
  8. }
  9. @GetMapping("current")
  10. public Principal user(Principal principal) {
  11. return principal;
  12. }
  13. @GetMapping("query")
  14. @PreAuthorize("hasAnyAuthority('query')")
  15. public String query() {
  16. return "具有query权限";
  17. }
  18. }

配置网关

引入依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>eshop-parent</artifactId>
  7. <groupId>com.curise.eshop</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <packaging>jar</packaging>
  12. <artifactId>eshop-gateway</artifactId>
  13. <description>网关</description>
  14. <dependencies>
  15. <dependency>
  16. <groupId>org.springframework.boot</groupId>
  17. <artifactId>spring-boot-starter-web</artifactId>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.springframework.cloud</groupId>
  21. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.cloud</groupId>
  25. <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.cloud</groupId>
  29. <artifactId>spring-cloud-starter-oauth2</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>org.springframework.cloud</groupId>
  33. <artifactId>spring-cloud-starter-security</artifactId>
  34. </dependency>
  35. <dependency>
  36. <groupId>org.springframework.boot</groupId>
  37. <artifactId>spring-boot-starter-actuator</artifactId>
  38. </dependency>
  39. </dependencies>
  40. <build>
  41. <plugins>
  42. <plugin>
  43. <groupId>org.springframework.boot</groupId>
  44. <artifactId>spring-boot-maven-plugin</artifactId>
  45. </plugin>
  46. </plugins>
  47. </build>
  48. </project>

配置文件

  1. server:
  2. port: 1202
  3. spring:
  4. application:
  5. name: eshop-gateway
  6. #--------------------eureka---------------------
  7. eureka:
  8. instance:
  9. prefer-ip-address: true
  10. instance-id: ${spring.cloud.client.ip-address}:${server.port}
  11. client:
  12. service-url:
  13. defaultZone: http://localhost:1111/eureka/
  14. #--------------------Zuul-----------------------
  15. zuul:
  16. routes:
  17. member:
  18. path: /member/**
  19. serviceId: eshop-member
  20. sensitiveHeaders: "*"
  21. auth:
  22. path: /auth/**
  23. serviceId: eshop-auth
  24. sensitiveHeaders: "*"
  25. retryable: false
  26. ignored-services: "*"
  27. ribbon:
  28. eager-load:
  29. enabled: true
  30. host:
  31. connect-timeout-millis: 3000
  32. socket-timeout-millis: 3000
  33. add-proxy-headers: true
  34. #---------------------OAuth2---------------------
  35. security:
  36. oauth2:
  37. client:
  38. access-token-uri: http://localhost:${server.port}/auth/oauth/token
  39. user-authorization-uri: http://localhost:${server.port}/auth/oauth/authorize
  40. client-id: web
  41. resource:
  42. user-info-uri: http://localhost:${server.port}/auth/api/member
  43. prefer-token-info: false
  44. #----------------------超时配置-------------------
  45. ribbon:
  46. ReadTimeout: 3000
  47. ConnectTimeout: 3000
  48. MaxAutoRetries: 1
  49. MaxAutoRetriesNextServer: 2
  50. eureka:
  51. enabled: true
  52. hystrix:
  53. command:
  54. default:
  55. execution:
  56. timeout:
  57. enabled: true
  58. isolation:
  59. thread:
  60. timeoutInMilliseconds: 3500

ZuulApplication 主类

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. @EnableZuulProxy
  4. @EnableOAuth2Sso
  5. public class ZuulApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(ZuulApplication.class, args);
  8. }
  9. }

Spring Security 配置

  1. @Configuration
  2. @EnableWebSecurity
  3. @Order(99)
  4. public class SecurityConfig extends WebSecurityConfigurerAdapter {
  5. @Override
  6. protected void configure(HttpSecurity http) throws Exception {
  7. http.csrf().disable();
  8. }
  9. }

接下来分别启动 eshop-server、eshop-member、eshop-auth、eshop-gateway。

先发送一个请求测试一下未认证的效果

b1ad4ed79a45b13d02e526a569289b54.png

获取认证

d643a511f39500fef0ca89faeb4ee771.png

使用 access_token 请求 auth 服务下的用户信息接口

33f818d1885a9abbfb29d018481662e0.png

使用 access_token 请求 member 服务下的用户信息接口

30dd6834a772203213487d39e7935074.png

请求 member 服务的 query 接口

4d0c9fc8b0b8d906272c197390ac4024.png

请求 member 服务的 hello 接口,数据库里并没有给用户 hello 权限

b09531740c27e0d80e3a32a1a7b0707d.png

刷新 token

220dfa77d6d761d31c912bd121b50e21.png

注销

a0159d8a640915aae1dac915f7f03572.png

后续还会慢慢完善,敬请期待!!

关于代码和数据表 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 认证,如下图:

68372d242e7d1854d2aabbd6c8fd906f.png

,添加 Basic Auth 认证后会在 headers 添加一个认证消息头

2764852449cd443f3f6cd878109de57a.png

添加 Basic Auth 认证的信息在代码中有体现:

799d434927cf484b94112585e0d3b4e5.png

客户端信息和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脚本:

  1. create table oauth_client_details (
  2.   client_id VARCHAR(128) PRIMARY KEY,
  3.   resource_ids VARCHAR(256),
  4.   client_secret VARCHAR(256),
  5.   scope VARCHAR(256),
  6.   authorized_grant_types VARCHAR(256),
  7.   web_server_redirect_uri VARCHAR(256),
  8.   authorities VARCHAR(256),
  9.   access_token_validity INTEGER,
  10.   refresh_token_validity INTEGER,
  11.   additional_information VARCHAR(4096),
  12.   autoapprove VARCHAR(256)
  13. );
  14.  
  15. create table oauth_client_token (
  16.   token_id VARCHAR(256),
  17.   token BLOB,
  18.   authentication_id VARCHAR(128) PRIMARY KEY,
  19.   user_name VARCHAR(256),
  20.   client_id VARCHAR(256)
  21. );
  22.  
  23. create table oauth_access_token (
  24.   token_id VARCHAR(256),
  25.   token BLOB,
  26.   authentication_id VARCHAR(128) PRIMARY KEY,
  27.   user_name VARCHAR(256),
  28.   client_id VARCHAR(256),
  29.   authentication BLOB,
  30.   refresh_token VARCHAR(256)
  31. );
  32.  
  33. create table oauth_refresh_token (
  34.   token_id VARCHAR(256),
  35.   token BLOB,
  36.   authentication BLOB
  37. );
  38.  
  39. create table oauth_code (
  40.   code VARCHAR(256), authentication BLOB
  41. );
  42.  
  43. create table oauth_approvals (
  44.  userId VARCHAR(256),
  45.  clientId VARCHAR(256),
  46.  scope VARCHAR(256),
  47.  status VARCHAR(10),
  48.  expiresAt TIMESTAMP,
  49.  lastModifiedAt TIMESTAMP
  50. );
  51.  
  52.  
  53. -- customized oauth_client_details table
  54. create table ClientDetails (
  55.   appId VARCHAR(128) PRIMARY KEY,
  56.   resourceIds VARCHAR(256),
  57.   appSecret VARCHAR(256),
  58.   scope VARCHAR(256),
  59.   grantTypes VARCHAR(256),
  60.   redirectUrl VARCHAR(256),
  61.   authorities VARCHAR(256),
  62.   access_token_validity INTEGER,
  63.   refresh_token_validity INTEGER,
  64.   additionalInformation VARCHAR(4096),
  65.   autoApproveScopes VARCHAR(256)
  66. );

调整后的sql脚步也放到了GitHub中,需要的可以自行下载

215022702f859213f5b107b5bcae43b1.png

然后在eshop_member数据库创建数据表,将客户端信息添加到oauth_client_details表中

3c38ff1e1cd7803a4eefdb13230c5a95.png

如果你的密码不是明文,记得client_secret需要加密后存储。

然后修改代码,配置从数据库读取客户端信息

2644dc2c047cb3194fa15cb2b15161ff.png

接下来启动服务测试即可。

获取授权

216d04c2fb8d5ef6f9f5780661ab2214.png

获取用户信息

e1b2e889bf6ff4cda47b3ef2711b1e1a.png

刷新token

a56b16d4e391b830d9e11bb9c5dcba5e.png

打开数据表发现token这些信息并没有存到表中,因为tokenStore使用的是redis方式,我们可以替换为从数据库读取。修改配置

3a9995de76a492cf145da7f72f25e410.png

e673e9a7a6cf092e5f4b075d8236fddc.png

重启服务再次测试

3f8fec2c4affc83a237abc1ce2145f6c.png

7ebe1910c86afc27b06dacb528ffc12c.png

查看数据表,发现token数据已经存到表里了。

8d1e3d9275b921cb2965fc9ff6cb2c75.png

2d844e1f42b7f87da633e1c6bc5f3178.png

源码地址:https://github.com/WYA1993/springcloud_oauth2.0。

来自:CSDN,作者:myCat

链接:https://blog.csdn.net/WYA1993/article/details/85050120


(完)

  1. 【推荐阅读】
  2. 如何画出一张优秀的架构图(老鸟必备)
  3. 被问 Linux 命令 su 和 sudo 的区别?当场蒙了!

好文章!点个在看!

本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号