写点什么

Spring Cloud 微服务实践 (5) - 认证中心

用户头像
xiaoboey
关注
发布于: 2020 年 09 月 23 日
Spring Cloud 微服务实践(5) - 认证中心

本文基于上一篇文章的理论,进行实际开发,在现有的Spring Cloud 微服务系统上集成Spring Cloud OAuth2,并且采用增强的JWT令牌传输用户授权。

1、源代码

Github上的地址: https://github.com/xiaoboey/from-zero-to-n/tree/master/two ,Spring Boot的版本是2.3.3,Spring Cloud的版本是Hoxton.SR8。



如果有一定的Spring Boot和Spring Cloud基础,是可以直接Clone代码,然后运行起来再结合代码来理解的。

2、OAuth2的非对称加密

这里我们用非对称加密来增强JWT的安全性:认证中心(AuthorizationServer)使用私钥签发JWT令牌,其他资源服务(ResourceServer)使用公钥来验证JWT令牌。非对称加密的这套机制可以确保只有认证中心才能签发令牌(授权),并且防止JWT在网络传输过程中被篡改,资源服务也可以确信JWT是认证中心签发的,这是非对称加密的“防篡改,不可抵赖”特性。



使用Java的keytool工具生成私钥:

keytool -genkeypair -alias oauth2-jwt -keyalg RSA -keystore oauth2-jwt.jks -validity 3650
  • keyalg: 加密算法

  • validity: 证书有效期,单位是天



注:截图里我是用的PowerShell,并且在命令行里指定了密码



查看证书:

keytool -list -keystore oauth2-jwt.jks





导出公钥:

keytool -export -alias oauth2-jwt -rfc --keystore oauth2-jwt.jks -file public.txt





但是Spring Cloud OAuth2不认这个公钥,还需要用openssl来转换一下(可以在Git Bash中执行openssl):

openssl x509 -inform pem -pubkey -in public.txt



注:上图中除了$ openssl那一行,其他就是导出的公钥,复制下来保存为oauth2-jwt.cert

3、认证中心(OAuth2 Server)

项目名称auth-server,pom.xml如下:

...
<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.boot</groupId>
<artifactId>spring-boot-starter-security</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>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<!--防止jks文件被mavne编译导致不可用-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</build>
...

从pom.xml可以看出,auth-server项目多了数据库相关的依赖(jpa、h2)和权限控制方面的依赖(security、oauth2)。



之所以要引入JPA和H2,是因为auth-server作为认证中心,需要持有用户的账号密码和用户关联的角色信息,以便进行用户的认证和授权。数据库选H2是想方便大家验证代码,clone下来就可以直接编译跑起来。



配置文件application.yml:

...
spring:
application:
name: auth-server
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:file:D:\\temp\\local-db\\h2-two\\testdb
jpa:
database-platform: org.hibernate.dialect.H2Dialect
hibernate:
ddl-auto: update
show-sql: false
...

注:ddl-auto设置为update,会自动根据Entity创建表。



JPA相关的配置JpaConfig.java

@Configuration
@EnableJpaRepositories(basePackages = {"com.example.two.authserver"})
@EntityScan({"com.example.two"})
@ComponentScan({"com.example.two.authserver"})
public class JpaConfig {
}



用户实体(User.java):

@Entity
public class User implements UserDetails, Serializable {
@Id
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 User(long id, String username, String password) {
this.id = id;
this.setUsername(username);
this.setPassword(password);
}
public Long getId() {
return id;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
@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.java):

@Entity
public class Role implements GrantedAuthority {
@Id
private Long id;
@Column(nullable = false)
private String name;
@Override
public String getAuthority() {
return name;
}
...
}

注:GrantedAuthority中authority的值,当前缀是ROLE_时表示角色,否则是权限。



UserService.java

@Service
public class UserService implements UserDetailsService {
private final UserRepository userDao;
public UserService(UserRepository userDao) {
this.userDao = userDao;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userDao.findByUsername(username);
}
}



在启动类(AuthServerApplication.java)这里进行数据库的初始化,写入用户和角色数据,方便后续的测试:

@SpringBootApplication
@EnableDiscoveryClient
public class AuthServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthServerApplication.class, args);
}
@Autowired
private RoleRepository roleRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostConstruct
public void initialize() {
//初始化角色表
if (!roleRepository.findById(1L).isPresent()) {
Role role = new Role(1L, "ROLE_ADMIN");
roleRepository.save(role);
}
//初始化用户表
User admin = userRepository.findByUsername("admin");
if (admin == null) {
Role role = roleRepository.findById(1L).get();
List<Role> roleList = new ArrayList<>();
roleList.add(role);
//增加用户admin并关联角色ROLE_ADMIN
admin = new User(1L, "admin", passwordEncoder.encode("admin"));
admin.setAuthorities(roleList);
userRepository.save(admin);
//增加用户worker
User worker = new User(2L, "worker", passwordEncoder.encode("worker"));
userRepository.save(worker);
}
}
}

注:内置了一个admin用户(密码admin,角色ROLE_ADMIN)和一个worker用户(密码worker,无角色)



WebSecurity的配置(WebSecurityConfig.java),前面的User、Role和UserService都是为这个配置做的准备:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final UserService userService;
public WebSecurityConfig(UserService userService) {
this.userService = userService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.csrf().disable()
.formLogin().disable()
.logout().disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}



认证中心的配置(AuthServerConfig.java):

@Configuration
@EnableAuthorizationServer
@EnableResourceServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UserService userService;
/**
* 配置AuthClient信息,也就是资源服务,这里是service-one和service-two
* GrantTypes表示授权类型,后面测试的时候再细说
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("service-one")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("client_credentials", "refresh_token", "password", SmsTokenGranter.GRANT_SMS_CODE)
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(7200)
.scopes("server")
.and()
.withClient("service-two")
.secret(passwordEncoder.encode("123456"))
.authorizedGrantTypes("client_credentials", "refresh_token", "password")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(7200)
.scopes("server");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.userDetailsService(userService)
.reuseRefreshTokens(false)
.tokenEnhancer(jwtTokenEnhancer());
// 自定义验证(短信验证码),其他验证模式可以参考这个实现
List<TokenGranter> tokenGranters = getDefaultTokenGranters(endpoints);
tokenGranters.add(new SmsTokenGranter(endpoints, userService));
endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters));
}
/**
* 这部分代码是根据 AuthorizationServerEndpointsConfigurer.getDefaultTokenGranters() 进行了修改
* 只是做了private method访问的调整,其他代码原封不动
*
* @param endpoints
* @return
*/
private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {
ClientDetailsService clientDetails = endpoints.getClientDetailsService();
AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
clientDetails, requestFactory));
}
return tokenGranters;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}
/**
* 设置Jwt处理的非对称加密私钥证书
* @return
*/
@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
KeyStoreKeyFactory keyStoreKeyFactory =
new KeyStoreKeyFactory(new ClassPathResource("oauth2-jwt.jks"), "mypass".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("oauth2-jwt"));
return converter;
}
}

注:@EnableAuthorizationServer和@EnableResourceServer开启了OAuth2的认证中心和资源服务器能力,身兼两职;



自定义授权类型“短信验证码”(SmsTokenGranter.java):

public class SmsTokenGranter extends AbstractTokenGranter {
public static final String GRANT_SMS_CODE = "sms_code";
protected UserDetailsService userDetailsService;
public SmsTokenGranter(AuthorizationServerEndpointsConfigurer endpoints,
UserDetailsService userDetailsService) {
super(endpoints.getTokenServices(), endpoints.getClientDetailsService(), endpoints.getOAuth2RequestFactory(), GRANT_SMS_CODE);
this.userDetailsService = userDetailsService;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
//TODO: 这里只是示例,内置了短信验证码
Map<String, String> parameters = tokenRequest.getRequestParameters();
String smsCode = parameters.get("sms_code");
if (smsCode.equals("123456")) {
UserDetails userDetails = userDetailsService.loadUserByUsername("admin");
PreAuthenticatedAuthenticationToken authenticationToken = new PreAuthenticatedAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(userDetails);
OAuth2Request oAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(oAuth2Request, authenticationToken);
} else {
throw new InvalidGrantException("SmsCode invalid!");
}
}
}

4、测试auth-server

为了方便测试,这里使用了Postman,官网:https://www.postman.com



认证中心auth-server启动后,可以从/oauth/token获取access token,就是JWT令牌。这里我们先用Postman测试一下令牌的获取,确保auth-server运转正常。建议大家在实际开发中,也先用第三方工具进行验证,因为不管是团队内部沟通还是跟外部交流,第三方工具出的测试结果更有说服力,更容易达成一致。



通过/oauth/token获取访问令牌,有多种方式。在AuthServerConfig.java中也可以看到,我们针对不同的客户端(OAuthClient)配置了多种GrantType,这里就分别测试一下。



GrantType: client_credentials,客户端凭据,这里所谓的凭据,就是AuthServerConfig.java中配置的客户端Id和secret。这个方式是用的客户端应用的身份,跟我们前面说的RBAC(基于角色的权限控制)里的用户、角色等是没关系的。

  • Url: http://localhost:8100/oauth/token

  • Method: POST

  • Params: grant_type=client_credentials

  • Authorization: Basic Auth,Username=service-one,Password=123456







GrantType: password,账号密码方式,提供用户的账号密码给auther-server进行用户认证,通过后返回包含用户账号和角色的JWT令牌。

  • Url: http://localhost:8100/oauth/token

  • Method: POST

  • Params: grant_type=password, username=admin,password=admin

  • Authorization: Basic Auth,Username=service-one,Password=123456

注意:Authorization跟client_credentials一样,也就是说先验证客户端的凭据,再验证用户的凭据。



GrantType: refresh_token,刷新令牌,我们用granttype=password测试时获得的refresh_token进行测试。

  • Url: http://localhost:8100/oauth/token

  • Method: POST

  • Params: grant_type=refresh_token,refresh_token=eyJhbGciOiJSUz...

  • Authorization: Basic Auth,Username=service-one,Password=123456



GrantType: sms_code,自定义认证,这里模拟的是短信验证码。微信、微博、支付宝等第三方登录,都可以按这个思路来实现。

  • Url: http://localhost:8100/oauth/token

  • Method: POST

  • Params: grant_type=sms_code,sms_code=123

  • Authorization: Basic Auth,Username=service-one,Password=123456

注:123是错误的sms_code,所以返回错误“SmsCode invalid!”,大家可以查看一下代码,看看正确的“短信验证码”是多少,随便把这个自定义认证方式多熟悉一下。



上一篇: 《Spring Cloud 微服务实践 (4) - OAuth2

下一篇: 《Spring Cloud 微服务实践 (6) - 资源服务器

发布于: 2020 年 09 月 23 日阅读数: 172
用户头像

xiaoboey

关注

IT老兵 2020.07.20 加入

资深Coder,爱好钓鱼逮鸟。

评论

发布
暂无评论
Spring Cloud 微服务实践(5) - 认证中心