Spring Security oauth2.0 服务端


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);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值