案例分析
首先来看案例的架构设计,在这个案例中有3个工程,分别是服务注册中心工程eureka-server、授权中心Uaa工程auth-service和资源工程service-hi,如图:

首先,浏览器向auth-service 服务器提供客户端信息、用户名和密码,请求获取Token。auth-service确认这些信息无误后,根据该用户的信息生成Token并返回给浏览器。浏览器在以后的每次请求都需要携带Token给资源服务service-hi,资源服务器获取到请求携带的Token后,通过远程调度将Token给授权服务auth-service确认。auth-service确认Token正确无误后,将该Token对应的用户的权限信息返回给资源服务service-hi。如果该Token对应的用户具有访问该API接口的权限,就正常返回请求结果,否则返回权限不足的错误提示。
编写Eureka Sever:
依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
配置:
server:
port: 8761
security:
user:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
启动类:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
编写Uaa授权服务:
在主Maven工程下创建-一个Module工程,取名为auth-service, 作为Uaa服务(授权服务),在auth-service工程的pom文件里引入工程所需的依赖,代码如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
</dependencies>
其中,spring-cloud-starter-oauth2 是对spring-cloud-starter-security、 spring- security-oauth2和spring-security-jwt这3个起步依赖的整合。在工程中使用了MySQL数据库,引入了MySQL的连接驱动依赖mysql-connector-java和JPA的起步依赖spring-boot- starter- data-jpa。在工程中使用了Web功能,引入了Web的起步依赖spring -boot starter- web。这个工程作为Eureka Client,引入了Eureka 的起步依赖spring-cloud-starter-eurekao
配置:
server:
port: 5000
context-path: /uaa
spring:
application:
name: service-auth
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
security:
oauth2:
resource:
filter-order: 3
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
在上面的配置中,配置了程序名为service -auth,程序的端口号为5000, context-path为“/uaa";配置了MySQL 数据库的相关配置,包括数据源、用户和密码,其中数据库名为spring-cloud-auth,需要初始化12.3.1节的数据库脚本;使用JPA作为ORM框架,并对JPA做了相关的配置;配置了服务注册中心的地址为htp://ocalhost:8761/eureka/; 配置security.oauth2 resource.filter-order为3,在Spring Boot 1.5.x版本,这是固定写法,在Spring Boot 1.5.x版本之前,默认即可。
配置Spring Security
由于auth-service需要对外暴露检查Token的API接口,所以auth-service也是一个资源服务,需要在工程中引入Spring Security,并做相关的配置,对auth-service资源进行保护。配置代码如下:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.csrf().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
WebScurityConfig类通过@EnableWebSecurity注解开启Web保护功能,通过@EnableGlobalMethodSecurity注解开启在方法上的保护功能。WebSecurityConfig 类继承了WebSecurity-ConfigurerAdapter类,并复写了以下3个方法来做相关的配置.
- configure(HttpSecurity http):HttpSecurity 中配置了所有的请求都需要安全验证
- configure(AuthenticationManagerBuilder auth);:AuthenticationManagerBuilder 中配置了验证的用户信息源和密码加密的策略,并且向IoC容器注入AuthenticationManager对象。这需要在OAuth2中配置,因为在OAuth2中配置了AuthenticationManager,密码验证才会开启。在本例中,采用的是密码验证。
- authenticationManagerBean():配置了验证管理的Bean。
UserService:
@Service
public class UserService implements UserDetailsService {
@Autowired
UserDao userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
return userRepository.findByUsername(s);
}
}
UserDao:
public interface UserDao extends JpaRepository<User, Long> {
User findByUsername(String username);
}
User:
@Entity
public class User implements UserDetails, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private String password;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private List<Role> authorities;
public User() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Role:
@Entity
public class Role implements GrantedAuthority {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getAuthority() {
return name;
}
}
配置Authorization Server:
@SpringBootApplication
@EnableResourceServer
@EnableEurekaClient
public class AuthServiceApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServiceApplication.class, args);
}
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
@Configuration
@EnableAuthorizationServer
protected class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
// private TokenStore tokenStore = new InMemoryTokenStore();
JdbcTokenStore jdbcTokenStore = new JdbcTokenStore(dataSource);
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
private UserService userService;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("browser")
.authorizedGrantTypes("refresh_token", "password")
.scopes("ui")
.and()
.withClient("service-hi")
.secret("123456")
.authorizedGrantTypes("client_credentials", "refresh_token", "password")
.scopes("server");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenStore(jdbcTokenStore)
.authenticationManager(authenticationManager)
.userDetailsService(userService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
}
}
在程序启动类ServiceAuthApplication加上@EnableEurekaClient注解,开启Eureka Client客户端的功能,加上@EnableResourceServer注解,开启Resource Server。 程序需要对外暴露获取Token的API接口和验证Token的API接口,所以该程序也是一个资源服务。
OAuth2AuthorizationConfig类继承AuthorizationServerConfigurerAdapter,并在这个类上加上注解@EnableAuthorizationServer, 开启授权服务的功能。作为授权服务需要配置3个选项,分别为ClientDetailsServiceConfigurer、AuthorizationServerEndpointsConfigurer 和AuthorizationServerSecurityConfigurer。
其中,ClientDetaisServiceConfigurer 配置了客户端的一些基本信息,clients.inMemory()方法配置了将客户端的信息存储在内存中,.withClient("browser")方法创建了一个clientld 为browser 的客户端,authorizedGrantTypes("refresh token", "password")方法配置了验证类型为refresh token和password, .scopesl("ui")方法配置了客户端域为“ui”。接着创建了另一个client,它的Id为“service-hi"。
AuthorizationServerEndpointsConfigurer需要配置tokenStore 、authenticationManager 和userServiceDetail其中,tokenStore (Token 的存储方式)采用的方式是将Token存储在内存中,即使用InMemory TokenStore。如果资源服务和授权服务是同一个服务,用InMemory TokenStore是最好的选择。如果资源服务和授权服务不是同一个服务, 则不用InMemoryTokenStore 进行存储Token。因为当授权服务出现故障,需要重启服务,之前存在内存中Token全部丢失,导致资源服务的Token全部失效。
另外一种方式是用JdbcTokenStore,即使用数据库去存储,使用Jdbc TokenStore存储需要引入连接数据库依赖,如本例中的MySQL连接器、JPA, 并且需要初始化数据库脚本。authenticationManager 需要配置AuthenticationManager这个Bean,这个Bean来源于WebSecurityConfigurerAdapter中的配置,只有配置了这个Bean才会开启密码类型的验证。最后配置了userDetailService, 用来读取验证用户的信息。
AuthorizationServerSecurityConfigurer配置了获取Token的策略,在本案例中对获取Token请求不进行拦截,只需要验证获取Token的验证信息,这些信息准确无误,就返回Token。另外配置了检查Token的策略。
暴露Remote Token Services接口:
本案例采用RemoteTokenServices这种方式对Token进行验证。如果其他资源服务需要验证Token,则需要远程调用授权服务暴露的验证Token的API接口。本案例中验证Token的API接口的代码如下:
@RestController
@RequestMapping("/users")
public class UserController {
@RequestMapping(value = "/current", method = RequestMethod.GET)
public Principal getUser(Principal principal) {
return principal;
}
}
获取Token:
前提:数据库中初始化了token的sql脚本:



postman:

编写service-hi资源服务:
在主Maven工程下,创建一一个 Module工程,取名为service-hi,这个工程作为资源服务。在service-hi工程的pom文件引入项目所需的依赖,代码如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
在工程中用到了MySQL数据库,采用JPA的ORM框架来操作数据库,所以需要在工程的pom文件引入JPA的起步依赖spring-boot-starter-data-jpa和MySQL数据库连接器依赖mysql-connector-java。作为Eureka Client, 需要在工程的pom文件引入Eureka 的起步依赖spring-cloud-starter-eureka。作为Web服务器,需要在工程的pom文件引入Web的起步依赖spring-boot-starter-web。另外使用Feign 作为远程调度框架,需要在工程的pom文件引入Feign的起步依赖spring-cloud-starter-feign。 最后作为资源服务器,需要在工程的pom文件引入OAuth2的起步依赖spring-cloud-starter- oauth2。
配置文件:application.yml
server:
port: 8762
spring:
application:
name: service-hi
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/auth?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
username: root
password: 123456
jpa:
hibernate:
ddl-auto: update
show-sql: true
security:
oauth2:
resource:
user-info-uri: http://localhost:5000/uaa/users/current
client:
client-id: service-hi
client-secret: 123456
access-token-uri: http://localhost:5000/uaa/oauth/token
grant-type: client_credentials,password
scope: server
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
配置了security.oauth2.resource, 指定了user-info-uri的地址,用于获取当前Token的用户信息,配置了security.oauth2.client 的相关信息,以及clientId、clientSecret等信息,这些配置需要和在Uaa服务中配置的一一对应。
service-hi工程作为Resource Server (资源服务),需要配置Resource Server的相关配置,配置代码如下:
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/user/registry").permitAll()
.anyRequest().authenticated();
}
}
在ResourceServerConfigurer类上加@EnableResourceServer 注解,开启Resource Server的功能,加@EnableGlobalMethodSecurity 注解,开启方法级别的保护。ResourceServerConfigurer 类继承ResourceServerConfigurerAdapter 类,并重写configre(HttpSecurity http)方法,通过ant表达式,配置哪些请求需要验证,哪些请求不需要验证。如本案例中“/useregister”的接口不需要验证,其他所有的请求都需要验证。
配置OAuth Client:
OAuth2 Client 用来访问被OAuth2保护的资源。service-hi 作为OAuth2 Client, 它的配置代码如下:
@EnableOAuth2Client
@EnableConfigurationProperties
@Configuration
public class OAuth2ClientConfig {
@Bean
@ConfigurationProperties(prefix = "security.oauth2.client")
public ClientCredentialsResourceDetails clientCredentialsResourceDetails() {
return new ClientCredentialsResourceDetails();
}
@Bean
public RequestInterceptor oauth2FeignRequestInterceptor() {
return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(),
clientCredentialsResourceDetails());
}
@Bean
public OAuth2RestTemplate clientCredentialsRestTemplate() {
return new OAuth2RestTemplate(clientCredentialsResourceDetails());
}
}
需要配置3个选项:
- 一是配置受保护的资源的信息,即ClientCredentialsResourceDetails;
- 二是配置一个过滤器,存储当前请求和上下文;
- 三是在Request域内创建AccessTokenRequest类型的Bean。
现在通过上述代码来具体说明,在OAuth2ClientConfig类上加@EnableOAuth2Client注解,开启OAuth2 Client 的功能;并配置了一个ClientCredentialsResourceDetails 类型的Bean, 该Bean是通过读取配置文件中前缀为security.oauth2.client的配置来获取Bean的配置属性的;注入一个OAuth2FeignRequestInterceptor类型过滤器的Bean;最后注入了一个用于向Uaa服务请求的OAuth2RestTemplate类型的Bean。
到目前为止,授权服务、资源服务和0Auth2客户端都已经搭建完毕,现在写一个注册API接口来做测试。
编写用户注册接口:
首先编写一个User类,在本案例总共采用了JPA作为ORM框架,需要在User类加上JPA的注解,同Uaa服务的User类一样。
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column
private String password;
......
}
数据操作类UserDao继承了JpaRepository, UserDao具备了基本的操作数据库单表的基本方法,代码如下:
public interface UserDao extends JpaRepository<User, Long> {
}
Service层的UserServicelmpl类包含-一个创建用户逻辑的方法, 其中用到BCryptPasswordEncoder类来加密密码,代码如下:
@Service
public class UserServiceImpl implements UserService{
private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
@Autowired
private UserDao userDao;
@Override
public User create(User user) {
String hash = encoder.encode(user.getPassword());
user.setPassword(hash);
User u = userDao.save(user);
return u;
}
}
编写UserController类,在类中有一个注册的API接口,代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/registry")
public User createUser(@RequestParam("username") String username, @RequestParam("password") String password) {
User u = new User();
u.setPassword(password);
u.setUsername(username);
return userService.create(u);
}
}
编写一个测试类HiController,其中有3个接口:第一个API接口“hi”,不需要任何权限,只需要验证Header中的Token正确与否,Token 正确即可访问;第二个API接口“hello",需要“ROLE ADMIN"权限;第三个接口“/getPrinciple”,, 用户获取当前Token 用户信息。代码如下:
@RestController
public class HiController {
Logger logger = LoggerFactory.getLogger(HiController.class);
@Value("${server.port}")
String port;
@RequestMapping("/hi")
public String home() {
return "hi :" + ", i am from port:" + port;
}
@PreAuthorize("hasAnyAuthority('ROLE_ADMIN')")
@RequestMapping("/hello")
public String hello() {
return "hello you!";
}
@GetMapping("/getPriciple")
public OAuth2Authentication getPrinciple(OAuth2Authentication oAuth2Authentication,
Principal principal, Authentication authentication) {
logger.info(oAuth2Authentication.getUserAuthentication().getAuthorities().toString());
logger.info(oAuth2Authentication.toString());
logger.info("principal.toString()" + principal.toString());
logger.info("principal.getName()" + principal.getName());
logger.info("authentication:" + authentication.getAuthorities().toString());
return oAuth2Authentication;
}
}
测试
调用注册API接口,注册一个用户:

调用获取Token的API接口:

访问不需要权限点的接口"/hi":

访问需要有权限"ROLE_ADMIN"权限点的API接口"/hello":

在数据库中给予用户"ROLE_ADMIN"权限:
insert into `role` values('1', 'ROLE_USER'),('2','ROLE_ADMIN');
insert into `user_role` values('3','2');
再次访问:

访问"/getPriciple"接口:

总结
案例的架构有改进之处,例如在资源服务器加一个登录接口,该接口不受Spring Security保护。登录成功后,service-hi 远程调用auth-service 获取Token返回给浏览器,浏览器以后所有的请求都需要携带该Token。
这个架构存在的缺陷就是每次请求都需要资源服务内部远程调度auth-service服务来验证Token的正确性,以及该Token对应的用户所具有的权限,额外多了一次内部请求。如果在高并发的情况下, auth-service需要集群部署,并且需要做缓存处理。
本文详细解析了在微服务架构中,如何利用OAuth2实现服务间及客户端的安全认证。介绍了服务注册中心EurekaServer、授权中心Uaa(auth-service)与资源服务(service-hi)之间的交互过程,包括Token的生成、验证及权限检查。

3801

被折叠的 条评论
为什么被折叠?



