有偿问答
面经分享
技术探讨
资料领取
登录
SpringCloud+SpringBoot+OAuth2+Spring Security+Redis实现的微服务统一认证授权
社长
1年前
⋅ 424 阅读
因为目前做了一个基于 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 **项目目录** ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/bcd8037ee28540a0a6c61d491743674a.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](https://blog.csdn.net/wya1993/article/category/7701476) ### 授权服务 首先构建 eshop-auth 服务,引入相关依赖 ```
eshop-parent
com.curise.eshop
1.0-SNAPSHOT
4.0.0
eshop-auth
war
授权模块
com.curise.eshop
eshop-common
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.cloud
spring-cloud-starter-security
org.springframework.boot
spring-boot-starter-data-redis
org.mybatis.spring.boot
mybatis-spring-boot-starter
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
com.alibaba
druid
log4j
log4j
org.springframework.boot
spring-boot-maven-plugin
``` 接下来,配置 Mybatis、redis、eureka,贴一下配置文件 ``` server: port: 1203 spring: application: name: eshop-auth redis: database: 0 host: 192.168.0.117 port: 6379 password: jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/eshop_member?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root druid: initialSize: 5 #初始化连接大小 minIdle: 5 #最小连接池数量 maxActive: 20 #最大连接池数量 maxWait: 60000 #获取连接时最大等待时间,单位毫秒 timeBetweenEvictionRunsMillis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 minEvictableIdleTimeMillis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒 validationQuery: 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 public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private MyUserDetailService userDetailService; @Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .allowFormAuthenticationForClients() .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(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() { return new JdbcClientDetailsService(dataSource); } @Bean public WebResponseExceptionTranslator webResponseExceptionTranslator(){ return new MssWebResponseExceptionTranslator(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) .userDetailsService(userDetailService) .authenticationManager(authenticationManager); endpoints.tokenServices(defaultTokenServices()); //认证异常翻译 // endpoints.exceptionTranslator(webResponseExceptionTranslator()); } /** *
注意,自定义TokenServices的时候,需要设置@Primary,否则报错,
* @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(…),测试后方法可行。 ``` public class RedisTokenStore implements TokenStore { private static final String ACCESS = "access:"; private static final String AUTH_TO_ACCESS = "auth_to_access:"; private static final String AUTH = "auth:"; private static final String REFRESH_AUTH = "refresh_auth:"; private static final String ACCESS_TO_REFRESH = "access_to_refresh:"; private static final String REFRESH = "refresh:"; private static final String REFRESH_TO_ACCESS = "refresh_to_access:"; private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:"; private static 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 = ""; public RedisTokenStore(RedisConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) { this.authenticationKeyGenerator = authenticationKeyGenerator; } public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) { this.serializationStrategy = serializationStrategy; } public void setPrefix(String prefix) { this.prefix = prefix; } private RedisConnection getConnection() { return this.connectionFactory.getConnection(); } private byte[] serialize(Object object) { return this.serializationStrategy.serialize(object); } private byte[] serializeKey(String object) { return this.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); } private byte[] serialize(String string) { return this.serializationStrategy.serialize(string); } private String deserializeString(byte[] bytes) { return this.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) { return this.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) { return this.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 public void storeAccessToken(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()) / 1000L) .intValue(); conn.expire(refreshToAccessKey, seconds); conn.expire(accessToRefreshKey, seconds); } } } conn.closePipeline(); } finally { conn.close(); } } private static String getApprovalKey(OAuth2Authentication authentication) { String userName = authentication.getUserAuthentication() == null ? "": authentication.getUserAuthentication().getName(); return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); } private static String getApprovalKey(String clientId, String userName) { return clientId + (userName == null ? "" : ":" + userName); } @Override public void removeAccessToken(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; } public void removeAccessToken(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
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 public void storeRefreshToken(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()) / 1000L) .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 public void removeRefreshToken(OAuth2RefreshToken refreshToken) { this.removeRefreshToken(refreshToken.getValue()); } public void removeRefreshToken(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 public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { this.removeAccessTokenUsingRefreshToken(refreshToken.getValue()); } private void removeAccessTokenUsingRefreshToken(String refreshToken) { byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken); List
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
findTokensByClientIdAndUserName(String clientId, String userName) { byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName)); List
byteList = null; RedisConnection conn = getConnection(); try { byteList = conn.lRange(approvalKey, 0, -1); } finally { conn.close(); } if (byteList == null || byteList.size() == 0) { return Collections.
emptySet(); } List
accessTokens = new ArrayList
(byteList.size()); for (byte[] bytes : byteList) { OAuth2AccessToken accessToken = deserializeAccessToken(bytes); accessTokens.add(accessToken); } return Collections.
unmodifiableCollection(accessTokens); } @Override public Collection
findTokensByClientId(String clientId) { byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId); List
byteList = null; RedisConnection conn = getConnection(); try { byteList = conn.lRange(key, 0, -1); } finally { conn.close(); } if (byteList == null || byteList.size() == 0) { return Collections.
emptySet(); } List
accessTokens = new ArrayList
(byteList.size()); for (byte[] bytes : byteList) { OAuth2AccessToken accessToken = deserializeAccessToken(bytes); accessTokens.add(accessToken); } return Collections.
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(); return new NoEncryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers().antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/oauth/**").authenticated() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); } /** * 不定义没有password grant_type * * @return * @throws Exception */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.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") public class MyUserDetailService implements UserDetailsService { @Autowired private MemberDao memberDao; @Override public UserDetails loadUserByUsername(String memberName) throws UsernameNotFoundException { Member member = memberDao.findByMemberName(memberName); if (member == null) { throw new UsernameNotFoundException(memberName); } Set
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。 ``` public class NoEncryptPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { return (String) charSequence; } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals((String) charSequence); } } ``` 另外,OAuth 的密码模式需要 AuthenticationManager 支持 ``` @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } ``` 定义一个 Controller,提供两个接口,/api/member 用来获取当前用户信息,/api/exit 用来注销当前用户 ``` @RestController @RequestMapping("/api") public class MemberController { @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; } } ``` ### 会员服务配置 引入依赖 ```
eshop-parent
com.curise.eshop
1.0-SNAPSHOT
4.0.0
eshop-member
war
会员模块
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.cloud
spring-cloud-starter-security
com.alibaba
fastjson
org.springframework.boot
spring-boot-maven-plugin
``` 配置资源服务器 ``` @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(); } } ``` 配置文件配置 ``` 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) public class MemberApplication { public static void main(String[] args) { SpringApplication.run(MemberApplication.class,args); } } ``` 提供对外接口 ``` @RestController @RequestMapping("/api") public class MemberController { @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权限"; } } ``` ### 配置网关 引入依赖 ```
eshop-parent
com.curise.eshop
1.0-SNAPSHOT
4.0.0
jar
eshop-gateway
网关
org.springframework.boot
spring-boot-starter-web
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
org.springframework.cloud
spring-cloud-starter-oauth2
org.springframework.cloud
spring-cloud-starter-security
org.springframework.boot
spring-boot-starter-actuator
org.springframework.boot
spring-boot-maven-plugin
``` 配置文件 ``` 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 #----------------------超时配置------------------- 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 public class ZuulApplication { public static void main(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。 先发送一个请求测试一下未认证的效果 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/89abed33971d4a8aa3bf288029c9e10b.png) 获取认证 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/d5e1f8d577264b379601ebe626463177.png) 使用 access_token 请求 auth 服务下的用户信息接口 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/52ee2764dd1b482c9b0477f1e6ade5e0.png) 使用 access_token 请求 member 服务下的用户信息接口 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/c3de69eb563d44f698af063dd4007878.png) 请求 member 服务的 query 接口 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/a59ad80e82aa431983f1833e14cad3d9.png) 请求 member 服务的 hello 接口,数据库里并没有给用户 hello 权限 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/e14c0df4fd4d4992adc22a375bea4aa0.png) 刷新 token ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/9e274bcfcec444ee96758a0d2195995a.png) 注销 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/1b8bf4818cdb40c0b097ad826f9d450a.png) 后续还会慢慢完善,敬请期待!! 关于代码和数据表 sql 已经上传到 GitHub。地址:[https://github.com/WYA1993/springcloud_oauth2.0](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 认证,如下图: ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/28730259bb2449afad44394345e36522.png) ,添加 Basic Auth 认证后会在 headers 添加一个认证消息头 ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/7759259af7b3432eab36d40d70a2f167.png) 添加 Basic Auth 认证的信息在代码中有体现: ![](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/bd091aa0c5374a28b0e0ba8f0fd04c78.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脚本: ``` create table oauth_client_details ( client_id VARCHAR(128) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(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) ); create table oauth_client_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(128) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256) ); create table oauth_access_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(128) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256), authentication BLOB, refresh_token VARCHAR(256) ); create table oauth_refresh_token ( token_id VARCHAR(256), token BLOB, authentication BLOB ); create table oauth_code ( code VARCHAR(256), authentication BLOB ); create table oauth_approvals ( userId VARCHAR(256), clientId VARCHAR(256), scope VARCHAR(256), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); -- customized oauth_client_details table create table ClientDetails ( appId VARCHAR(128) PRIMARY KEY, resourceIds VARCHAR(256), appSecret VARCHAR(256), scope VARCHAR(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中,需要的可以自行下载 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/a276eb09fa534f618285b0eb4c1140e4.png) 然后在eshop_member数据库创建数据表,将客户端信息添加到oauth_client_details表中 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/dc4150305f2143ed9ce7ae9f4c6831f3.png) 如果你的密码不是明文,记得client_secret需要加密后存储。 然后修改代码,配置从数据库读取客户端信息 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/ad807abbcec24547bcf7ec11e7ad4c3b.png) 接下来启动服务测试即可。 获取授权 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/2d4325d8381a41db986174f5d2cb5d2a.png) 获取用户信息 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/a768b73f87d245728d60e5b18d067fc6.png) 刷新token ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/33f362857d9c44d783e1b16d75336143.png) 打开数据表发现token这些信息并没有存到表中,因为tokenStore使用的是redis方式,我们可以替换为从数据库读取。修改配置 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/cb4e9e97d59e47ee90ad5b7322424cf3.png) ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/98865385f32d4eeca6122a716f108e63.png) 重启服务再次测试 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/f51201b5c4ea408d81442a6b9cc765e5.png) ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/349f5e63f00e453c975ccc7d83d2e4d1.png) 查看数据表,发现token数据已经存到表里了。 ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/7c73c82e6afa4d7993173c80d1079d77.png) ![图片](https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/20211113/22f3cdd199e848bab2455970ee950a70.png) > 源码地址:https://github.com/WYA1993/springcloud_oauth2.0。 > > 来自:CSDN,作者:myCat > > 链接:https://blog.csdn.net/WYA1993/article/details/85050120
阅读全部
全部评论:
0
条
我有话说:
@
发送
-- 目录 --
关注官方公众号:
Java问答社
接收最新有赏问答推送!
最新发布
1.
SpringBoot 接口数据加解密技巧,so easy!
2.
一个依赖搞定 Spring Boot 反爬虫,防止接口盗刷!
3.
Java8 Stream 极大简化了代码,它是如何实现的?
4.
马上大四了,秋招还是春招好?先找工作还是找实习?
5.
万字详解 Linux 常用指令(值得收藏)
6.
4年工作经验,多线程间的5种通信方式都说不出来,你敢信?
最新评论
部署文档没有了,您能提供下吗
部署文档没有了,能提供下吗
我测你的🐎
源码从哪里获取请问
想学
那篇石墨文档 没有权限查看哇