赞
踩
通常的,单体架构,我们会采用Shiro对系统做防护以及权限控制。在搭建微服务系统时,同样也要对资源做保护,只有通过认证的资源才能被访问。下面,我们将借助Spring Cloud OAuth和Spring Cloud Security搭建一个统一给微服务发放访问令牌的认证服务器elsa-auth。
在微服务架构下,我们通常根据不同的业务来构建不同的微服务子系统,各个子系统对外提供相应的服务。客户端除了浏览器外,还可能是手机App,小程序等。在微服务架构出现之前,我们的系统一般为单体模式,客户端只是单一的浏览器,所以通常情况下都是通过Session进行客户端,服务端通信,Session模式有个弊端,就是在一般存在于应用内,而随着客户端种类越来越多,这种交互方式变得越来越困难(当然可以通过Session缓存化的方式来解决),于是OAuth协议应运而生。
OAuth是一种用来规范令牌(Token)发放的授权机制,目前最新版本为2.0,其主要包含了四种授权模式:授权码模式、简化模式、密码模式和客户端模式。Spring Cloud OAuth对这四种授权模式进行了实现。如有不理解的,可以访问如下阮一峰介绍的Oauth2。
由于我们的前端系统是通过用户名和密码来登录系统的,所以我们选用密码模式。
File==>新建==>Other==>搜索Maven,选择Maven Module,然后Next
填写Module Name:elsa-auth,点击Next
一直Next至FInish为止,创建完成,项目结构如下
右键点击Elsa-Auth项目:点击Java Build Path,在Resouce资源下创建资源目录resources。
Elsa-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"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.elsa</groupId> <artifactId>elsa-cloud</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>elas-auth</artifactId> <name>Elsa-Auth</name> <description>Elsa-Cloud认证服务器</description> <dependencies> <dependency> <groupId>com.elsa</groupId> <artifactId>elsa-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
在elsa-common模块引入相关依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
spring-boot-starter-data-redis
-因为后续我们需要将认证服务器生成的Token存储到Redis中,并且Redis依赖可能会被多个微服务使用到
spring-cloud-starter-netflix-eureka-client
-因为每个微服务都可能需要通过Eureka客户端将服务注册到注册中心,所以将依赖添加到通用模块,以方便其他微服务依赖。
@EnableDiscoveryClient
@SpringBootApplication
public class ElsaAuthApp
{
public static void main(String[] args) {
SpringApplication.run(ElsaAuthApp.class, args);
}
}
@EnableDiscoveryClient注解,用于开启服务注册与发现功能
编写配置文件application.yml,Eureka相关配置的含义已通过注解体现,可自行查看。在application.yml如果没有配置Redis相关配置,则采用的是Redis默认配置,但是为了更为直观,建议还是在application.yml中添加Resis配置。
server: port: 8101 spring: application: name: Elsa-Auth # redis相关配置 redis: database: 0 host: 127.0.0.1 port: 6379 jedis: pool: min-idle: 8 max-idle: 500 max-active: 2000 max-wait: 10000 timeout: 5000 eureka: instance: # 向Eureka 服务端发送心跳的间隔时间,单位为秒,用于服务续约。这里配置为20秒,即每隔20秒向febs-register发送心跳,表明当前服务没有宕机 lease-renewal-interval-in-seconds: 20 client: # 为true时表示将当前服务注册到Eureak服务端 register-with-eureka: true # 为true时表示从Eureka 服务端获取注册的服务信息 fetch-registry: true # 新实例信息的变化到Eureka服务端的间隔时间,单位为秒 instance-info-replication-interval-seconds: 30 # 默认值为30秒,即每30秒去Eureka服务端上获取服务并缓存,这里指定为3秒的原因是方便开发时测试,实际可以指定为默认值即可; registry-fetch-interval-seconds: 3 serviceUrl: # 指定Eureka服务端地址 defaultZone: http://elsa:123456@localhost:8001/register/eureka/
首先我们需要定义一个WebSecurity类型的认证安全配置类ElsaSecurityConfigure,在com.elsa.auth路径下新增configure包,然后在configure包下新增ElsaSecurityConfigure类,代码如下所示:
@Order(2) // 增加过滤链的优先级,因为ElsaResourceServerConfigure的优先级为3 @EnableWebSecurity // 开启和Web相关的安全配置 public class ElsaSecurityConfigure extends WebSecurityConfigurerAdapter { @Autowired private ElsaUserDetailService userDetailService; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder();//一个相同的密码,每次加密出来的加密串都不同 } // 一个相同的密码,每次加密出来的加密串都不同 public static void main(String[] args) { String password = "123456"; PasswordEncoder encoder = new BCryptPasswordEncoder(); System.out.println(encoder.encode(password)); System.out.println(encoder.encode(password)); } //密码模式需要使用到这个Bean:AuthenticationManager @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers() .antMatchers("/oauth/**") //安全配置类只对/oauth/开头的请求有效 .and() .authorizeRequests() .antMatchers("/oauth/**").authenticated() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); } }
// 一个相同的密码,每次加密出来的加密串都不同
public static void main(String[] args) {
String password = "123456";
PasswordEncoder encoder = new BCryptPasswordEncoder();
System.out.println(encoder.encode(password));
System.out.println(encoder.encode(password));
}
运行该main方法,可以看到两次输出的结果并不一样:
$2a$10$CztjcNZW8xMlol4EAN/L8eroQly7NZfZe5lNcih.arCEd9MDwkHAi
$2a$10$Jstxp5K0rsp6xocA70M.aOfCYkrdZFV/6mIacOKb6ZtnpBN.r1waK
虽然我们现在正在搭建的是一个认证服务器,但是认证服务器本身也可以对外提供REST服务,比如通过Token获取当前登录用户信息,注销当前Token等,所以它也是一台资源服务器。于是我们需要定义一个资源服务器的配置类,在com.elsa.auth.configure包下新建ElsaResourceServerConfigure类:
@Configuration
@EnableResourceServer //开启资源服务器相关配置
public class ElsaResourceServerConfigure extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.requestMatchers().antMatchers("/**") //表明该安全配置对所有请求都生效
.and()
.authorizeRequests()
.antMatchers("/**").authenticated();
}
}
相信看到这里的人会发现,ElsaSecurityConfigure和ElsaResourceServerConfigure两个配置的功能似乎是一样的,都是对请求过滤的。
ElsaSecurityConfigure对/oauth/开头的请求生效,而ElsaResourceServerConfigure对所有请求都生效,那么当一个请求进来时,到底哪个安全配置先生效?其实并没有哪个配置先生效这么一说,当在Spring Security中定义了多个过滤器链的时候,根据其优先级,只有优先级较高的过滤器链会先进行匹配。
那么ElsaSecurityConfigure和ElsaResourceServerConfigure的优先级是多少?首先我们查看ElsaSecurityConfigure继承的类WebSecurityConfigurerAdapter的源码:
@Order(100)
public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
......
}
可以看到类上使用了@Order(100)标注,说明其顺序是100。
再来看看ElsaResourceServerConfigure类上@EnableResourceServer注解源码:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}
该注解引入了ResourceServerConfiguration配置类,查看ResourceServerConfiguration源码:
@Configuration
public class ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered {
private int order = 3;
......
}
所以ElsaResourceServerConfigure的顺序是3。在Spring中,数字越小,优先级越高,也就是说ElsaResourceServerConfigure的优先级要高于ElsaSecurityConfigure,这也就意味着所有请求都会被ElsaResourceServerConfigure过滤器链处理,包括/oauth/开头的请求。这显然不是我们要的效果,我们原本是希望以/oauth/开头的请求由ElsaSecurityConfigure过滤器链处理,剩下的其他请求由ElsaResourceServerConfigure过滤器链处理。
为了解决上面的问题,我们可以手动指定这两个类的优先级,让ElsaSecurityConfigure的优先级高于ElsaResourceServerConfigure。在ElsaSecurityConfigure类上使用Order(2)注解标注即可:
@Order(2)
@EnableWebSecurity
public class ElsaSecurityConfigure extends WebSecurityConfigurerAdapter {
......
}
ElsaSecurityConfigure和ElsaResourceServerConfigure的区别:
ElsaSecurityConfigure用于处理/oauth开头的请求,Spring Cloud OAuth内部定义的获取令牌,刷新令牌的请求地址都是以/oauth/开头的,也就是说FebsSecurityConfigure用于处理和令牌相关的请求;
ElsaResourceServerConfigure用于处理非/oauth/开头的请求,其主要用于资源的保护,客户端只能通过OAuth2协议发放的令牌来从资源服务器中获取受保护的资源。
接着我们定义一个和认证服务器相关的授权配置类。在configure包下新建ElsaAuthorizationServerConfigure,配置的解释在代码中体现,代码如下所示:
@Configuration @EnableAuthorizationServer //开启认证服务器相关配置 public class ElsaAuthorizationServerConfigure extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private ElsaUserDetailService userDetailService; @Autowired private PasswordEncoder passwordEncoder; /** * 1.客户端从认证服务器获取令牌的时候,必须使用client_id为elsa,client_secret为123456的标识来获取; * 2. 该client_id支持password模式获取令牌,并且可以通过refresh_token来获取新的令牌; * 3. 在获取client_id为elsa的令牌的时候,scope只能指定为all,否则将获取失败 */ @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() // 需要指定多个client,可以继续使用withClient配置 .withClient("elsa") .secret(passwordEncoder.encode("123456")) .authorizedGrantTypes("password", "refresh_token") .scopes("all"); } // tokenStore使用的是RedisTokenStore,认证服务器生成的令牌将被存储到Redis中 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { endpoints.tokenStore(tokenStore()) .userDetailsService(userDetailService) .authenticationManager(authenticationManager) .tokenServices(defaultTokenServices()); } // 认证服务器生成的令牌将被存储到Redis中 @Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); } @Primary @Bean public DefaultTokenServices defaultTokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); // 设置为true表示开启刷新令牌的支持 tokenServices.setSupportRefreshToken(true); // 指定了令牌的基本配置,比如令牌有效时间为60 * 60 * 24秒,刷新令牌有效时间为60 * 60 * 24 * 7秒 tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24); tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7); return tokenServices; } }
ElsaSecurityConfigure及ElsaAuthorizationServerConfigure用到的ElsaUserDetailService。在com.elsa.auth路径下新增service包,然后在service包下新增ElsaUserDetailService类,代码如下所示:
@Service public class ElsaUserDetailService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { ElsaAuthUser user = new ElsaAuthUser(); user.setUsername(username); user.setPassword(this.passwordEncoder.encode("123456")); return new User(username, user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("user:add")); } }
ElsaUserDetailService实现了UserDetailsService接口的loadUserByUsername方法,主要用于校验用户账号和密码,以及授权等,我们模拟了一个用户,用户名为用户输入的用户名,密码为123456(后期再改造为从数据库中获取用户),然后返回org.springframework.security.core.userdetails.User。这里使用的是User类包含7个参数的构造器,其还包含一个三个参数的构造器User(String username, String password,Collection<? extends GrantedAuthority> authorities),由于权限参数不能为空,所以这里先使用AuthorityUtils.commaSeparatedStringToAuthorityList方法模拟一个user:add权限。
loadUserByUsername方法返回一个UserDetails对象,该对象也是一个接口,包含一些用于描述用户信息的方法,源码如下:
public interface UserDetails extends Serializable { //获取用户包含的权限,返回权限集合,权限是一个继承了GrantedAuthority的对象; Collection<? extends GrantedAuthority> getAuthorities(); //用于获取密码和用户名; String getPassword(); //方法返回boolean类型,用于判断账户是否未过期,未过期返回true反之返回false; String getUsername(); //方法用于判断账户是否未锁定; boolean isAccountNonExpired(); //用于判断用户凭证是否没过期,即密码是否未过期; boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); //方法用于判断用户是否可用。 boolean isEnabled(); }
实际中我们可以自定义UserDetails接口的实现类,也可以直接使用Spring Security提供的UserDetails接口实现类org.springframework.security.core.userdetails.User。
ElsaUserDetailService中ElsaAuthUser为我们自定义的用户实体类,代表我们从数据库中查询出来的用户。我们在febs-common中定义该实体类,在elsa-cmmon模块下新增com.elsa.common.entity包,然后在entity包下新增ElsaAuthUser:
public class ElsaAuthUser implements Serializable { private static final long serialVersionUID = -1748289340320186418L; private String username; private String password; private boolean accountNonExpired = true; private boolean accountNonLocked= true; private boolean credentialsNonExpired= true; private boolean enabled= true; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isAccountNonExpired() { return accountNonExpired; } public void setAccountNonExpired(boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; } public boolean isAccountNonLocked() { return accountNonLocked; } public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; } public boolean isCredentialsNonExpired() { return credentialsNonExpired; } public void setCredentialsNonExpired(boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } }
最后定义一个Controller,对外提供一些REST服务。在com.elsa.auth路径下新增controller包,在controller包下新增SecurityController:
@RestController public class SecurityController { @Autowired private ConsumerTokenServices consumerTokenServices; @GetMapping("oauth/test") public String testOauth() { return "oauth"; } //currentUser用户获取当前登录用户 @GetMapping("user") public Principal currentUser(Principal principal) { return principal; } //signout方法通过ConsumerTokenServices来注销当前Token @DeleteMapping("signout") public ElsaResponse signout(HttpServletRequest request) throws ElsaAuthException { String authorization = request.getHeader("Authorization"); String token = StringUtils.replace(authorization, "bearer ", ""); ElsaResponse elsaResponse = new ElsaResponse(); if (!consumerTokenServices.revokeToken(token)) { throw new ElsaAuthException("退出登录失败"); } return elsaResponse.message("退出登录成功"); } }
ElsaResponse为系统的统一相应格式,我们在elsa-common模块中定义它,在elsa-common模块的com.elsa.common.entity路径下新增ElsaResponse类:
public class ElsaResponse extends HashMap<String, Object> { private static final long serialVersionUID = -8713837118340960775L; public ElsaResponse message(String message) { this.put("message", message); return this; } public ElsaResponse data(Object data) { this.put("data", data); return this; } @Override public ElsaResponse put(String key, Object value) { super.put(key, value); return this; } public String getMessage() { return String.valueOf(get("message")); } public Object getData() { return get("data"); } }
ElsaAuthException为自定义异常,在elsa-common模块com.elsa.common路径下新增exception包,然后在该包下新增ElsaAuthException:
public class ElsaAuthException extends Exception{
private static final long serialVersionUID = -6916154462432027437L;
public ElsaAuthException(String message){
super(message);
}
}
分别启动如下应用
1.redis
2.ElsaRegesterApp
3.ElsaAuthApp
grant_type填password,表示密码模式,然后填写用户名和密码,根据我们定义的ElsaUserDetailService逻辑,这里用户名随便填,密码必须为123456。
除了这几个参数外,我们需要在请求头中配置Authorization信息,否则请求将返回401:
值为Basic加空格加client_id:client_secret(就是在ElsaAuthorizationServerConfigure类configure(ClientDetailsServiceConfigurer clients)方法中定义的client和secret)经过base64加密后的值(可以使用http://tool.oschina.net/encrypt?type=3):
点击Send结果如下
查看Redis
我们已经成功获取了访问令牌access_token,接下来使用这个令牌去获取/user资源。
使用PostMan发送 localhost:8101/user GET请求,带上令牌,可以看到已经成功返回了数据。
接着我们使用PostMan发送 localhost:8101/oauth/test GET请求:
可以看到,虽然我们在请求头中已经带上了正确的令牌,但是并没有成功获取到资源,正如前面所说的那样,/oauth/开头的请求由ElsaSecurityConfigure定义的过滤器链处理,它不受资源服务器配置管理,所以使用令牌并不能成功获取到资源。
注销成功后Redis中数据已清空
然后使用refresh_token去换取新的令牌,使用PostMan发送 localhost:8101/oauth/token POST请求,请求参数如下:
刷新令牌在Headers添加参数:
Authorization=Basic ZWxzYToxMjM0NTY=
Params中添加两个参数:
grant_type=refresh_token
refresh_token=登录时得到的refresh_token
可以看到,成功获取到了新的令牌。
源码地址:认证服务器
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。