Spring Security OAuth2之自定义令牌配置与使用JWT令牌替换默认令牌

生活中,最使人疲惫的往往不是道路的遥远,而是心中的郁闷;最使人痛苦的往往不是生活的不幸,而是希望的破灭;最使人颓废的往往不是前途的坎坷,而是自信的丧失;最使人绝望的往往不是挫折的打击,而是心灵的死亡。所以我们要有自己的梦想,让梦想的星光指引着我们走出落漠,走出惆怅,带着我们走进自己的理想。

导读:本篇文章讲解 Spring Security OAuth2之自定义令牌配置与使用JWT令牌替换默认令牌,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com,来源:原文

搭建基本的认证与资源服务

添加依赖

        <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_tokenrefresh_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"
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/136860.html

(0)
飞熊的头像飞熊bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!