SpringCloud+OAuth2+Redis实现的微服务统一认证授权
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"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>eshop-parent</artifactId> <groupId>com.curise.eshop</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eshop-auth</artifactId> <packaging>war</packaging> <description>授权模块</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 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()); } /** * <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认证时会报如下异常:
原因是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<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 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<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(); 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<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。
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; } }
会员服务配置
引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>eshop-parent</artifactId> <groupId>com.curise.eshop</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eshop-member</artifactId> <packaging>war</packaging> <description>会员模块</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(); } }
配置文件配置
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权限"; } }
配置网关
引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>eshop-parent</artifactId> <groupId>com.curise.eshop</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <packaging>jar</packaging> <artifactId>eshop-gateway</artifactId> <description>网关</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
配置文件
server:
port: 1202
spring:
application:
name: eshop-gateway
#--------------------eureka---------------------
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:1111/eureka/
#--------------------Zuul-----------------------
zuul:
routes:
member:
path: /member/**
serviceId: eshop-member
sensitiveHeaders: "*"
auth:
path: /auth/**
serviceId: eshop-auth
sensitiveHeaders: "*"
retryable: false
ignored-services: "*"
ribbon:
eager-load:
enabled: true
host:
connect-timeout-millis: 3000
socket-timeout-millis: 3000
add-proxy-headers: true
#---------------------OAuth2---------------------
security:
oauth2:
client:
access-token-uri: http://localhost:${server.port}/auth/oauth/token
user-authorization-uri: http://localhost:${server.port}/auth/oauth/authorize
client-id: web
resource:
user-info-uri: http://localhost:${server.port}/auth/api/member
prefer-token-info: false
#----------------------超时配置-------------------
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。
先发送一个请求测试一下未认证的效果
获取认证
使用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