SpringCloudOAuth2实现⽤户认证及单点登录
⽂章较长,建议推荐,建议转发,建议收藏,建议关注哈。
OAuth 2 有四种授权模式,分别是授权码模式(authorization code)、简化模式(implicit)、密码模式(resource owner password credentials)、客户端模式(client credentials),具体 OAuth2 是什么,可以参考这篇⽂章。()
本⽂我们将使⽤授权码模式和密码模式两种⽅式来实现⽤户认证和授权管理。
OAuth2 其实是⼀个关于授权的⽹络标准,它制定了设计思路和运⾏流程,利⽤这个标准我们其实是可以⾃⼰实现 OAuth2 的认证过程的。今天要介绍的 spring-cloud-starter-oauth2 ,其实是 Spring Cloud 按照 OAuth2 的标准并结合 spring-security 封装好的⼀个具体实现。
什么情况下需要⽤ OAuth2
⾸先⼤家最熟悉的就是⼏乎每个⼈都⽤过的,⽐如⽤登录、⽤ QQ 登录、⽤微博登录、⽤ Google 账号登录、⽤ github 授权登录等等,这些都是典型的 OAuth2 使⽤场景。假设我们做了⼀个⾃⼰的服务平台,如果不使⽤ OAuth2 登录⽅式,那么我们需要⽤户先完成注册,然后⽤注册号的账号密码或者⽤⼿机验证码登录。⽽使⽤了 OAuth2 之后,相信很多⼈使⽤过、甚⾄开发过⽹页服务、⼩程序,当我们进⼊⽹页、⼩程序界⾯,第⼀次使⽤就⽆需注册,直接使⽤授权登录即可,⼤⼤提⾼了使⽤效率。因为每个⼈都有号,有了就可以马上使⽤第三⽅服务,这体验不要太好了。⽽对于我们的服务来说,我们也不需要存储⽤户的密码,只要存储认证平台返回的唯⼀ID 和⽤户信息即可。
以上是使⽤了 OAuth2 的授权码模式,利⽤第三⽅的权威平台实现⽤户⾝份的认证。当然了,如果你的公司内部有很多个服务,可以专门提取出⼀个认证中⼼,这个认证中⼼就充当上⾯所说的权威认证平台的⾓⾊,所有的服务都要到这个认证中⼼做认证。
这样⼀说,发现没,这其实就是个单点登录的功能。这就是另外⼀种使⽤场景,对于多服务的平台,可以使⽤ OAuth2 实现服务的单点登录,只做⼀次登录,就可以在多个服务中⾃由穿⾏,当然仅限于授权范围内的服务和接⼝。
实现统⼀认证功能
本篇先介绍密码模式实现的单点登录,下⼀篇再继续说授权码模式。
在微服务横⾏的今天,谁敢说⾃⼰⼿上没⼏个微服务。微服务减少了服务间的耦合,同时也在某些⽅⾯增加了系统的复杂度,⽐如说⽤户认证。假设我们这⾥实现了⼀个电商平台,⽤户看到的就是⼀个 APP 或者⼀个 web 站点,实际上背后是由多个独⽴的服务构成的,⽐如⽤户服务、订单服务、产品服务等。⽤户只要第⼀次输⼊⽤户名、密码完成登录后,⼀段时间内,都可以任意访问各个页⾯,⽐如产品列表页⾯、我的订单页⾯、我的关注等页⾯。
我们可以想象⼀下,⾃然能够想到,在请求各个服务、各个接⼝的时候,⼀定携带着什么凭证,然后各个服务才知道请求接⼝的⽤户是哪个,不然肯定有问题,那其实这⾥⾯的凭证简单来说就是⼀个 Token,标识⽤户⾝份的 Token。
系统架构说明
认证中⼼:oauth2-auth-server,OAuth2 主要实现端,Token 的⽣成、刷新、验证都在认证中⼼完成。
订单服务:oauth2-client-order-server,微服务之⼀,接收到请求后会到认证中⼼验证。
⽤户服务:oauth2-client-user-server,微服务之⼆,接收到请求后会到认证中⼼验证。
客户端:例如 APP 端、web 端等终端
上图描述了使⽤了 OAuth2 的客户端与微服务间的请求过程。⼤致的过程就是客户端⽤⽤户名和密码到认证服务端换取 token,返回给客户端,客户端拿着 token 去各个微服务请求数据接⼝,⼀般这个 token 是放到 header 中的。当微服务接到请求后,先要拿着 token 去认证服务端检查 token 的合法性,如果合法,再根据⽤户所属的⾓⾊及具有的权限动态的返回数据。
创建并配置认证服务端
配置最多的就是认证服务端,验证账号、密码,存储 token,检查 token ,刷新 token 等都是认证服务端的⼯作。
1、引⼊需要的 maven 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
spring-cloud-starter-oauth2包含了spring-cloud-starter-security,所以不⽤再单独引⼊了。之所以引⼊ redis 包,是因为下⾯会介绍⼀种⽤ redis 存储 token 的⽅式。
2、配置好 l
将项⽬基本配置设置好,并加⼊有关 redis 的配置,稍后会⽤到。
spring:
application:
name: auth-server
redis:
database: 2
host: localhost
port: 32768
password: 1qaz@WSX
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
timeout: 100ms
server:
port: 6001
management:
endpoint:
health:
enabled: true
3、spring security 基础配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 允许匿名访问所有接⼝主要是 oauth 接⼝
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").permitAll();
}
}
使⽤@EnableWebSecurity注解修饰,并继承⾃WebSecurityConfigurerAdapter类。
这个类的重点就是声明PasswordEncoder和AuthenticationManager两个 Bean。稍后会⽤到。其中BCryptPasswordEncoder是⼀个密码加密⼯具类,它可以实现不可逆的加
密,AuthenticationManager是为了实现 OAuth2 的 password 模式必须要指定的授权管理 Bean。
4、实现 UserDetailsService
如果你之前⽤过 Security 的话,那肯定对这个类很熟悉,它是实现⽤户⾝份验证的⼀种⽅式,也是最简单⽅便的⼀种。另外还有结合AuthenticationProvider的⽅式,有机会讲Security 的时候再展开来讲吧。
UserDetailsService的核⼼就是loadUserByUsername⽅法,它要接收⼀个字符串参数,也就是传过来的⽤户名,返回⼀个UserDetails对象。
@Slf4j
@Component(value = "kiteUserDetailsService")
public class KiteUserDetailsService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("usernameis:" + username);
// 查询数据库操作
if(!username.equals("admin")){
throw new UsernameNotFoundException("the user is not found");
}else{
// ⽤户⾓⾊也应在数据库中获取
String role = "ROLE_ADMIN";
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority(role));
// 线上环境应该通过⽤户名查询数据库获取加密后的密码
String password = de("123456");
return new org.userdetails.User(username,password, authorities);
}
}
}
这⾥为了做演⽰,把⽤户名、密码和所属⾓⾊都写在代码⾥了,正式环境中,这⾥应该是从数据库或者其他地⽅根据⽤户名将加密后的密码及所属⾓⾊查出来的。账号 admin ,密码 123456,稍后在换取 token 的时候会⽤到。并且给这个⽤户设置 "ROLE_ADMIN" ⾓⾊。
5、OAuth2 配置⽂件
创建⼀个配置⽂件继承⾃AuthorizationServerConfigurerAdapter.
@Configuration
@EnableAuthorizationServer
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
public PasswordEncoder passwordEncoder;
@Autowired
public UserDetailsService kiteUserDetailsService;
@Autowiredspringcloud和springboot
private AuthenticationManager authenticationManager;
@Autowired
private TokenStore redisTokenStore;
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
/**
* redis token ⽅式
*/
endpoints.authenticationManager(authenticationManager)
.userDetailsService(kiteUserDetailsService)
.tokenStore(redisTokenStore);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("order-client")
.de("order-secret-8888"))
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
.
accessTokenValiditySeconds(3600)
.scopes("all")
.and()
.withClient("user-client")
.de("user-secret-8888"))
.authorizedGrantTypes("refresh_token", "authorization_code", "password")
.accessTokenValiditySeconds(3600)
.scopes("all");
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
}
}
有三个 configure ⽅法的重写。
AuthorizationServerEndpointsConfigurer参数的重写
endpoints.authenticationManager(authenticationManager)
.userDetailsService(kiteUserDetailsService)
.tokenStore(redisTokenStore);
authenticationManage() 调⽤此⽅法才能⽀持 password 模式。
userDetailsService()设置⽤户验证服务。
tokenStore()指定 token 的存储⽅式。
redisTokenStore Bean 的定义如下:
@Configuration
public class RedisTokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore (){
return new RedisTokenStore(redisConnectionFactory);
}
}
ClientDetailsServiceConfigurer参数的重写,在这⾥定义各个端的约束条件。包括
ClientId、Client-Secret:这两个参数对应请求端定义的 cleint-id 和 client-secret
authorizedGrantTypes 可以包括如下⼏种设置中的⼀种或多种:
authorization_code:授权码类型。
implicit:隐式授权类型。
password:资源所有者(即⽤户)密码类型。
client_credentials:客户端凭据(客户端ID以及Key)类型。
refresh_token:通过以上授权获得的刷新令牌来获取新的令牌。
accessTokenValiditySeconds:token 的有效期
scopes:⽤来限制客户端访问的权限,在换取的 token 的时候会带上 scope 参数,只有在 scopes 定义内的,才可以正常换取 token。
上⾯代码中是使⽤ inMemory ⽅式存储的,将配置保存到内存中,相当于硬编码了。正式环境下的做法是持久化到数据库中,⽐如 mysql 中。
具体的做法如下:
1. 在数据库中增加表,并插⼊数据
create table oauth_client_details (
client_id VARCHAR(256) 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)
);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('user-client', '$2a$10$o2l5kA7z.Caekp72h5kU7uqdTDrlamLq.57M1F6ulJln9tRtOJufq', 'all',
'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
INSERT INTO oauth_client_details
(client_id, client_secret, scope, authorized_grant_types,
web_server_redirect_uri, authorities, access_token_validity,
refresh_token_validity, additional_information, autoapprove)
VALUES
('order-client', '$2a$10$GoIOhjqFKVyrabUNcie8d.ADX.qZSxpYbO6YK4L2gsNzlCIxEUDlW', 'all',
'authorization_code,refresh_token,password', null, null, 3600, 36000, null, true);
注意: client_secret 字段不能直接是 secret 的原始值,需要经过加密。因为是⽤的BCryptPasswordEncoder,所以最终插⼊的值应该是经过de()之后的值。
2. 然后在配置⽂件 l 中添加关于数据库的配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cloud?characterEncoding=UTF-8&useSSL=false
username: root
password: password
hikari:
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
maximum-pool-size: 9
Spring Boot 2.0 之后默认使⽤ hikari 作为数据库连接池。如果使⽤其他连接池需要引⼊相关包,然后对应的增加配置。
3. 在 OAuth2 配置类(OAuth2Config)中增加 DataSource 的注⼊
@Autowired
private DataSource dataSource;
4. 将public void configure(ClientDetailsServiceConfigurer clients)重写⽅法修改为如下:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
JdbcClientDetailsServiceBuilder jcsb = clients.jdbc(dataSource);
jcsb.passwordEncoder(passwordEncoder);
}
还有⼀个重写的⽅法public void configure(AuthorizationServerSecurityConfigurer security),这个⽅法限制客户端访问认证接⼝的权限。
security.allowFormAuthenticationForClients();
security.checkTokenAccess("isAuthenticated()");
第⼀⾏代码是允许客户端访问 OAuth2 授权接⼝,否则请求 token 会返回 401。
第⼆⾏和第三⾏分别是允许已授权⽤户访问 checkToken 接⼝和获取 token 接⼝。
完成之后,启动项⽬,如果你⽤的是 IDEA 会在下⽅的 Mapping 窗⼝中看到 oauth2 相关的 RESTful 接⼝。
主要有如下⼏个:
POST /oauth/authorize 授权码模式认证授权接⼝
GET/POST /oauth/token 获取 token 的接⼝
POST /oauth/check_token 检查 token 合法性接⼝
创建⽤户客户端项⽬
上⾯创建完成了认证服务端,下⾯开始创建⼀个客户端,对应到我们系统中的业务相关的微服务。我们假设这个微服务项⽬是管理⽤户相关数据的,所以叫做⽤户客户端。
1、引⽤相关的 maven 包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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-redis</artifactId>
</dependency>
2、l 配置⽂件
spring:
application:
name: client-user
redis:
database: 2
host: localhost
port: 32768
password: 1qaz@WSX
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 0
timeout: 100ms
server:
port: 6101
servlet:
context-path: /client-user
security:
oauth2:
client:
client-id: user-client
client-secret: user-secret-8888
user-authorization-uri: localhost:6001/oauth/authorize
access-token-uri: localhost:6001/oauth/token
resource:
id: user-client
user-info-uri: user-info
authorization:
check-token-access: localhost:6001/oauth/check_token
上⾯是常规配置信息以及 redis 配置,重点是下⾯的 security 的配置,这⾥的配置稍有不注意就会出现 401 或者其他问题。
client-id、client-secret 要和认证服务中的配置⼀致,如果是使⽤ inMemory 还是 jdbc ⽅式。
user-authorization-uri 是授权码认证⽅式需要的,下⼀篇⽂章再说。
access-token-uri 是密码模式需要⽤到的获取 token 的接⼝。
authorization.check-token-access 也是关键信息,当此服务端接收到来⾃客户端端的请求后,需要拿着请求中的 token 到认证服务端做 token 验证,就是请求的这个接⼝3、资源配置⽂件
在 OAuth2 的概念⾥,所有的接⼝都被称为资源,接⼝的权限也就是资源的权限,所以 Spring Security OAuth2 中提供了关于资源的注解@EnableResourceServer,和
@EnableWebSecurity的作⽤类似。
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.client.client-id}")
private String clientId;
@Value("${security.oauth2.client.client-secret}")
private String secret;
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论