• 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.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.6.4version>
        <relativePath/>
    parent>
    
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-securityartifactId>
    dependency>
    
    <dependency>
        <groupId>org.springframework.security.oauth.bootgroupId>
        <artifactId>spring-security-oauth2-autoconfigureartifactId>
        <version>2.2.7.RELEASEversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

     

    查出用户相关数据

    实现 UserDetailsService 接口
    主要用途:获取用户信息,可从数据库查询
    构成参数: 用户名、用户密码、 用户权限,可自定义构造方法

    @Slf4j
    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String userName) {
            User userDetails = new User( userName, "password", new ArrayList<>());
            return userDetails;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

     

    自定义的身份验证逻辑

    实现 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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

     

    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()
            ;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

     

    AuthorizationServerConfigurerAdapter

    oauth2.0 服务点配置

    • 继承 AuthorizationServerConfigurerAdapter
    • 添加注解 @EnableAuthorizationServer
    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
        @Autowired
        private DataSource dataSource;
    
        @Autowired
        private AuthenticationManager authenticationManager;
    
        @Autowired
        UserDetailsServiceImpl userDetailsService;
    
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

     

    配置 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()");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

     

    配置客户端

    初始化客户端详情信息,注意:不建议同时使用多种模式

    • 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)));
                
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    一般来说,建议使用 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    对称加密

    • 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

     

    Token存储管理

    定义 Token 的存储方式

    • InMemoryTokenStore:存储在内存
    • JdbcTokenStore:存储在持久层数据库
    • RedisTokenStore:存储在 Redis
    • JwtTokenStore:不存储 token,使用算数方式验证 token
    @Bean
    public TokenStore tokenStore() {
        InMemoryTokenStore tokenStore = new InMemoryTokenStore();
        return tokenStore;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    若使用数据库方式存储即 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

     

    配置授权端点

    配置授权服务器端点的属性和增强功能,主要用来来配置令牌(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)
        ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

     
    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)
            ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    refreshToken 配置

    • support:默认为 false,设置为 true 后,可以使用 refresh_token 来换取新 token
    • reuse:默认为 true,refreshToken 过期前都可以换取新的 accessToken;修改为 false,refreshToken 只能使用一次
    tokenServices.setSupportRefreshToken(true);
    tokenServices.setReuseRefreshToken(false);
    
    • 1
    • 2
  • 相关阅读:
    idea module 重命名
    Redis的使用
    社交媒体安全:个人信息泄露与社交工程攻击的防范
    图的数据结构
    【LeetCode刷题】面试题 17.19. 消失的两个数字
    在项目中如何利用JS去修改CSS的属性值(二) --- :root+var()方法
    java毕业设计电动机营销系统Mybatis+系统+数据库+调试部署
    CS8416国产替代DP8416 数字音频接收器
    RFLA: Gaussian Receptive Field based Label Assignment for Tiny Object Detection
    Qt编写物联网管理平台36-通信协议
  • 原文地址:https://blog.csdn.net/weixin_42555971/article/details/127507385