文章目录
oauth2.0 协议的具体类容就不在这里展开说明了,请自行搜索一下。
本文主要记录如何使用 Spring Security 做授权服务器,简单理解就是颁发 token
引入依赖
pom 文件配置
spring-boot 版本:2.6.4
oauth 版本:2.2.7.RELEASE
注意:spring-boot 2.7 后,oauth service 就不再维护了,下文已此版本进行讨论
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.2.7.RELEASE</version>
</dependency>
查出用户相关数据
实现 UserDetailsService 接口
主要用途:获取用户信息,可从数据库查询
构成参数: 用户名、用户密码、 用户权限,可自定义构造方法
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String userName) {
User userDetails = new User( userName, "password", new ArrayList<>());
return userDetails;
}
}
自定义的身份验证逻辑
实现 AuthenticationProvider 接口
主要用途:校验校验账号密码、用户权限等
- 校验成功后,生成 UserDetails,返回 UsernamePasswordAuthenticationToken
- 校验失败抛出异常,AccountExpiredException:帐户已过期、LockedException:账号被锁定、CredentialsExpiredException:帐户凭据已过期、DisabledException:帐户被禁用等
- 也可以自定义异常,需要继承 AuthenticationException
- supports():多个 Provider 时有用,一个则可以直接返回 true。详解:如果当前的 AuthenticationProvider 支持作为 Authentication 对象而提供的类型,则可以实现此方法以返回true。注意,即使该方法对一个对象返回 true,authenticate()方法仍然有可能通过返回null来拒绝请求。Spring Security这样的设计是较为灵活的,使得我们可以实现一个 AuthenticationProvider,它可以根据请求的详细信息来拒绝身份验证请求,而不仅仅是根据请求的类型来判断。
@Service("userAuthProvider")
@Slf4j
public class UserAuthProvider implements AuthenticationProvider {
@Autowired
private UserDetailsServiceImpl iUserDetailsService;
@Autowired
private IUserMapper userMapper;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String userName = authentication.getName();
String password = (String) authentication.getCredentials();
UserPO userPO = userMapper.selectByName(userName);
if (Optional.ofNullable(userPO).isPresent() && password.equals(userPO.getPassword())) {
UserDetails userDetails = iUserDetailsService.loadUserByUsername(userName);
log.info("登录成功");
return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
} else {
throw new DisabledException("报错");
}
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
WebSecurityConfigurerAdapter
继承 WebSecurityConfigurerAdapter 类,并添加注解 @Configuration、@EnableWebSecurity
- @EnableWebSecurity:1、加载了WebSecurityConfiguration配置类, 配置安全认证策略;2、 加载了AuthenticationConfiguration, 配置了认证信息
- authenticationManagerBean():用于将生成 AuthenticationManager 对象
- AuthenticationManagerBuilder:引入 provider,下面例子引入了自定义的 userAuthProvider。详解:AuthenticationManager 的实现 ProviderManager 管理了众多的 AuthenticationProvider。每一个AuthenticationProvider 都只支持特定类型的 Authentication,如果不支持将会跳过。另一个作用就是对适配的 Authentication 进行认证,只要有一个认证成功,那么就认为认证成功
- WebSecurity:可以过滤一些不需要权限校验的资源
- **HttpSecurity.formLogin **:加载默认登录页面,没有过滤的接口或资源需要先通过校验后才能访问
- HttpSecurity.authorizeRequests:配置资源需要权限才能访问
- HttpSecurity.csrf:配置允许跨域
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
@Qualifier("userAuthProvider")
private UserAuthProvider userAuthProvider;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(userAuthProvider);
}
@Override
public void configure(WebSecurity web) {
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.permitAll()
.and()
.authorizeRequests()
.antMatchers("/test/**" ).permitAll()
.anyRequest()
.authenticated()
.and()
.csrf()
.disable()
.cors()
;
}
}
AuthorizationServerConfigurerAdapter
oauth2.0 服务点配置
- 继承 AuthorizationServerConfigurerAdapter
- 添加注解 @EnableAuthorizationServer
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
UserDetailsServiceImpl userDetailsService;
...
}
配置 token 校验方式
对应于配置 AuthorizationServer 安全认证的相关信息,创建 ClientCredentialsTokenEndpointFilter 核心过滤器
- allowFormAuthenticationForClients:允许客户表单认证,详解:使 /oauth/token 接口支持 client_id 和 client_secret 做登陆认证
- checkTokenAccess: 开放 /oauth/check_token?token=xxx 接口。此接口可以判断 token 是否有效
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.allowFormAuthenticationForClients()
.checkTokenAccess("permitAll()");
}
配置客户端
初始化客户端详情信息,注意:不建议同时使用多种模式
- jdbc:从数据库读取
- inMemory:从内存读取
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource);
clients
.inMemory()
.withClient(CLIENT_ID)
.authorizedGrantTypes("password", "refresh_token")
.scopes("read", "write", "trust")
.resourceIds(RESOURCE_ID)
.secret()
.accessTokenValiditySeconds(Math.toIntExact(TimeUnit.DAYS.toSeconds(30)))
.refreshTokenValiditySeconds(Math.toIntExact(TimeUnit.DAYS.toSeconds(30)));
}
一般来说,建议使用 jdbc 模式,方便增删改 client 信息
数据库添加表 oauth_client_details
CREATE TABLE `oauth_client_details` (
`client_id` VARCHAR ( 256 ) CHARACTER
SET utf8 NOT NULL,
`resource_ids` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
`client_secret` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
`scope` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
`authorized_grant_types` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
`web_server_redirect_uri` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
`authorities` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
`access_token_validity` INT ( 11 ) DEFAULT NULL,
`refresh_token_validity` INT ( 11 ) DEFAULT NULL,
`additional_information` VARCHAR ( 4096 ) CHARACTER
SET utf8 DEFAULT NULL,
`autoapprove` VARCHAR ( 256 ) CHARACTER
SET utf8 DEFAULT NULL,
PRIMARY KEY ( `client_id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
- client_id:主键,必须唯一,不能为空.
- resource_ids:设置 client 可以访问哪些资源服务,如果没设置,则可以访问所有,多个资源时用逗号(,)分隔
- client_secret:指定客户端(client)的访问密匙,加上 {noop} 表示不加密,如 {noop}123
- scope:指定客户端申请的权限范围,可选值包括read、write,、rust;若有多个权限范围用逗号(,)
- authorized_grant_types:指定客户端支持的 grant_type,可选值包括authorization_code、password,refresh_token、implicit、client_credentials, 若支持多个grant_type用逗号(,)分隔
- web_server_redirect_uri:客户端的重定向URI,可为空,多个则用逗号隔开
- authorities:客户端所拥有的 Spring Security 的权限值,类似于角色,若有多个权限值用逗号(,)分隔
- access_token_validity 设定客户端的 access_token 的有效时间值(单位:秒),若不设定值则使用默认的有效时间值(60 * 60 * 12, 12小时)
- refresh_token_validity:设定客户端的refresh_token的有效时间值(单位:秒),若不设定值则使用默认的有效时间值(60 * 60 * 24 * 30, 30天);若客户端的 grant_type 不包括 refresh_token,则不用关心该字段
- additional_information:这是一个预留的字段,在 Oauth 的流程中没有实际的使用,但若设置值,必须是JSON格式的数据
- autoapprove:设置用户是否自动 Approval 操作,默认值为 ‘false’,可选值包括 ‘true’,‘false’, ‘read’,‘write’.
该字段只适用于 grant_type=“authorization_code” 即授权码的情况,当用户登录成功后,若该值为’true’或支持的scope值,则会跳过用户 Approve 的页面, 直接授权.
配置 JWT 转换器
JWT 有多种生产策略,以下介绍两种
关键字加密
- SigningKey:只是用来验签,不是用来加密的,jwt里不要放敏感信息
private static final String SIGNING_KEY = "fat";
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey(SIGNING_KEY);
return jwtAccessTokenConverter;
}
对称加密
- KeyPair:密钥对
- jwt.jks:此文件放在 resources 文件夹下,生成策略不在这里详述
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(
new ClassPathResource("jwt.jks"), "key".toCharArray())
.getKeyPair("auth-server");
converter.setKeyPair(keyPair);
return converter;
}
Token存储管理
定义 Token 的存储方式
- InMemoryTokenStore:存储在内存
- JdbcTokenStore:存储在持久层数据库
- RedisTokenStore:存储在 Redis
- JwtTokenStore:不存储 token,使用算数方式验证 token
@Bean
public TokenStore tokenStore() {
InMemoryTokenStore tokenStore = new InMemoryTokenStore();
return tokenStore;
}
若使用数据库方式存储即 JdbcTokenStore 时,需要创建 oauth_access_token、oauth_refresh_token 两个表,分别存储 access_token 和 refresh_token
oauth_access_token
- create_time:数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成(扩展字段)
- token_id:该字段的值是将 access_token 的值通过MD5加密后存储的
- token:存储将 OAuth2AccessToken.java 对象序列化后的二进制数据,是真实的 AccessToken 的数据值.
- authentication_id:该字段具有唯一性,其值是根据当前的username(如果有),client_id 与 scope 通过 MD5 加密生成的
- user_name:登录时的用户名,若客户端没有用户名(如grant_type=“client_credentials”),则该值等于空
- client_id:用于唯一标识每一个客户端
- authentication:存储将 OAuth2Authentication.java 对象序列化后的二进制数据.
- refresh_token:该字段的值是将 refresh_token 的值通过MD5加密后存储的.
CREATE TABLE oauth_access_token (
create_time TIMESTAMP DEFAULT now(),
token_id VARCHAR ( 255 ),
token BLOB,
authentication_id VARCHAR ( 255 ) UNIQUE,
user_name VARCHAR ( 255 ),
client_id VARCHAR ( 255 ),
authentication BLOB,
refresh_token VARCHAR ( 255 )
) ENGINE = INNODB DEFAULT CHARSET = utf8;
oauth_refresh_token
如果客户端的 grant_type 不支持 refresh_token,则不会使用该表
- oauth_refresh_token:create_time 数据的创建时间,精确到秒,由数据库在插入数据时取当前系统时间自动生成
- token_id:该字段的值是将 refresh_token 的值通过MD5加密后存储的
- token:存储将OAuth2RefreshToken.java 对象序列化后的二进制数据
- authentication:存储将 OAuth2Authentication.java 对象序列化后的二进制数据.
CREATE TABLE oauth_refresh_token (
create_time TIMESTAMP DEFAULT now(),
token_id VARCHAR ( 255 ),
token BLOB,
authentication BLOB
) ENGINE = INNODB DEFAULT CHARSET = utf8;
配置授权端点
配置授权服务器端点的属性和增强功能,主要用来来配置令牌(token)的访问端点和令牌服务(token services)
- 密码模式必须配置 authenticationManager,authenticationManager 在 WebSecurityConfigurerAdapter 已创建
- 默认 token 使用 UUID
- 自定义的主要扩展点使用 TokenEnhancer
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(tokenStore());
endpoints
.authenticationManager(authenticationManager)
.tokenServices(tokenServices)
;
}
JWT 配置
- 创建 jwtAccessTokenConverter,实现 TokenEnhancer 接口
- TokenEnhancerChain 添加 jwtAccessTokenConverter,注意:TokenEnhancerChain 添加需要穿 List
- tokenServices 添加 TokenEnhancerChain
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
enhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter()));
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenEnhancer(enhancerChain);
tokenServices.setTokenStore(tokenStore());
endpoints
.authenticationManager(authenticationManager)
.tokenServices(tokenServices)
;
}
refreshToken 配置
- support:默认为 false,设置为 true 后,可以使用 refresh_token 来换取新 token
- reuse:默认为 true,refreshToken 过期前都可以换取新的 accessToken;修改为 false,refreshToken 只能使用一次
tokenServices.setSupportRefreshToken(true);
tokenServices.setReuseRefreshToken(false);