Spring Security OAuth2之自定义令牌配置与使用JWT令牌替换默认令牌
搭建基本的认证与资源服务
添加依赖
<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.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
搭建认证服务
创建MyAuthorizationServerConfig
类并继承WebSecurityConfigurerAdapter
,同时实用@EnableAuthorizationServer
注解申明这里一个认证服务配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义MyUserDetailService,实现账号登录相关校验逻辑,预设密码必须为123456,且拥有admin权限,使用任意账号即可登录。
@Service
public class MyUserDetailService implements UserDetailsService {
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return new User(username, this.passwordEncoder.encode("123456"),
true, true, true,
true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
配置客户端,若不配置指定client-id和client-secret,则会随机分配
security:
oauth2:
client:
client-id: web
client-secret: 123456789
registered-redirect-uri: http://127.0.0.1:8888/test
搭建资源服务
定义资源一个资源类,用于访问测试。
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("index")
public Object index(Authentication authentication) {
return authentication;
}
}
创建MyResourceServerConfig
类继承ResourceServerConfigurerAdapter
类,同时使用@EnableResourceServer
注解申明这是一个资源服务配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
@Configuration
@EnableResourceServer
public class MyResourceServerConfig extends ResourceServerConfigurerAdapter {
}
配置指定资源服务器地址
security:
oauth2:
resource:
user-info-uri: http://127.0.0.1:8888
自定义令牌配置
配置认证服务器
创建MySecurityConfig
配置类
@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Bean(name = BeanIds.AUTHENTICATION_MANAGER)
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
创建AuthorizationServerConfig
类继承AuthorizationServerConfigurerAdapter
适配器,同时重写configure()
相关的2个方法
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MyUserDetailService userDetailService;
/**
* 指定AuthenticationManage和UserDetailService
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(userDetailService);
}
/**
* 创建两个客户端:web app,不同客户端获取不同的令牌
* web客户端令牌有效时间为3600秒。app客户端令牌有效时间为7200秒。
* web客户端的的refresh_token有效时间为86400*7秒,在规定时间内可通过refresh_token换取新的令牌
* web客户端获取令牌时,scope只能指定为all、a、b中的某个值,否则将获取失败
* web客户端只能通过密码模式(password)来获取令牌,而app客户端无限制
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400 * 7)
.scopes("all", "a", "b")
.authorizedGrantTypes("password")
.and()
.withClient("app")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(7200);
}
}
在新版本的spring-cloud-starter-oauth2
指定client_secret
需要进行加密处理,否则将如此如下错误:
1.请求出现异常
{
"error": "unauthorized",
"error_description": "Full authentication is required to access this resource"
}
2.控制台输出告警
2022-10-22 13:15:48.489 WARN 22500 --- [qtp645715998-37] o.s.s.c.bcrypt.BCryptPasswordEncoder : Encoded password does not look like BCrypt
执行测试
请求头添加:key=Authorization
,value=Basic加上client_id:client_secret
经过base64加密后的值
base64加密:https://1024tools.com/base64
web:123456789
编码后得到:d2ViOjEyMzQ1Njc4OQ==
Post请求,得到Token
{
"access_token": "853fde5b-f385-4196-9da8-9a84d85e4f36",
"token_type": "bearer",
"expires_in": 3599,
"scope": "all"
}
将scope指定为配置中不存在的一个值
{
"error": "invalid_scope",
"error_description": "Invalid scope: q",
"scope": "all a b"
}
app:123456789
编码后得到:YXBwOjEyMzQ1Njc4OQ==
Post请求,得到Token,注意这里的scope是没有限制的
{
"access_token": "6e9d5c74-c999-4655-8894-ede8c59ceb9a",
"token_type": "bearer",
"expires_in": 7200,
"scope": "b"
}
令牌存储策略
令牌默认是存储在内存中的,可以将它保存到第三方存储中,通常使用Redis。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
创建令牌通存储策略配置
@Configuration
public class TokenStoreConfig {
@Autowired
private RedisConnectionFactory redisConnectionFactory;
@Bean
public TokenStore redisTokenStore (){
return new RedisTokenStore(redisConnectionFactory);
}
}
配置认证服务器
在认证服务器指定要求使用该令牌存储策略
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MyUserDetailService userDetailService;
@Autowired
private TokenStore redisTokenStore;
/**
* 指定AuthenticationManage和UserDetailService
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(redisTokenStore)
.userDetailsService(userDetailService);
}
/**
* 创建两个客户端:web app,不同客户端获取不同的令牌
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400 * 7)
.scopes("all", "a", "b")
.authorizedGrantTypes("password")
.and()
.withClient("app")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(7200);
}
}
执行测试
重启项目,获取令牌,查看Redis中是否存储了令牌信息
Redis:0>keys *
1) "client_id_to_access:web"
2) "access:d816292d-9487-4def-b680-897025382778"
3) "auth:d816292d-9487-4def-b680-897025382778"
4) "auth_to_access:81b695a6db737a1bed92d38ec9beb09d"
5) "uname_to_access:web:test_user"
JWT替换默认令牌
默认令牌使用UUID生成,使用JWT替换默认的令牌,只需要指定TokenStore为JwtTokenStore即可。
创建JWTokenConfig配置类
@Configuration
public class JWTokenConfig {
/**
* 注意:若定义了redisTokenStore,则此处需要使用@Primary申明,默认优先使用jwtTokenStore令牌存储策略
* @return
*/
@Bean
@Primary
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// 设置签名密钥
accessTokenConverter.setSigningKey("signingKey");
return accessTokenConverter;
}
}
配置认证服务器
在认证服务器中指定使用JWTokenConfig
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private MyUserDetailService userDetailService;
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
/**
* 指定AuthenticationManage和UserDetailService
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.userDetailsService(userDetailService);
}
/**
* 创建两个客户端:web app,不同客户端获取不同的令牌
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400 * 7)
.scopes("all", "a", "b")
.authorizedGrantTypes("password")
.and()
.withClient("app")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(7200);
}
}
获取令牌,返回如下格式令牌
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjY0NTUyMzIsInVzZXJfbmFtZSI6InRlc3RfdXNlciIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImQ2Y2U1MmNmLWQxNmItNDNiOS1iMjM1LTg3MWNkMzQ0MTljNiIsImNsaWVudF9pZCI6IndlYiIsInNjb3BlIjpbImEiXX0.du3OtENDz8VHjZ3QSWBe491mFDirt_9xmq-06bG1puc",
"token_type": "bearer",
"expires_in": 3599,
"scope": "a",
"jti": "d6ce52cf-d16b-43b9-b235-871cd34419c6"
}
访问https://jwt.io/
,将access_token
中的内容复制并解析
JWT扩展增强
Token解析将得到PAYLOAD,如果想在JWT中添加额外信息,需要实现TokenEnhancer,相当于是一个Token增强器
创建JWTokenEnhancer
创建JWTokenEnhancer类,实现TokenEnhancer接口,重写enhance()方法,添加额外的JWT信息。
public class JWTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
Map<String, Object> info = new HashMap<>();
info.put("msg", "hello world Jwt");
DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken) oAuth2AccessToken;
defaultOAuth2AccessToken.setAdditionalInformation(info);
return oAuth2AccessToken;
}
}
配置JWTokenConfig
在JWTokenConfig
类里注册该JWTokenEnhancer对象
@Configuration
public class JWTokenConfig {
/**
* 注意:若定义了redisTokenStore,则此处需要使用@Primary申明,默认优先使用jwtTokenStore令牌存储策略
*
* @return
*/
@Bean
@Primary
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
// 设置签名密钥
accessTokenConverter.setSigningKey("signingKey");
return accessTokenConverter;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new JWTokenEnhancer();
}
}
认证服务器配置JWT增强器
@Autowired
private TokenStore jwtTokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private TokenEnhancer tokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> enhancers = new ArrayList<>();
enhancers.add(tokenEnhancer);
enhancers.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(enhancers);
endpoints.authenticationManager(authenticationManager)
.tokenStore(jwtTokenStore)
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain)
.userDetailsService(userDetailService);
}
执行测试
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiJoZWxsbyB3b3JsZCBKd3QiLCJ1c2VyX25hbWUiOiJ0ZXN0X3VzZXIiLCJzY29wZSI6WyJhIl0sImV4cCI6MTY2NjQ1NjI0NCwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZTA4YmQwNmItYjRhNi00NDFlLWI2YzUtZTdkOWQ3MjViZmFiIiwiY2xpZW50X2lkIjoid2ViIn0.tUsuBH_o6YTyhqhfEAwGrY69ij-HhYs52cTHvYAmzfI",
"token_type": "bearer",
"expires_in": 3599,
"scope": "a",
"msg": "hello world Jwt",
"jti": "e08bd06b-b4a6-441e-b6c5-e7d9d725bfab"
}
解析JWT
在Java代码中解析JWT
添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
解析JWT
解析JWT需要注意:signkey需要和JwtAccessTokenConverter中指定的签名密钥一致
@RestController
public class TestController {
@GetMapping("index")
public Object index(Authentication authentication, HttpServletRequest request) {
System.out.println("authentication = " + authentication.toString());
String header = request.getHeader("Authorization");
String token = StringUtils.substringAfter(header, "bearer ");
// signkey需要和JwtAccessTokenConverter中指定的签名密钥一致
return Jwts.parser().setSigningKey("signingKey".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
}
}
执行测试
先获取令牌
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiJoZWxsbyB3b3JsZCBKd3QiLCJ1c2VyX25hbWUiOiJ0ZXN0X3VzZXIiLCJzY29wZSI6WyJhIl0sImV4cCI6MTY2NjQ1Njc0MSwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNmE4YzRkMGUtMGZiZS00N2U1LTk2MzItNDAzODYyYTY4MTVlIiwiY2xpZW50X2lkIjoid2ViIn0.KDz0PCe53HBycIocxOoE8fV6OjyPZik4w9NBy3fiWbI",
"token_type": "bearer",
"expires_in": 3599,
"scope": "a",
"msg": "hello world Jwt",
"jti": "6a8c4d0e-0fbe-47e5-9632-403862a6815e"
}
视频生成的JWT令牌访问资源服务
{
"msg": "hello world Jwt",
"user_name": "test_user",
"scope": [
"a"
],
"exp": 1666456741,
"authorities": [
"admin"
],
"jti": "6a8c4d0e-0fbe-47e5-9632-403862a6815e",
"client_id": "web"
}
刷新令牌
令牌过期后需要使用refresh_token
从系统中换取一个新的可用令牌。
目前,系统是未返回refresh_token
,需要在认证服务器自定义配置。
配置认证服务器
只需要指定授权方式处,添加refresh_token,是支持四种标准的OAuth2获取令牌方式。
配置后,web客户端获取令牌时将返回
refresh_token
,refresh_token
有效期为10天,即10天之内可以用它换取新的可用令牌。
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("web")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(86400 * 7)
.scopes("all", "a", "b")
.authorizedGrantTypes("password","refresh_token")
.and()
.withClient("app")
.secret(new BCryptPasswordEncoder().encode("123456789"))
.accessTokenValiditySeconds(7200);
}
执行测试
重启项目,获取令牌
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiJoZWxsbyB3b3JsZCBKd3QiLCJ1c2VyX25hbWUiOiJ0ZXN0X3VzZXIiLCJzY29wZSI6WyJhIl0sImV4cCI6MTY2NjQ1NzIzOSwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNzRmMWYyNDgtZDZhNS00YWU4LWE0MDktZTY3NjE3OWYxYTY1IiwiY2xpZW50X2lkIjoid2ViIn0.DnNcgCGCUBGvdyC_N6DvKM0eH4IZcixEIZZ3eQyT0JI",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiJoZWxsbyB3b3JsZCBKd3QiLCJ1c2VyX25hbWUiOiJ0ZXN0X3VzZXIiLCJzY29wZSI6WyJhIl0sImF0aSI6Ijc0ZjFmMjQ4LWQ2YTUtNGFlOC1hNDA5LWU2NzYxNzlmMWE2NSIsImV4cCI6MTY2NzA1ODQzOSwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZGY5ZjIyMzUtMDhlOS00MDJmLWIwODAtMGM0MDM1MmY4MDA5IiwiY2xpZW50X2lkIjoid2ViIn0.WNLX61exdGm2tYNdwPVuQbM9T6z0ajV6IQcwqAYkPYs",
"expires_in": 3599,
"scope": "a",
"msg": "hello world Jwt",
"jti": "74f1f248-d6a5-4ae8-a409-e676179f1a65"
}
使用refresh_token
换取新的令牌
注意:请求头添加:key=Authorization
,value=Basic加上client_id:client_secret
经过base64加密后的值
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiJoZWxsbyB3b3JsZCBKd3QiLCJ1c2VyX25hbWUiOiJ0ZXN0X3VzZXIiLCJzY29wZSI6WyJhIl0sImV4cCI6MTY2NjQ1NzM1MiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiNDNlMTE0MjUtMzJkYy00MTI5LWEzOTItZmRkYzU3ZGU2NmVhIiwiY2xpZW50X2lkIjoid2ViIn0.0gIsZPQR1NgI3zi2FNf-TDxFROkMOQMnCNsKrwPKqm4",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtc2ciOiJoZWxsbyB3b3JsZCBKd3QiLCJ1c2VyX25hbWUiOiJ0ZXN0X3VzZXIiLCJzY29wZSI6WyJhIl0sImF0aSI6IjQzZTExNDI1LTMyZGMtNDEyOS1hMzkyLWZkZGM1N2RlNjZlYSIsImV4cCI6MTY2NzA1ODQzOSwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiZGY5ZjIyMzUtMDhlOS00MDJmLWIwODAtMGM0MDM1MmY4MDA5IiwiY2xpZW50X2lkIjoid2ViIn0.ZpC1nLPjBEZh3PhvlG-WUsHy7O93vsLstKwxOataqPM",
"expires_in": 3599,
"scope": "a",
"msg": "hello world Jwt",
"jti": "43e11425-32dc-4129-a392-fddc57de66ea"
}